Skip to content

Commit a4efccc

Browse files
committed
fix(browser): Fix idle span ending
1 parent 48b0a35 commit a4efccc

File tree

3 files changed

+59
-49
lines changed

3 files changed

+59
-49
lines changed

packages/core/src/tracing/idleSpan.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -113,37 +113,48 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
113113
const previousActiveSpan = getActiveSpan();
114114
const span = _startIdleSpan(startSpanOptions);
115115

116-
function _endSpan(timestamp: number = timestampInSeconds()): void {
117-
// Ensure we end with the last span timestamp, if possible
118-
const spans = getSpanDescendants(span).filter(child => child !== span);
116+
// We patch span.end to ensure we can run some things before the span is ended
117+
// eslint-disable-next-line @typescript-eslint/unbound-method
118+
span.end = new Proxy(span.end, {
119+
apply(target, thisArg, args: Parameters<Span['end']>) {
120+
if (beforeSpanEnd) {
121+
beforeSpanEnd(span);
122+
}
119123

120-
// If we have no spans, we just end, nothing else to do here
121-
if (!spans.length) {
122-
span.end(timestamp);
123-
return;
124-
}
124+
const timestamp = args[0] || timestampInSeconds();
125+
const spanEndTimestamp = spanTimeInputToSeconds(timestamp);
125126

126-
const childEndTimestamps = spans
127-
.map(span => spanToJSON(span).timestamp)
128-
.filter(timestamp => !!timestamp) as number[];
129-
const latestSpanEndTimestamp = childEndTimestamps.length ? Math.max(...childEndTimestamps) : undefined;
130-
131-
const spanEndTimestamp = spanTimeInputToSeconds(timestamp);
132-
// In reality this should always exist here, but type-wise it may be undefined...
133-
const spanStartTimestamp = spanToJSON(span).start_timestamp;
134-
135-
// The final endTimestamp should:
136-
// * Never be before the span start timestamp
137-
// * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp
138-
// * Otherwise be the passed end timestamp
139-
// Final timestamp can never be after finalTimeout
140-
const endTimestamp = Math.min(
141-
spanStartTimestamp ? spanStartTimestamp + finalTimeout / 1000 : Infinity,
142-
Math.max(spanStartTimestamp || -Infinity, Math.min(spanEndTimestamp, latestSpanEndTimestamp || Infinity)),
143-
);
144-
145-
span.end(endTimestamp);
146-
}
127+
// Ensure we end with the last span timestamp, if possible
128+
const spans = getSpanDescendants(span).filter(child => child !== span);
129+
130+
// If we have no spans, we just end, nothing else to do here
131+
if (!spans.length) {
132+
onIdleSpanEnded(spanEndTimestamp);
133+
return Reflect.apply(target, thisArg, args);
134+
}
135+
136+
const childEndTimestamps = spans
137+
.map(span => spanToJSON(span).timestamp)
138+
.filter(timestamp => !!timestamp) as number[];
139+
const latestSpanEndTimestamp = childEndTimestamps.length ? Math.max(...childEndTimestamps) : undefined;
140+
141+
// In reality this should always exist here, but type-wise it may be undefined...
142+
const spanStartTimestamp = spanToJSON(span).start_timestamp;
143+
144+
// The final endTimestamp should:
145+
// * Never be before the span start timestamp
146+
// * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp
147+
// * Otherwise be the passed end timestamp
148+
// Final timestamp can never be after finalTimeout
149+
const endTimestamp = Math.min(
150+
spanStartTimestamp ? spanStartTimestamp + finalTimeout / 1000 : Infinity,
151+
Math.max(spanStartTimestamp || -Infinity, Math.min(spanEndTimestamp, latestSpanEndTimestamp || Infinity)),
152+
);
153+
154+
onIdleSpanEnded(endTimestamp);
155+
return Reflect.apply(target, thisArg, [endTimestamp]);
156+
},
157+
});
147158

148159
/**
149160
* Cancels the existing idle timeout, if there is one.
@@ -173,7 +184,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
173184
_idleTimeoutID = setTimeout(() => {
174185
if (!_finished && activities.size === 0 && _autoFinishAllowed) {
175186
_finishReason = FINISH_REASON_IDLE_TIMEOUT;
176-
_endSpan(endTimestamp);
187+
span.end(endTimestamp);
177188
}
178189
}, idleTimeout);
179190
}
@@ -186,7 +197,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
186197
_idleTimeoutID = setTimeout(() => {
187198
if (!_finished && _autoFinishAllowed) {
188199
_finishReason = FINISH_REASON_HEARTBEAT_FAILED;
189-
_endSpan(endTimestamp);
200+
span.end(endTimestamp);
190201
}
191202
}, childSpanTimeout);
192203
}
@@ -223,21 +234,17 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
223234
}
224235
}
225236

226-
function onIdleSpanEnded(): void {
237+
function onIdleSpanEnded(endTimestamp: number): void {
227238
_finished = true;
228239
activities.clear();
229240

230-
if (beforeSpanEnd) {
231-
beforeSpanEnd(span);
232-
}
233-
234241
_setSpanForScope(scope, previousActiveSpan);
235242

236243
const spanJSON = spanToJSON(span);
237244

238-
const { timestamp: endTimestamp, start_timestamp: startTimestamp } = spanJSON;
245+
const { start_timestamp: startTimestamp } = spanJSON;
239246
// This should never happen, but to make TS happy...
240-
if (!endTimestamp || !startTimestamp) {
247+
if (!startTimestamp) {
241248
return;
242249
}
243250

@@ -267,7 +274,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
267274

268275
// Add a delta with idle timeout so that we prevent false positives
269276
const timeoutWithMarginOfError = (finalTimeout + idleTimeout) / 1000;
270-
const spanEndedBeforeFinalTimeout = childEndTimestamp - childStartTimestamp < timeoutWithMarginOfError;
277+
const spanEndedBeforeFinalTimeout = childEndTimestamp - childStartTimestamp <= timeoutWithMarginOfError;
271278

272279
if (DEBUG_BUILD) {
273280
const stringifiedSpan = JSON.stringify(childSpan, undefined, 2);
@@ -312,10 +319,6 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
312319
}
313320

314321
_popActivity(endedSpan.spanContext().spanId);
315-
316-
if (endedSpan === span) {
317-
onIdleSpanEnded();
318-
}
319322
});
320323

321324
client.on('idleSpanEnableAutoFinish', spanToAllowAutoFinish => {
@@ -338,7 +341,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
338341
if (!_finished) {
339342
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' });
340343
_finishReason = FINISH_REASON_FINAL_TIMEOUT;
341-
_endSpan();
344+
span.end();
342345
}
343346
}, finalTimeout);
344347

packages/core/src/tracing/sentrySpan.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,10 @@ export class SentrySpan implements Span {
342342

343343
if (hasMeasurements) {
344344
DEBUG_BUILD &&
345-
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2));
345+
logger.log(
346+
'[Measurements] Adding measurements to transaction event',
347+
JSON.stringify(measurements, undefined, 2),
348+
);
346349
transaction.measurements = measurements;
347350
}
348351

packages/core/test/lib/tracing/idleSpan.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ describe('startIdleSpan', () => {
122122
span.setAttribute('foo', 'bar');
123123
// Try adding a child here - we do this in browser tracing...
124124
const inner = startInactiveSpan({ name: 'from beforeSpanEnd', startTime: baseTimeInSeconds });
125-
inner?.end(baseTimeInSeconds);
125+
inner.end(baseTimeInSeconds + 1);
126126
});
127-
const idleSpan = startIdleSpan({ name: 'idle span 2', startTime: baseTimeInSeconds }, { beforeSpanEnd });
127+
const idleSpan = startIdleSpan({ name: 'idle span', startTime: baseTimeInSeconds }, { beforeSpanEnd });
128128
expect(idleSpan).toBeDefined();
129129

130130
expect(beforeSpanEnd).not.toHaveBeenCalled();
@@ -141,6 +141,10 @@ describe('startIdleSpan', () => {
141141
expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
142142
const transaction = transactions[0];
143143

144+
expect(transaction.start_timestamp).toBe(baseTimeInSeconds);
145+
// It considers the end time of the span we added in beforeSpanEnd
146+
expect(transaction.timestamp).toBe(baseTimeInSeconds + 1);
147+
144148
expect(transaction.contexts?.trace?.data).toEqual(
145149
expect.objectContaining({
146150
foo: 'bar',
@@ -178,9 +182,9 @@ describe('startIdleSpan', () => {
178182
});
179183

180184
// discardedSpan - startTimestamp is too large
181-
const discardedSpan = startInactiveSpan({ name: 'discarded span', startTime: baseTimeInSeconds + 99 });
185+
const discardedSpan = startInactiveSpan({ name: 'discarded span 1', startTime: baseTimeInSeconds + 99 });
182186
// discardedSpan2 - endTime is too large
183-
const discardedSpan2 = startInactiveSpan({ name: 'discarded span', startTime: baseTimeInSeconds + 3 });
187+
const discardedSpan2 = startInactiveSpan({ name: 'discarded span 2', startTime: baseTimeInSeconds + 3 });
184188
discardedSpan2.end(baseTimeInSeconds + 99)!;
185189

186190
// Should be cancelled - will not finish

0 commit comments

Comments
 (0)