From 6730ec3ae608c2795acfd9f7e0835e13750d2861 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 13:32:44 +0100 Subject: [PATCH 1/2] 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 --- CHANGELOG.md | 1 + .../src/js/tracing/integrations/appStart.ts | 5 +++ .../tracing/integrations/appStart.test.ts | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cc8c8e2d..8b7d7a927f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- App start (Cold/Warm Start) data is no longer lost when the first transaction is not sampled (`tracesSampleRate < 1.0`) ([#XXXX](https://github.com/getsentry/sentry-react-native/pull/XXXX)) - Resolve relative `SOURCEMAP_FILE` paths against the project root in the Xcode build script ([#5730](https://github.com/getsentry/sentry-react-native/pull/5730)) - Fixes the issue with unit mismatch in `adjustTransactionDuration` ([#5740](https://github.com/getsentry/sentry-react-native/pull/5740)) - Handle `inactive` state for spans ([#5742](https://github.com/getsentry/sentry-react-native/pull/5742)) diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index 88f1d1d38f..09b2263ceb 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -7,6 +7,7 @@ import { getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, + spanIsSampled, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; @@ -241,6 +242,10 @@ export const appStartIntegration = ({ return; } + if (!spanIsSampled(rootSpan)) { + return; + } + setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId); }; diff --git a/packages/core/test/tracing/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index b5a26bf5f0..a15f931ba8 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -5,6 +5,7 @@ import { getIsolationScope, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SentryNonRecordingSpan, setCurrentClient, startInactiveSpan, timestampInSeconds, @@ -463,6 +464,50 @@ describe('App Start Integration', () => { }); }); + it('Does not lock firstStartedActiveRootSpanId to unsampled root span', async () => { + mockAppStart({ cold: true }); + + const integration = appStartIntegration(); + const client = new TestClient({ + ...getDefaultTestClientOptions(), + enableAppStartTracking: true, + tracesSampleRate: 1.0, + }); + setCurrentClient(client); + integration.setup(client); + integration.afterAllSetup(client); + + // Simulate an unsampled root span starting first + const unsampledSpan = new SentryNonRecordingSpan(); + client.emit('spanStart', unsampledSpan); + + // Then a sampled root span starts + const sampledSpan = startInactiveSpan({ + name: 'Sampled Root Span', + forceTransaction: true, + }); + const sampledSpanId = sampledSpan.spanContext().spanId; + + // Process a transaction event matching the sampled span + const event = getMinimalTransactionEvent(); + event.contexts!.trace!.span_id = sampledSpanId; + + const actualEvent = await processEventWithIntegration(integration, event); + + // App start should be attached to the sampled transaction + const appStartSpan = (actualEvent as TransactionEvent)?.spans?.find( + ({ description }) => description === 'Cold Start', + ); + expect(appStartSpan).toBeDefined(); + expect(appStartSpan).toEqual( + expect.objectContaining({ + description: 'Cold Start', + op: APP_START_COLD_OP, + }), + ); + expect((actualEvent as TransactionEvent)?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined(); + }); + it('Adds Cold App Start Span to Active Span', async () => { const [timeOriginMilliseconds, appStartTimeMilliseconds] = mockAppStart({ cold: true }); From 3aebd2fe184d5f5e77ec8315b9ed698ded586ac7 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 4 Mar 2026 13:35:59 +0100 Subject: [PATCH 2/2] fix(core): Update CHANGELOG entry wording Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7d7a927f..e9160f7e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ ### Fixes -- App start (Cold/Warm Start) data is no longer lost when the first transaction is not sampled (`tracesSampleRate < 1.0`) ([#XXXX](https://github.com/getsentry/sentry-react-native/pull/XXXX)) +- App start data not attached to sampled transactions when preceded by unsampled transactions ([#5756](https://github.com/getsentry/sentry-react-native/pull/5756)) - Resolve relative `SOURCEMAP_FILE` paths against the project root in the Xcode build script ([#5730](https://github.com/getsentry/sentry-react-native/pull/5730)) - Fixes the issue with unit mismatch in `adjustTransactionDuration` ([#5740](https://github.com/getsentry/sentry-react-native/pull/5740)) - Handle `inactive` state for spans ([#5742](https://github.com/getsentry/sentry-react-native/pull/5742))