From b820ee53764e47992c71c53ede290209159868e6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 19 Dec 2025 12:59:02 +0100 Subject: [PATCH 1/2] fix(appstart): Skip span ID check when standalone mode is enabled --- .../src/js/tracing/integrations/appStart.ts | 25 +++++--- .../tracing/integrations/appStart.test.ts | 64 +++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index 181737b188..88f1d1d38f 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -320,21 +320,26 @@ export const appStartIntegration = ({ return; } - if (!firstStartedActiveRootSpanId) { - debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); - return; - } - if (!event.contexts?.trace) { debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return; } - if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { - debug.warn( - '[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.', - ); - return; + // When standalone is true, we create our own transaction and don't need to verify + // it matches the first navigation transaction. When standalone is false, we need to + // ensure we're attaching app start to the first transaction (not a later one). + if (!standalone) { + if (!firstStartedActiveRootSpanId) { + debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); + return; + } + + if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { + debug.warn( + '[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.', + ); + return; + } } const appStart = await NATIVE.fetchNativeAppStart(); diff --git a/packages/core/test/tracing/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index 883dbe4242..b5a26bf5f0 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -6,6 +6,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient, + startInactiveSpan, timestampInSeconds, } from '@sentry/core'; import { @@ -385,6 +386,69 @@ describe('App Start Integration', () => { expect(actualEvent).toStrictEqual(undefined); expect(NATIVE.fetchNativeAppStart).toHaveBeenCalledTimes(1); }); + + it('Attaches app start to standalone transaction even when navigation transaction starts first', async () => { + // This test simulates the Android scenario where React Navigation auto-instrumentation + // starts a navigation transaction before the standalone app start transaction is created. + // The fix ensures that when standalone: true, the span ID check is skipped so app start + // can be attached to the standalone transaction even if a navigation transaction started first. + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + mockAppStart({ cold: true }); + + const integration = appStartIntegration({ + standalone: true, + }); + const client = new TestClient({ + ...getDefaultTestClientOptions(), + enableAppStartTracking: true, + tracesSampleRate: 1.0, + }); + setCurrentClient(client); + integration.setup(client); + + // Simulate a navigation transaction starting first (like React Navigation auto-instrumentation) + // This will set firstStartedActiveRootSpanId to the navigation span's ID + const navigationSpan = startInactiveSpan({ + name: 'calendar/home', + op: 'navigation', + forceTransaction: true, + }); + const navigationSpanId = navigationSpan?.spanContext().spanId; + if (navigationSpan) { + navigationSpan.end(); + } + + // Now capture standalone app start - it should still work even though navigation span started first + // The standalone transaction will have a different span ID, but the fix skips the check + await integration.captureStandaloneAppStart(); + + const actualEvent = client.event as TransactionEvent | undefined; + expect(actualEvent).toBeDefined(); + expect(actualEvent?.spans).toBeDefined(); + expect(actualEvent?.spans?.length).toBeGreaterThan(0); + + // Verify that app start was attached successfully + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); + expect(appStartSpan).toBeDefined(); + expect(appStartSpan).toEqual( + expect.objectContaining>({ + description: 'Cold Start', + op: APP_START_COLD_OP, + }), + ); + + // Verify the standalone transaction has a different span ID than the navigation transaction + // This confirms that the span ID check was skipped (otherwise app start wouldn't be attached) + expect(actualEvent?.contexts?.trace?.span_id).toBeDefined(); + if (navigationSpanId) { + expect(actualEvent?.contexts?.trace?.span_id).not.toBe(navigationSpanId); + } + + expect(actualEvent?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined(); + }); }); describe('App Start Attached to the First Root Span', () => { From 36c955185f23eb528a794bdd529d396e8ae8f533 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 19 Dec 2025 13:01:53 +0100 Subject: [PATCH 2/2] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1d808eea..1bc4b4f122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Fix for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483)) +- Skip span ID check when standalone mode is enabled ([#5493](https://github.com/getsentry/sentry-react-native/pull/5493)) ### Dependencies