Skip to content

Commit 4cebd92

Browse files
antonisclaude
andauthored
fix(core): App start data lost when first transaction is unsampled (#5756)
* fix(core): Skip unsampled spans when recording first root span ID for app start When `tracesSampleRate < 1.0`, the first root span could be unsampled, permanently locking `firstStartedActiveRootSpanId` to a span that would never reach `processEvent`. This caused app start data to be lost for the entire session. Now we check `spanIsSampled()` before locking the ID, so app start attaches to the first sampled root span instead. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(core): Update CHANGELOG entry wording Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 29950e3 commit 4cebd92

File tree

3 files changed

+51
-0
lines changed

3 files changed

+51
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
### Fixes
4747

48+
- App start data not attached to sampled transactions when preceded by unsampled transactions ([#5756](https://github.com/getsentry/sentry-react-native/pull/5756))
4849
- Resolve relative `SOURCEMAP_FILE` paths against the project root in the Xcode build script ([#5730](https://github.com/getsentry/sentry-react-native/pull/5730))
4950
- Fixes the issue with unit mismatch in `adjustTransactionDuration` ([#5740](https://github.com/getsentry/sentry-react-native/pull/5740))
5051
- Handle `inactive` state for spans ([#5742](https://github.com/getsentry/sentry-react-native/pull/5742))

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getCurrentScope,
88
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
99
SentryNonRecordingSpan,
10+
spanIsSampled,
1011
startInactiveSpan,
1112
timestampInSeconds,
1213
} from '@sentry/core';
@@ -241,6 +242,10 @@ export const appStartIntegration = ({
241242
return;
242243
}
243244

245+
if (!spanIsSampled(rootSpan)) {
246+
return;
247+
}
248+
244249
setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId);
245250
};
246251

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getIsolationScope,
66
SEMANTIC_ATTRIBUTE_SENTRY_OP,
77
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
8+
SentryNonRecordingSpan,
89
setCurrentClient,
910
startInactiveSpan,
1011
timestampInSeconds,
@@ -463,6 +464,50 @@ describe('App Start Integration', () => {
463464
});
464465
});
465466

467+
it('Does not lock firstStartedActiveRootSpanId to unsampled root span', async () => {
468+
mockAppStart({ cold: true });
469+
470+
const integration = appStartIntegration();
471+
const client = new TestClient({
472+
...getDefaultTestClientOptions(),
473+
enableAppStartTracking: true,
474+
tracesSampleRate: 1.0,
475+
});
476+
setCurrentClient(client);
477+
integration.setup(client);
478+
integration.afterAllSetup(client);
479+
480+
// Simulate an unsampled root span starting first
481+
const unsampledSpan = new SentryNonRecordingSpan();
482+
client.emit('spanStart', unsampledSpan);
483+
484+
// Then a sampled root span starts
485+
const sampledSpan = startInactiveSpan({
486+
name: 'Sampled Root Span',
487+
forceTransaction: true,
488+
});
489+
const sampledSpanId = sampledSpan.spanContext().spanId;
490+
491+
// Process a transaction event matching the sampled span
492+
const event = getMinimalTransactionEvent();
493+
event.contexts!.trace!.span_id = sampledSpanId;
494+
495+
const actualEvent = await processEventWithIntegration(integration, event);
496+
497+
// App start should be attached to the sampled transaction
498+
const appStartSpan = (actualEvent as TransactionEvent)?.spans?.find(
499+
({ description }) => description === 'Cold Start',
500+
);
501+
expect(appStartSpan).toBeDefined();
502+
expect(appStartSpan).toEqual(
503+
expect.objectContaining({
504+
description: 'Cold Start',
505+
op: APP_START_COLD_OP,
506+
}),
507+
);
508+
expect((actualEvent as TransactionEvent)?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined();
509+
});
510+
466511
it('Adds Cold App Start Span to Active Span', async () => {
467512
const [timeOriginMilliseconds, appStartTimeMilliseconds] = mockAppStart({ cold: true });
468513

0 commit comments

Comments
 (0)