1
+ /* eslint-disable complexity */
1
2
/* eslint-disable max-lines */
2
3
// Inspired from Donnie McNeal's solution:
3
4
// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536
@@ -174,13 +175,14 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
174
175
return origUseRoutes ;
175
176
}
176
177
177
- let isMountRenderPass : boolean = true ;
178
+ const allRoutes : RouteObject [ ] = [ ] ;
178
179
179
180
const SentryRoutes : React . FC < {
180
181
children ?: React . ReactNode ;
181
182
routes : RouteObject [ ] ;
182
183
locationArg ?: Partial < Location > | string ;
183
184
} > = ( props : { children ?: React . ReactNode ; routes : RouteObject [ ] ; locationArg ?: Partial < Location > | string } ) => {
185
+ const isMountRenderPass = React . useRef ( true ) ;
184
186
const { routes, locationArg } = props ;
185
187
186
188
const Routes = origUseRoutes ( routes , locationArg ) ;
@@ -198,11 +200,15 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
198
200
const normalizedLocation =
199
201
typeof stableLocationParam === 'string' ? { pathname : stableLocationParam } : stableLocationParam ;
200
202
201
- if ( isMountRenderPass ) {
202
- updatePageloadTransaction ( getActiveRootSpan ( ) , normalizedLocation , routes ) ;
203
- isMountRenderPass = false ;
203
+ routes . forEach ( route => {
204
+ allRoutes . push ( ...getChildRoutesRecursively ( route ) ) ;
205
+ } ) ;
206
+
207
+ if ( isMountRenderPass . current ) {
208
+ updatePageloadTransaction ( getActiveRootSpan ( ) , normalizedLocation , routes , undefined , undefined , allRoutes ) ;
209
+ isMountRenderPass . current = false ;
204
210
} else {
205
- handleNavigation ( normalizedLocation , routes , navigationType , version ) ;
211
+ handleNavigation ( normalizedLocation , routes , navigationType , version , undefined , undefined , allRoutes ) ;
206
212
}
207
213
} , [ navigationType , stableLocationParam ] ) ;
208
214
@@ -222,6 +228,7 @@ export function handleNavigation(
222
228
version : V6CompatibleVersion ,
223
229
matches ?: AgnosticDataRouteMatch ,
224
230
basename ?: string ,
231
+ allRoutes ?: RouteObject [ ] ,
225
232
) : void {
226
233
const branches = Array . isArray ( matches ) ? matches : _matchRoutes ( routes , location , basename ) ;
227
234
@@ -233,8 +240,14 @@ export function handleNavigation(
233
240
if ( ( navigationType === 'PUSH' || navigationType === 'POP' ) && branches ) {
234
241
const [ name , source ] = getNormalizedName ( routes , location , branches , basename ) ;
235
242
243
+ let txnName = name ;
244
+
245
+ if ( locationIsInsideDescendantRoute ( location , allRoutes || routes ) ) {
246
+ txnName = prefixWithSlash ( rebuildRoutePathFromAllRoutes ( allRoutes || routes , location ) ) ;
247
+ }
248
+
236
249
startBrowserTracingNavigationSpan ( client , {
237
- name,
250
+ name : txnName ,
238
251
attributes : {
239
252
[ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : source ,
240
253
[ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'navigation' ,
@@ -286,12 +299,93 @@ function sendIndexPath(pathBuilder: string, pathname: string, basename: string):
286
299
return [ formattedPath , 'route' ] ;
287
300
}
288
301
289
- function pathEndsWithWildcard ( path : string , branch : RouteMatch < string > ) : boolean {
290
- return ( path . slice ( - 2 ) === '/*' && branch . route . children && branch . route . children . length > 0 ) || false ;
302
+ function pathEndsWithWildcard ( path : string ) : boolean {
303
+ return path . endsWith ( '*' ) ;
291
304
}
292
305
293
306
function pathIsWildcardAndHasChildren ( path : string , branch : RouteMatch < string > ) : boolean {
294
- return ( path === '*' && branch . route . children && branch . route . children . length > 0 ) || false ;
307
+ return ( pathEndsWithWildcard ( path ) && branch . route . children && branch . route . children . length > 0 ) || false ;
308
+ }
309
+
310
+ // function pathIsWildcardWithNoChildren(path: string, branch: RouteMatch<string>): boolean {
311
+ // return (pathEndsWithWildcard(path) && (!branch.route.children || branch.route.children.length === 0)) || false;
312
+ // }
313
+
314
+ function routeIsDescendant ( route : RouteObject ) : boolean {
315
+ return ! ! ( ! route . children && route . element && route . path && route . path . endsWith ( '/*' ) ) ;
316
+ }
317
+
318
+ function locationIsInsideDescendantRoute ( location : Location , routes : RouteObject [ ] ) : boolean {
319
+ const matchedRoutes = _matchRoutes ( routes , location ) as RouteMatch [ ] ;
320
+
321
+ if ( matchedRoutes ) {
322
+ for ( const match of matchedRoutes ) {
323
+ if ( routeIsDescendant ( match . route ) && pickSplat ( match ) ) {
324
+ return true ;
325
+ }
326
+ }
327
+ }
328
+
329
+ return false ;
330
+ }
331
+
332
+ function getChildRoutesRecursively ( route : RouteObject , allRoutes : RouteObject [ ] = [ ] ) : RouteObject [ ] {
333
+ if ( route . children && ! route . index ) {
334
+ route . children . forEach ( child => {
335
+ allRoutes . push ( ...getChildRoutesRecursively ( child , allRoutes ) ) ;
336
+ } ) ;
337
+ }
338
+
339
+ allRoutes . push ( route ) ;
340
+
341
+ return allRoutes ;
342
+ }
343
+
344
+ function pickPath ( match : RouteMatch ) : string {
345
+ return trimWildcard ( match . route . path || '' ) ;
346
+ }
347
+
348
+ function pickSplat ( match : RouteMatch ) : string {
349
+ return match . params [ '*' ] || '' ;
350
+ }
351
+
352
+ function trimWildcard ( path : string ) : string {
353
+ return path [ path . length - 1 ] === '*' ? path . slice ( 0 , - 1 ) : path ;
354
+ }
355
+
356
+ function trimSlash ( path : string ) : string {
357
+ return path [ path . length - 1 ] === '/' ? path . slice ( 0 , - 1 ) : path ;
358
+ }
359
+
360
+ function prefixWithSlash ( path : string ) : string {
361
+ return path [ 0 ] === '/' ? path : `/${ path } ` ;
362
+ }
363
+
364
+ function rebuildRoutePathFromAllRoutes ( allRoutes : RouteObject [ ] , location : Location ) : string {
365
+ const matchedRoutes = _matchRoutes ( allRoutes , location ) as RouteMatch [ ] ;
366
+
367
+ if ( matchedRoutes ) {
368
+ for ( const match of matchedRoutes ) {
369
+ if ( match . route . path && match . route . path !== '*' ) {
370
+ const path = pickPath ( match ) ;
371
+ const strippedPath = stripBasenameFromPathname ( location . pathname , prefixWithSlash ( match . pathnameBase ) ) ;
372
+
373
+ return trimSlash (
374
+ trimSlash ( path || '' ) +
375
+ prefixWithSlash (
376
+ rebuildRoutePathFromAllRoutes (
377
+ allRoutes . filter ( route => route !== match . route ) ,
378
+ {
379
+ pathname : strippedPath ,
380
+ } ,
381
+ ) ,
382
+ ) ,
383
+ ) ;
384
+ }
385
+ }
386
+ }
387
+
388
+ return '' ;
295
389
}
296
390
297
391
function getNormalizedName (
@@ -321,7 +415,10 @@ function getNormalizedName(
321
415
pathBuilder += newPath ;
322
416
323
417
// If the path matches the current location, return the path
324
- if ( basename + branch . pathname === location . pathname ) {
418
+ if (
419
+ location . pathname . endsWith ( basename + branch . pathname ) ||
420
+ location . pathname . endsWith ( `${ basename } ${ branch . pathname } /` )
421
+ ) {
325
422
if (
326
423
// If the route defined on the element is something like
327
424
// <Route path="/stores/:storeId/products/:productId" element={<div>Product</div>} />
@@ -330,13 +427,13 @@ function getNormalizedName(
330
427
// eslint-disable-next-line deprecation/deprecation
331
428
getNumberOfUrlSegments ( pathBuilder ) !== getNumberOfUrlSegments ( branch . pathname ) &&
332
429
// We should not count wildcard operators in the url segments calculation
333
- pathBuilder . slice ( - 2 ) !== '/*'
430
+ ! pathEndsWithWildcard ( pathBuilder )
334
431
) {
335
432
return [ ( _stripBasename ? '' : basename ) + newPath , 'route' ] ;
336
433
}
337
434
338
435
// if the last character of the pathbuilder is a wildcard and there are children, remove the wildcard
339
- if ( pathEndsWithWildcard ( pathBuilder , branch ) ) {
436
+ if ( pathIsWildcardAndHasChildren ( pathBuilder , branch ) ) {
340
437
pathBuilder = pathBuilder . slice ( 0 , - 1 ) ;
341
438
}
342
439
@@ -347,7 +444,11 @@ function getNormalizedName(
347
444
}
348
445
}
349
446
350
- return [ _stripBasename ? stripBasenameFromPathname ( location . pathname , basename ) : location . pathname , 'url' ] ;
447
+ const fallbackTransactionName = _stripBasename
448
+ ? stripBasenameFromPathname ( location . pathname , basename )
449
+ : location . pathname || '/' ;
450
+
451
+ return [ fallbackTransactionName , 'url' ] ;
351
452
}
352
453
353
454
function updatePageloadTransaction (
@@ -356,6 +457,7 @@ function updatePageloadTransaction(
356
457
routes : RouteObject [ ] ,
357
458
matches ?: AgnosticDataRouteMatch ,
358
459
basename ?: string ,
460
+ allRoutes ?: RouteObject [ ] ,
359
461
) : void {
360
462
const branches = Array . isArray ( matches )
361
463
? matches
@@ -364,10 +466,16 @@ function updatePageloadTransaction(
364
466
if ( branches ) {
365
467
const [ name , source ] = getNormalizedName ( routes , location , branches , basename ) ;
366
468
367
- getCurrentScope ( ) . setTransactionName ( name ) ;
469
+ let txnName = name ;
470
+
471
+ if ( locationIsInsideDescendantRoute ( location , allRoutes || routes ) ) {
472
+ txnName = prefixWithSlash ( rebuildRoutePathFromAllRoutes ( allRoutes || routes , location ) ) ;
473
+ }
474
+
475
+ getCurrentScope ( ) . setTransactionName ( txnName ) ;
368
476
369
477
if ( activeRootSpan ) {
370
- activeRootSpan . updateName ( name ) ;
478
+ activeRootSpan . updateName ( txnName ) ;
371
479
activeRootSpan . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_SOURCE , source ) ;
372
480
}
373
481
}
@@ -387,21 +495,27 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
387
495
return Routes ;
388
496
}
389
497
390
- let isMountRenderPass : boolean = true ;
498
+ const allRoutes : RouteObject [ ] = [ ] ;
391
499
392
500
const SentryRoutes : React . FC < P > = ( props : P ) => {
501
+ const isMountRenderPass = React . useRef ( true ) ;
502
+
393
503
const location = _useLocation ( ) ;
394
504
const navigationType = _useNavigationType ( ) ;
395
505
396
506
_useEffect (
397
507
( ) => {
398
508
const routes = _createRoutesFromChildren ( props . children ) as RouteObject [ ] ;
399
509
400
- if ( isMountRenderPass ) {
401
- updatePageloadTransaction ( getActiveRootSpan ( ) , location , routes ) ;
402
- isMountRenderPass = false ;
510
+ routes . forEach ( route => {
511
+ allRoutes . push ( ...getChildRoutesRecursively ( route ) ) ;
512
+ } ) ;
513
+
514
+ if ( isMountRenderPass . current ) {
515
+ updatePageloadTransaction ( getActiveRootSpan ( ) , location , routes , undefined , undefined , allRoutes ) ;
516
+ isMountRenderPass . current = false ;
403
517
} else {
404
- handleNavigation ( location , routes , navigationType , version ) ;
518
+ handleNavigation ( location , routes , navigationType , version , undefined , undefined , allRoutes ) ;
405
519
}
406
520
} ,
407
521
// `props.children` is purposely not included in the dependency array, because we do not want to re-run this effect
0 commit comments