From fcde6fa44934cd3d472be079a93c85fa330b2988 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 11:24:11 +0100 Subject: [PATCH 1/4] fix(core): Defer initial navigation span creation until navigation container is registered Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 + .../core/src/js/tracing/reactnavigation.ts | 13 +- .../core/test/tracing/reactnavigation.test.ts | 170 ++++++++++++++++++ 3 files changed, 185 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbba68c2f..8715c6010d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Defer initial navigation span creation until navigation container is registered ([#XXXX](https://github.com/getsentry/sentry-react-native/pull/XXXX)) + ## 8.3.0 ### Features diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index aa9f549bb8..1e651ef230 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -195,6 +195,7 @@ export const reactNavigationIntegration = ({ let navigationProcessingSpan: Span | undefined; let initialStateHandled: boolean = false; + let isSetupComplete: boolean = false; let stateChangeTimeout: ReturnType | undefined; let recentRouteKeys: string[] = []; @@ -233,14 +234,15 @@ export const reactNavigationIntegration = ({ } }); - startIdleNavigationSpan(); + isSetupComplete = true; if (!navigationContainer) { // This is expected as navigation container is registered after the root component is mounted. return undefined; } - // Navigation container already registered, just populate with route state + // Navigation container already registered, create and populate initial span + startIdleNavigationSpan(); updateLatestNavigationSpanWithCurrentRoute(); initialStateHandled = true; }; @@ -282,8 +284,11 @@ export const reactNavigationIntegration = ({ } if (!latestNavigationSpan) { - debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`); - return undefined; + if (!isSetupComplete) { + debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`); + return undefined; + } + startIdleNavigationSpan(); } // Navigation Container is registered after the first navigation diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts index fc478b3f99..d5a4baa541 100644 --- a/packages/core/test/tracing/reactnavigation.test.ts +++ b/packages/core/test/tracing/reactnavigation.test.ts @@ -597,6 +597,176 @@ describe('ReactNavigationInstrumentation', () => { }); }); + describe('deferred initial span creation', () => { + test('initial span is created in registerNavigationContainer when called after afterAllSetup', async () => { + const instrumentation = reactNavigationIntegration({ + routeChangeTimeoutMs: 200, + }); + + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [instrumentation], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // No span created yet — no navigation container registered + expect(getActiveSpan()).toBeUndefined(); + + // Register navigation container after init (production flow) + const mockNavigationContainer = new MockNavigationContainer(); + instrumentation.registerNavigationContainer({ current: mockNavigationContainer }); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanToJSON(span!).description).toBe('Route'); + + await jest.runOnlyPendingTimersAsync(); + await client.flush(); + + expect(client.event).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'Route', + }), + ); + }); + + test('initial span is not discarded by idle timeout when container registers after init', async () => { + const instrumentation = reactNavigationIntegration({ + routeChangeTimeoutMs: 200, + }); + + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [instrumentation], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // Simulate a delay before container registration (e.g. auth gate, slow render) + jest.advanceTimersByTime(500); + expect(getActiveSpan()).toBeUndefined(); + + // Register after 500ms — should still create the span successfully + const mockNavigationContainer = new MockNavigationContainer(); + instrumentation.registerNavigationContainer({ current: mockNavigationContainer }); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + + await jest.runOnlyPendingTimersAsync(); + await client.flush(); + + expect(client.event).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'Route', + }), + ); + }); + + test('afterAllSetup does not create span when navigation container is not registered', () => { + const instrumentation = reactNavigationIntegration(); + + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [instrumentation], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // No navigation container registered — no span should be created + expect(getActiveSpan()).toBeUndefined(); + }); + + test('subsequent navigations work after deferred initial span', async () => { + const instrumentation = reactNavigationIntegration({ + routeChangeTimeoutMs: 200, + }); + + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [instrumentation], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + mockNavigation = createMockNavigationAndAttachTo(instrumentation); + + // Flush the initial span + jest.runOnlyPendingTimers(); + await client.flush(); + + expect(client.event).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'Initial Screen', + }), + ); + + // Navigate to a new screen + mockNavigation.navigateToNewScreen(); + jest.runOnlyPendingTimers(); + await client.flush(); + + expect(client.event).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'New Screen', + }), + ); + }); + + test('runApplication creates span after deferred initial state is handled', async () => { + const { mockedOnRunApplication } = mockAppRegistryIntegration(); + + const instrumentation = reactNavigationIntegration(); + + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [instrumentation], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // Register container after init + const mockNavigationContainer = new MockNavigationContainer(); + instrumentation.registerNavigationContainer({ current: mockNavigationContainer }); + + // Flush initial span + await jest.runOnlyPendingTimersAsync(); + + // Simulate app restart via runApplication + const runApplicationCallback = mockedOnRunApplication.mock.calls[0][0]; + runApplicationCallback(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanToJSON(span!).op).toBe('navigation'); + }); + }); + describe('options', () => { test('waits until routeChangeTimeoutMs', () => { const instrumentation = reactNavigationIntegration({ From b1eedca53222dc2efb7191dd41df2fbb8063fa46 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 11:24:54 +0100 Subject: [PATCH 2/4] fix(core): Update changelog with PR number 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 8715c6010d..2ff9af2c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Defer initial navigation span creation until navigation container is registered ([#XXXX](https://github.com/getsentry/sentry-react-native/pull/XXXX)) +- Defer initial navigation span creation until navigation container is registered ([#5789](https://github.com/getsentry/sentry-react-native/pull/5789)) ## 8.3.0 From e4c667d30bba01cbaba3815fd2c996d57251f74a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 12:30:40 +0100 Subject: [PATCH 3/4] change log for clarity Co-authored-by: LucasZF --- packages/core/src/js/tracing/reactnavigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 1e651ef230..d7ecc74985 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -285,7 +285,7 @@ export const reactNavigationIntegration = ({ if (!latestNavigationSpan) { if (!isSetupComplete) { - debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`); + debug.log(`${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`); return undefined; } startIdleNavigationSpan(); From 0f13be96b8c0a0b37f3bc4ebbf2a5e704ac21c95 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 14:30:59 +0100 Subject: [PATCH 4/4] Lint issue --- packages/core/src/js/tracing/reactnavigation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index d7ecc74985..9296be1d3d 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -285,7 +285,9 @@ export const reactNavigationIntegration = ({ if (!latestNavigationSpan) { if (!isSetupComplete) { - debug.log(`${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`); + debug.log( + `${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`, + ); return undefined; } startIdleNavigationSpan();