@@ -1308,3 +1308,128 @@ test('Slow lazy route navigation with early span end still gets parameterized ro
13081308 expect ( event . contexts ?. trace ?. op ) . toBe ( 'navigation' ) ;
13091309 expect ( event . contexts ?. trace ?. data ?. [ 'sentry.source' ] ) . toBe ( 'route' ) ;
13101310} ) ;
1311+
1312+ test ( 'Captured navigation context is used instead of stale window.location during rapid navigation' , async ( {
1313+ page,
1314+ } ) => {
1315+ // Validates fix for race condition where captureCurrentLocation would use stale WINDOW.location.
1316+ // Navigate to slow route, then quickly to another route before lazy handler resolves.
1317+ await page . goto ( '/' ) ;
1318+
1319+ const allNavigationTransactions : Array < { name : string ; traceId : string } > = [ ] ;
1320+
1321+ const collectorPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
1322+ if ( transactionEvent ?. transaction && transactionEvent . contexts ?. trace ?. op === 'navigation' ) {
1323+ allNavigationTransactions . push ( {
1324+ name : transactionEvent . transaction ,
1325+ traceId : transactionEvent . contexts . trace . trace_id || '' ,
1326+ } ) ;
1327+ }
1328+ return allNavigationTransactions . length >= 2 ;
1329+ } ) ;
1330+
1331+ const slowFetchLink = page . locator ( 'id=navigation-to-slow-fetch' ) ;
1332+ await expect ( slowFetchLink ) . toBeVisible ( ) ;
1333+ await slowFetchLink . click ( ) ;
1334+
1335+ // Navigate away quickly before slow-fetch's async handler resolves
1336+ await page . waitForTimeout ( 50 ) ;
1337+
1338+ const anotherLink = page . locator ( 'id=navigation-to-another' ) ;
1339+ await anotherLink . click ( ) ;
1340+
1341+ await expect ( page . locator ( 'id=another-lazy-route' ) ) . toBeVisible ( { timeout : 10000 } ) ;
1342+
1343+ await page . waitForTimeout ( 2000 ) ;
1344+
1345+ await Promise . race ( [
1346+ collectorPromise ,
1347+ new Promise < 'timeout' > ( resolve => setTimeout ( ( ) => resolve ( 'timeout' ) , 3000 ) ) ,
1348+ ] ) . catch ( ( ) => { } ) ;
1349+
1350+ expect ( allNavigationTransactions . length ) . toBeGreaterThanOrEqual ( 1 ) ;
1351+
1352+ // /another-lazy transaction must have correct name (not corrupted by slow-fetch handler)
1353+ const anotherLazyTransaction = allNavigationTransactions . find ( t => t . name . startsWith ( '/another-lazy/sub' ) ) ;
1354+ expect ( anotherLazyTransaction ) . toBeDefined ( ) ;
1355+
1356+ const corruptedToRoot = allNavigationTransactions . filter ( t => t . name === '/' ) ;
1357+ expect ( corruptedToRoot . length ) . toBe ( 0 ) ;
1358+
1359+ if ( allNavigationTransactions . length >= 2 ) {
1360+ const uniqueNames = new Set ( allNavigationTransactions . map ( t => t . name ) ) ;
1361+ expect ( uniqueNames . size ) . toBe ( allNavigationTransactions . length ) ;
1362+ }
1363+ } ) ;
1364+
1365+ test ( 'Second navigation span is not corrupted by first slow lazy handler completing late' , async ( { page } ) => {
1366+ // Validates fix for race condition where slow lazy handler would update the wrong span.
1367+ // Navigate to slow route (which fetches /api/slow-data), then quickly to fast route.
1368+ // Without fix: second transaction gets wrong name and/or contains leaked spans.
1369+
1370+ await page . goto ( '/' ) ;
1371+
1372+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1373+ const allNavigationTransactions : Array < { name : string ; traceId : string ; spans : any [ ] } > = [ ] ;
1374+
1375+ const collectorPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
1376+ if ( transactionEvent ?. transaction && transactionEvent . contexts ?. trace ?. op === 'navigation' ) {
1377+ allNavigationTransactions . push ( {
1378+ name : transactionEvent . transaction ,
1379+ traceId : transactionEvent . contexts . trace . trace_id || '' ,
1380+ spans : transactionEvent . spans || [ ] ,
1381+ } ) ;
1382+ }
1383+ return false ;
1384+ } ) ;
1385+
1386+ // Navigate to slow-fetch (500ms lazy delay, fetches /api/slow-data)
1387+ const slowFetchLink = page . locator ( 'id=navigation-to-slow-fetch' ) ;
1388+ await expect ( slowFetchLink ) . toBeVisible ( ) ;
1389+ await slowFetchLink . click ( ) ;
1390+
1391+ // Wait 150ms (before 500ms lazy loading completes), then navigate away
1392+ await page . waitForTimeout ( 150 ) ;
1393+
1394+ const anotherLink = page . locator ( 'id=navigation-to-another' ) ;
1395+ await anotherLink . click ( ) ;
1396+
1397+ await expect ( page . locator ( 'id=another-lazy-route' ) ) . toBeVisible ( { timeout : 10000 } ) ;
1398+
1399+ // Wait for slow-fetch lazy handler to complete and transactions to be sent
1400+ await page . waitForTimeout ( 2000 ) ;
1401+
1402+ await Promise . race ( [
1403+ collectorPromise ,
1404+ new Promise < 'timeout' > ( resolve => setTimeout ( ( ) => resolve ( 'timeout' ) , 3000 ) ) ,
1405+ ] ) . catch ( ( ) => { } ) ;
1406+
1407+ expect ( allNavigationTransactions . length ) . toBeGreaterThanOrEqual ( 1 ) ;
1408+
1409+ // /another-lazy transaction must have correct name, not "/slow-fetch/:id"
1410+ const anotherLazyTransaction = allNavigationTransactions . find ( t => t . name . startsWith ( '/another-lazy/sub' ) ) ;
1411+ expect ( anotherLazyTransaction ) . toBeDefined ( ) ;
1412+
1413+ // Key assertion 2: /another-lazy transaction must NOT contain spans from /slow-fetch route
1414+ // The /api/slow-data fetch is triggered by the slow-fetch route's lazy loading
1415+ if ( anotherLazyTransaction ) {
1416+ const leakedSpans = anotherLazyTransaction . spans . filter (
1417+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1418+ ( span : any ) => span . description ?. includes ( 'slow-data' ) || span . data ?. url ?. includes ( 'slow-data' ) ,
1419+ ) ;
1420+ expect ( leakedSpans . length ) . toBe ( 0 ) ;
1421+ }
1422+
1423+ // Key assertion 3: If slow-fetch transaction exists, verify it has the correct name
1424+ // (not corrupted to /another-lazy)
1425+ const slowFetchTransaction = allNavigationTransactions . find ( t => t . name . includes ( 'slow-fetch' ) ) ;
1426+ if ( slowFetchTransaction ) {
1427+ expect ( slowFetchTransaction . name ) . toMatch ( / \/ s l o w - f e t c h / ) ;
1428+ // Verify slow-fetch transaction doesn't contain spans that belong to /another-lazy
1429+ const wrongSpans = slowFetchTransaction . spans . filter (
1430+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1431+ ( span : any ) => span . description ?. includes ( 'another-lazy' ) || span . data ?. url ?. includes ( 'another-lazy' ) ,
1432+ ) ;
1433+ expect ( wrongSpans . length ) . toBe ( 0 ) ;
1434+ }
1435+ } ) ;
0 commit comments