Skip to content

Commit 076c7ec

Browse files
antonisclaude
andcommitted
fix(tracing): Skip native frames and stall tracking for unsampled spans
The NativeFrames and StallTracking integrations performed their full work (native bridge calls, 50ms polling loop) regardless of whether the span would be sampled, unlike the Profiling integration which already checks spanIsSampled() correctly. With tracesSampleRate: 0.2, this meant 80% of navigation spans triggered unnecessary fetchNativeFrames() bridge calls and stall tracking loop activations. On low-end devices these operations are significantly more expensive and contribute to the performance overhead reported in #5665. The fix adds a spanIsSampled() guard at the entry of fetchStartFramesForSpan in NativeFrames and _onSpanStart in StallTracking. Since end-frame fetching already bails when no start frames are found, the single guard in fetchStartFramesForSpan covers both start and end for NativeFrames. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cfd2d4f commit 076c7ec

4 files changed

Lines changed: 64 additions & 2 deletions

File tree

packages/core/src/js/tracing/integrations/nativeFrames.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Client, Event, Integration, Measurements, MeasurementUnit, Span } from '@sentry/core';
2-
import { debug, getRootSpan, spanToJSON, timestampInSeconds } from '@sentry/core';
2+
import { debug, getRootSpan, spanIsSampled, spanToJSON, timestampInSeconds } from '@sentry/core';
33
import type { NativeFramesResponse } from '../../NativeRNSentry';
44
import { AsyncExpiringMap } from '../../utils/AsyncExpiringMap';
55
import { isRootSpan } from '../../utils/span';
@@ -86,6 +86,10 @@ export const nativeFramesIntegration = (): Integration => {
8686
};
8787

8888
const fetchStartFramesForSpan = (span: Span): void => {
89+
if (!spanIsSampled(span)) {
90+
return;
91+
}
92+
8993
const spanId = span.spanContext().spanId;
9094
const spanType = isRootSpan(span) ? 'root' : 'child';
9195
debug.log(`[${INTEGRATION_NAME}] Fetching frames for ${spanType} span start (${spanId}).`);

packages/core/src/js/tracing/integrations/stalltracking.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable max-lines */
22
import type { Client, Integration, Measurements, MeasurementUnit, Span } from '@sentry/core';
3-
import { debug, getRootSpan, spanToJSON, timestampInSeconds } from '@sentry/core';
3+
import { debug, getRootSpan, spanIsSampled, spanToJSON, timestampInSeconds } from '@sentry/core';
44
import type { AppStateStatus } from 'react-native';
55
import { AppState } from 'react-native';
66
import { STALL_COUNT, STALL_LONGEST_TIME, STALL_TOTAL_TIME } from '../../measurements';
@@ -123,6 +123,10 @@ export const stallTrackingIntegration = ({
123123
return;
124124
}
125125

126+
if (!spanIsSampled(rootSpan)) {
127+
return;
128+
}
129+
126130
if (statsByRootSpan.has(rootSpan)) {
127131
debug.error(
128132
'[StallTracking] Tried to start stall tracking on a transaction already being tracked. Measurements might be lost.',

packages/core/test/tracing/integrations/nativeframes.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,31 @@ describe('NativeFramesInstrumentation', () => {
520520
}),
521521
);
522522
});
523+
524+
describe('unsampled spans', () => {
525+
beforeEach(() => {
526+
global.Date.now = jest.fn(() => mockDate.getTime());
527+
528+
getCurrentScope().clear();
529+
getIsolationScope().clear();
530+
getGlobalScope().clear();
531+
532+
const options = getDefaultTestClientOptions({
533+
tracesSampleRate: 0,
534+
enableNativeFramesTracking: true,
535+
integrations: [nativeFramesIntegration()],
536+
});
537+
client = new TestClient(options);
538+
setCurrentClient(client);
539+
client.init();
540+
});
541+
542+
it('does not fetch native frames for unsampled spans', () => {
543+
startSpan({ name: 'unsampled transaction', forceTransaction: true }, () => {
544+
// span starts and ends — no work expected
545+
});
546+
547+
expect(NATIVE.fetchNativeFrames).not.toHaveBeenCalled();
548+
});
549+
});
523550
});

packages/core/test/tracing/integrations/stallTracking/stalltracking.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,31 @@ describe('StallTracking', () => {
225225

226226
expect(client.eventQueue[0].measurements).toBeUndefined();
227227
});
228+
229+
it('does not track stalls for unsampled spans', async () => {
230+
getCurrentScope().clear();
231+
getIsolationScope().clear();
232+
getGlobalScope().clear();
233+
234+
const unsampledOptions = getDefaultTestClientOptions({
235+
tracesSampleRate: 0,
236+
enableStallTracking: true,
237+
integrations: [stallTrackingIntegration()],
238+
enableAppStartTracking: false,
239+
});
240+
const unsampledClient = new TestClient(unsampledOptions);
241+
setCurrentClient(unsampledClient);
242+
unsampledClient.init();
243+
244+
startSpan({ name: 'unsampled transaction', forceTransaction: true }, () => {
245+
expensiveOperation();
246+
jest.runOnlyPendingTimers();
247+
});
248+
249+
await unsampledClient.flush();
250+
251+
// Event is either not sent or has no stall measurements
252+
const event = unsampledClient.eventQueue[0];
253+
expect(event?.measurements?.stall_count).toBeUndefined();
254+
});
228255
});

0 commit comments

Comments
 (0)