@@ -1228,3 +1228,134 @@ test('Query/hash navigation does not corrupt transaction name', async ({ page })
12281228 const corruptedToRoot = navigationTransactions . filter ( t => t . name === '/' ) ;
12291229 expect ( corruptedToRoot . length ) . toBe ( 0 ) ;
12301230} ) ;
1231+
1232+ test ( 'Captured navigation context is used instead of stale window.location during rapid navigation' , async ( {
1233+ page,
1234+ } ) => {
1235+ // Validates fix for race condition where captureCurrentLocation would use stale WINDOW.location.
1236+ // Navigate to slow route, then quickly to another route before lazy handler resolves.
1237+ await page . goto ( '/' ) ;
1238+
1239+ const allNavigationTransactions : Array < { name : string ; traceId : string } > = [ ] ;
1240+
1241+ const collectorPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
1242+ if ( transactionEvent ?. transaction && transactionEvent . contexts ?. trace ?. op === 'navigation' ) {
1243+ allNavigationTransactions . push ( {
1244+ name : transactionEvent . transaction ,
1245+ traceId : transactionEvent . contexts . trace . trace_id || '' ,
1246+ } ) ;
1247+ }
1248+ return allNavigationTransactions . length >= 2 ;
1249+ } ) ;
1250+
1251+ const slowFetchLink = page . locator ( 'id=navigation-to-slow-fetch' ) ;
1252+ await expect ( slowFetchLink ) . toBeVisible ( ) ;
1253+ await slowFetchLink . click ( ) ;
1254+
1255+ // Navigate away quickly before slow-fetch's async handler resolves
1256+ await page . waitForTimeout ( 50 ) ;
1257+
1258+ const anotherLink = page . locator ( 'id=navigation-to-another' ) ;
1259+ await anotherLink . click ( ) ;
1260+
1261+ await expect ( page . locator ( 'id=another-lazy-route' ) ) . toBeVisible ( { timeout : 10000 } ) ;
1262+
1263+ await page . waitForTimeout ( 2000 ) ;
1264+
1265+ await Promise . race ( [
1266+ collectorPromise ,
1267+ new Promise < 'timeout' > ( resolve => setTimeout ( ( ) => resolve ( 'timeout' ) , 3000 ) ) ,
1268+ ] ) . catch ( ( ) => { } ) ;
1269+
1270+ expect ( allNavigationTransactions . length ) . toBeGreaterThanOrEqual ( 1 ) ;
1271+
1272+ // /another-lazy transaction must have correct name (not corrupted by slow-fetch handler)
1273+ const anotherLazyTransaction = allNavigationTransactions . find (
1274+ t =>
1275+ t . name . startsWith ( '/another-lazy/sub' ) ,
1276+ ) ;
1277+ expect ( anotherLazyTransaction ) . toBeDefined ( ) ;
1278+
1279+ const corruptedToRoot = allNavigationTransactions . filter ( t => t . name === '/' ) ;
1280+ expect ( corruptedToRoot . length ) . toBe ( 0 ) ;
1281+
1282+ if ( allNavigationTransactions . length >= 2 ) {
1283+ const uniqueNames = new Set ( allNavigationTransactions . map ( t => t . name ) ) ;
1284+ expect ( uniqueNames . size ) . toBe ( allNavigationTransactions . length ) ;
1285+ }
1286+ } ) ;
1287+
1288+ test ( 'Second navigation span is not corrupted by first slow lazy handler completing late' , async ( { page } ) => {
1289+ // Validates fix for race condition where slow lazy handler would update the wrong span.
1290+ // Navigate to slow route (which fetches /api/slow-data), then quickly to fast route.
1291+ // Without fix: second transaction gets wrong name and/or contains leaked spans.
1292+
1293+ await page . goto ( '/' ) ;
1294+
1295+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1296+ const allNavigationTransactions : Array < { name : string ; traceId : string ; spans : any [ ] } > = [ ] ;
1297+
1298+ const collectorPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
1299+ if ( transactionEvent ?. transaction && transactionEvent . contexts ?. trace ?. op === 'navigation' ) {
1300+ allNavigationTransactions . push ( {
1301+ name : transactionEvent . transaction ,
1302+ traceId : transactionEvent . contexts . trace . trace_id || '' ,
1303+ spans : transactionEvent . spans || [ ] ,
1304+ } ) ;
1305+ }
1306+ return false ;
1307+ } ) ;
1308+
1309+ // Navigate to slow-fetch (500ms lazy delay, fetches /api/slow-data)
1310+ const slowFetchLink = page . locator ( 'id=navigation-to-slow-fetch' ) ;
1311+ await expect ( slowFetchLink ) . toBeVisible ( ) ;
1312+ await slowFetchLink . click ( ) ;
1313+
1314+ // Wait 150ms (before 500ms lazy loading completes), then navigate away
1315+ await page . waitForTimeout ( 150 ) ;
1316+
1317+ const anotherLink = page . locator ( 'id=navigation-to-another' ) ;
1318+ await anotherLink . click ( ) ;
1319+
1320+ await expect ( page . locator ( 'id=another-lazy-route' ) ) . toBeVisible ( { timeout : 10000 } ) ;
1321+
1322+ // Wait for slow-fetch lazy handler to complete and transactions to be sent
1323+ await page . waitForTimeout ( 2000 ) ;
1324+
1325+ await Promise . race ( [
1326+ collectorPromise ,
1327+ new Promise < 'timeout' > ( resolve => setTimeout ( ( ) => resolve ( 'timeout' ) , 3000 ) ) ,
1328+ ] ) . catch ( ( ) => { } ) ;
1329+
1330+ expect ( allNavigationTransactions . length ) . toBeGreaterThanOrEqual ( 1 ) ;
1331+
1332+ // /another-lazy transaction must have correct name, not "/slow-fetch/:id"
1333+ const anotherLazyTransaction = allNavigationTransactions . find (
1334+ t =>
1335+ t . name . startsWith ( '/another-lazy/sub' ) ,
1336+ ) ;
1337+ expect ( anotherLazyTransaction ) . toBeDefined ( ) ;
1338+
1339+ // Key assertion 2: /another-lazy transaction must NOT contain spans from /slow-fetch route
1340+ // The /api/slow-data fetch is triggered by the slow-fetch route's lazy loading
1341+ if ( anotherLazyTransaction ) {
1342+ const leakedSpans = anotherLazyTransaction . spans . filter (
1343+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1344+ ( span : any ) => span . description ?. includes ( 'slow-data' ) || span . data ?. url ?. includes ( 'slow-data' ) ,
1345+ ) ;
1346+ expect ( leakedSpans . length ) . toBe ( 0 ) ;
1347+ }
1348+
1349+ // Key assertion 3: If slow-fetch transaction exists, verify it has the correct name
1350+ // (not corrupted to /another-lazy)
1351+ const slowFetchTransaction = allNavigationTransactions . find ( t => t . name . includes ( 'slow-fetch' ) ) ;
1352+ if ( slowFetchTransaction ) {
1353+ expect ( slowFetchTransaction . name ) . toMatch ( / \/ s l o w - f e t c h / ) ;
1354+ // Verify slow-fetch transaction doesn't contain spans that belong to /another-lazy
1355+ const wrongSpans = slowFetchTransaction . spans . filter (
1356+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1357+ ( span : any ) => span . description ?. includes ( 'another-lazy' ) || span . data ?. url ?. includes ( 'another-lazy' ) ,
1358+ ) ;
1359+ expect ( wrongSpans . length ) . toBe ( 0 ) ;
1360+ }
1361+ } ) ;
0 commit comments