@@ -12,10 +12,8 @@ import type { Client, Integration, Span } from '@sentry/core';
1212import {
1313 addNonEnumerableProperty ,
1414 debug ,
15- getActiveSpan ,
1615 getClient ,
1716 getCurrentScope ,
18- getRootSpan ,
1917 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
2018 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
2119 SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
@@ -41,7 +39,14 @@ import type {
4139 UseRoutes ,
4240} from '../types' ;
4341import { checkRouteForAsyncHandler } from './lazy-routes' ;
44- import { initializeRouterUtils , resolveRouteNameAndSource , transactionNameHasWildcard } from './utils' ;
42+ import {
43+ clearNavigationContext ,
44+ getActiveRootSpan ,
45+ initializeRouterUtils ,
46+ resolveRouteNameAndSource ,
47+ setNavigationContext ,
48+ transactionNameHasWildcard ,
49+ } from './utils' ;
4550
4651let _useEffect : UseEffect ;
4752let _useLocation : UseLocation ;
@@ -230,11 +235,14 @@ function trackLazyRouteLoad(span: Span, promise: Promise<unknown>): void {
230235
231236/**
232237 * Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
238+ * When capturedSpan is provided, updates that specific span instead of the current active span.
239+ * This prevents race conditions where a lazy handler resolves after the user has navigated away.
233240 */
234241export function processResolvedRoutes (
235242 resolvedRoutes : RouteObject [ ] ,
236243 parentRoute ?: RouteObject ,
237244 currentLocation : Location | null = null ,
245+ capturedSpan ?: Span ,
238246) : void {
239247 resolvedRoutes . forEach ( child => {
240248 allRoutes . add ( child ) ;
@@ -249,56 +257,44 @@ export function processResolvedRoutes(
249257 addResolvedRoutesToParent ( resolvedRoutes , parentRoute ) ;
250258 }
251259
252- // After processing lazy routes, check if we need to update an active transaction
253- const activeRootSpan = getActiveRootSpan ( ) ;
254- if ( activeRootSpan ) {
255- const spanOp = spanToJSON ( activeRootSpan ) . op ;
260+ // Use captured span if provided, otherwise fall back to current active span
261+ const targetSpan = capturedSpan ?? getActiveRootSpan ( ) ;
262+ if ( targetSpan ) {
263+ const spanJson = spanToJSON ( targetSpan ) ;
264+
265+ // Skip update if span has already ended (timestamp is set when span.end() is called)
266+ if ( spanJson . timestamp ) {
267+ DEBUG_BUILD && debug . warn ( '[React Router] Lazy handler resolved after span ended - skipping update' ) ;
268+ return ;
269+ }
270+
271+ const spanOp = spanJson . op ;
256272
257- // Try to use the provided location first, then fall back to global window location if needed
273+ // Use captured location for route matching (ensures we match against the correct route)
274+ // Fall back to window.location only if no captured location and no captured span
275+ // (i.e., this is not from an async handler)
258276 let location = currentLocation ;
259- if ( ! location ) {
277+ if ( ! location && ! capturedSpan ) {
260278 if ( typeof WINDOW !== 'undefined' ) {
261279 const globalLocation = WINDOW . location ;
262- if ( globalLocation ) {
280+ if ( globalLocation ?. pathname ) {
263281 location = { pathname : globalLocation . pathname } ;
264282 }
265283 }
266284 }
267285
268- // If the user navigated away before this lazy handler resolved, skip updating the span.
269- if ( currentLocation && typeof WINDOW !== 'undefined' ) {
270- try {
271- const windowLocation = WINDOW . location ;
272- if ( windowLocation ) {
273- const capturedKey = computeLocationKey ( currentLocation ) ;
274- const currentKey = computeLocationKey ( {
275- pathname : windowLocation . pathname ,
276- search : windowLocation . search || '' ,
277- hash : windowLocation . hash || '' ,
278- } ) ;
279-
280- if ( currentKey !== capturedKey ) {
281- return ;
282- }
283- }
284- } catch {
285- DEBUG_BUILD && debug . warn ( '[React Router] Could not access window.location' ) ;
286- return ;
287- }
288- }
289-
290286 if ( location ) {
291287 if ( spanOp === 'pageload' ) {
292288 // Re-run the pageload transaction update with the newly loaded routes
293289 updatePageloadTransaction ( {
294- activeRootSpan,
290+ activeRootSpan : targetSpan ,
295291 location : { pathname : location . pathname } ,
296292 routes : Array . from ( allRoutes ) ,
297293 allRoutes : Array . from ( allRoutes ) ,
298294 } ) ;
299295 } else if ( spanOp === 'navigation' ) {
300296 // For navigation spans, update the name with the newly loaded routes
301- updateNavigationSpan ( activeRootSpan , location , Array . from ( allRoutes ) , false , _matchRoutes ) ;
297+ updateNavigationSpan ( targetSpan , location , Array . from ( allRoutes ) , false , _matchRoutes ) ;
302298 }
303299 }
304300 }
@@ -750,7 +746,14 @@ function wrapPatchRoutesOnNavigation(
750746 }
751747
752748 const lazyLoadPromise = ( async ( ) => {
753- const result = await originalPatchRoutes ( args ) ;
749+ // Set context so async handlers can access correct targetPath and span
750+ setNavigationContext ( targetPath , activeRootSpan ) ;
751+ let result ;
752+ try {
753+ result = await originalPatchRoutes ( args ) ;
754+ } finally {
755+ clearNavigationContext ( ) ;
756+ }
754757
755758 const currentActiveRootSpan = getActiveRootSpan ( ) ;
756759 if ( currentActiveRootSpan && ( spanToJSON ( currentActiveRootSpan ) as { op ?: string } ) . op === 'navigation' ) {
@@ -1206,17 +1209,3 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
12061209 // will break advanced type inference done by react router params
12071210 return SentryRoutes ;
12081211}
1209-
1210- function getActiveRootSpan ( ) : Span | undefined {
1211- const span = getActiveSpan ( ) ;
1212- const rootSpan = span ? getRootSpan ( span ) : undefined ;
1213-
1214- if ( ! rootSpan ) {
1215- return undefined ;
1216- }
1217-
1218- const op = spanToJSON ( rootSpan ) . op ;
1219-
1220- // Only use this root span if it is a pageload or navigation span
1221- return op === 'navigation' || op === 'pageload' ? rootSpan : undefined ;
1222- }
0 commit comments