Skip to content

Commit 9b30bd7

Browse files
authored
fix(core,browser): Delete SentryNonRecordingSpan from fetch/xhr map (#19336)
This resolves a leak where `SentryNonRecordingSpan` are pilled up when `tracingSampleRate` is set to `0`. Theoretically `SentryNonRecordingSpan` are still treated as spans and added to the `spans` list, but never removed By moving `shouldCreateSpanResult` closer to the actual span logic, this is now resolved. Closes #19337 (added automatically)
1 parent 1063860 commit 9b30bd7

3 files changed

Lines changed: 83 additions & 15 deletions

File tree

packages/browser/src/tracing/request.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,24 +332,28 @@ function xhrCallback(
332332

333333
const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(url);
334334

335-
// check first if the request has finished and is tracked by an existing span which should now end
336-
if (handlerData.endTimestamp && shouldCreateSpanResult) {
335+
// Handle XHR completion - clean up spans from the record
336+
if (handlerData.endTimestamp) {
337337
const spanId = xhr.__sentry_xhr_span_id__;
338338
if (!spanId) return;
339339

340340
const span = spans[spanId];
341-
if (span && sentryXhrData.status_code !== undefined) {
342-
setHttpStatus(span, sentryXhrData.status_code);
343-
span.end();
344341

345-
onRequestSpanEnd?.(span, {
346-
headers: createHeadersSafely(parseXhrResponseHeaders(xhr as XMLHttpRequest & SentryWrappedXMLHttpRequest)),
347-
error: handlerData.error,
348-
});
342+
if (span) {
343+
if (shouldCreateSpanResult && sentryXhrData.status_code !== undefined) {
344+
setHttpStatus(span, sentryXhrData.status_code);
345+
span.end();
346+
347+
onRequestSpanEnd?.(span, {
348+
headers: createHeadersSafely(parseXhrResponseHeaders(xhr as XMLHttpRequest & SentryWrappedXMLHttpRequest)),
349+
error: handlerData.error,
350+
});
351+
}
349352

350353
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
351354
delete spans[spanId];
352355
}
356+
353357
return undefined;
354358
}
355359

packages/core/src/fetch.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,23 @@ export function instrumentFetchRequest(
8181

8282
const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(url);
8383

84-
if (handlerData.endTimestamp && shouldCreateSpanResult) {
84+
if (handlerData.endTimestamp) {
8585
const spanId = handlerData.fetchData.__span;
8686
if (!spanId) return;
8787

8888
const span = spans[spanId];
89-
if (span) {
90-
endSpan(span, handlerData);
9189

92-
_callOnRequestSpanEnd(span, handlerData, spanOriginOrOptions);
90+
if (span) {
91+
// Only end the span and call hooks if we're actually recording
92+
if (shouldCreateSpanResult) {
93+
endSpan(span, handlerData);
94+
_callOnRequestSpanEnd(span, handlerData, spanOriginOrOptions);
95+
}
9396

9497
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
9598
delete spans[spanId];
9699
}
100+
97101
return undefined;
98102
}
99103

packages/core/test/lib/fetch.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { describe, expect, it, vi } from 'vitest';
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { HandlerDataFetch } from '../../src';
23
import { _addTracingHeadersToFetchRequest, instrumentFetchRequest } from '../../src/fetch';
34
import type { Span } from '../../src/types-hoist/span';
45

5-
const { DEFAULT_SENTRY_TRACE, DEFAULT_BAGGAGE } = vi.hoisted(() => ({
6+
const { DEFAULT_SENTRY_TRACE, DEFAULT_BAGGAGE, hasSpansEnabled } = vi.hoisted(() => ({
67
DEFAULT_SENTRY_TRACE: 'defaultTraceId-defaultSpanId-1',
78
DEFAULT_BAGGAGE: 'sentry-trace_id=defaultTraceId,sentry-sampled=true,sentry-sample_rate=0.5,sentry-sample_rand=0.232',
9+
hasSpansEnabled: vi.fn(),
810
}));
911

1012
const CUSTOM_SENTRY_TRACE = '123-abc-1';
@@ -23,7 +25,18 @@ vi.mock('../../src/utils/traceData', () => {
2325
};
2426
});
2527

28+
vi.mock('../../src/utils/hasSpansEnabled', () => {
29+
return {
30+
hasSpansEnabled,
31+
};
32+
});
33+
2634
describe('_addTracingHeadersToFetchRequest', () => {
35+
beforeEach(() => {
36+
vi.clearAllMocks();
37+
hasSpansEnabled.mockReturnValue(false);
38+
});
39+
2740
describe('when request is a string', () => {
2841
describe('and no request headers are set', () => {
2942
it.each([
@@ -412,6 +425,53 @@ describe('_addTracingHeadersToFetchRequest', () => {
412425
});
413426

414427
describe('instrumentFetchRequest', () => {
428+
describe('span cleanup', () => {
429+
it.each([
430+
{ name: 'non-recording', hasTracingEnabled: false },
431+
{ name: 'recording', hasTracingEnabled: true },
432+
])('cleans up $name spans from the spans record when fetch completes', ({ hasTracingEnabled }) => {
433+
hasSpansEnabled.mockReturnValue(hasTracingEnabled);
434+
435+
const spans: Record<string, Span> = {};
436+
437+
const handlerData = {
438+
fetchData: { url: '/api/test', method: 'GET' },
439+
args: ['/api/test'] as unknown[],
440+
startTimestamp: Date.now(),
441+
};
442+
443+
instrumentFetchRequest(
444+
handlerData,
445+
() => true,
446+
() => false,
447+
spans,
448+
{ spanOrigin: 'auto.http.fetch' },
449+
);
450+
451+
// @ts-expect-error -- we know it exists
452+
const spanId = handlerData.fetchData.__span;
453+
454+
expect(spanId).toBeDefined();
455+
expect(spans[spanId]).toBeDefined();
456+
457+
const completedHandlerData: HandlerDataFetch = {
458+
...handlerData,
459+
endTimestamp: Date.now() + 100,
460+
response: { status: 200, headers: new Headers() } as Response,
461+
};
462+
463+
instrumentFetchRequest(
464+
completedHandlerData,
465+
() => true,
466+
() => false,
467+
spans,
468+
{ spanOrigin: 'auto.http.fetch' },
469+
);
470+
471+
expect(spans[spanId]).toBeUndefined();
472+
});
473+
});
474+
415475
describe('options object mutation', () => {
416476
it('does not mutate the original options object', () => {
417477
const originalOptions = { method: 'POST', body: JSON.stringify({ data: 'test' }) };

0 commit comments

Comments
 (0)