diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts index 2cfa2935b5bf..34e5105f8f9d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts @@ -23,6 +23,37 @@ test('Creates a pageload transaction with parameterized route', async ({ page }) expect(event.contexts?.trace?.op).toBe('pageload'); }); +test('Does not create a navigation transaction on initial load to deep lazy route', async ({ page }) => { + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId' + ); + }); + + await page.goto('/lazy/inner/1/2/3'); + + const pageloadEvent = await pageloadPromise; + + expect(pageloadEvent.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId'); + + const lazyRouteContent = page.locator('id=innermost-lazy-route'); + await expect(lazyRouteContent).toBeVisible(); + + // "Race" between navigation transaction and a timeout to ensure no navigation transaction is created within the timeout period + const result = await Promise.race([ + navigationPromise.then(() => 'navigation'), + new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 1500)), + ]); + + expect(result).toBe('timeout'); +}); + test('Creates a navigation transaction inside a lazy route', async ({ page }) => { const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { return ( diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index a464875a8575..10db32231195 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -567,6 +567,13 @@ export function handleNavigation(opts: { return; } + // Avoid starting a navigation span on initial load when a pageload root span is active. + // This commonly happens when lazy routes resolve during the first render and React Router emits a POP. + const activeRootSpan = getActiveRootSpan(); + if (activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload' && navigationType === 'POP') { + return; + } + if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) { const [name, source] = resolveRouteNameAndSource( location,