1+ /* eslint-disable complexity */
12/* eslint-disable max-lines */
23// Inspired from Donnie McNeal's solution:
34// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536
@@ -174,13 +175,14 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
174175 return origUseRoutes ;
175176 }
176177
177- let isMountRenderPass : boolean = true ;
178+ const allRoutes : RouteObject [ ] = [ ] ;
178179
179180 const SentryRoutes : React . FC < {
180181 children ?: React . ReactNode ;
181182 routes : RouteObject [ ] ;
182183 locationArg ?: Partial < Location > | string ;
183184 } > = ( props : { children ?: React . ReactNode ; routes : RouteObject [ ] ; locationArg ?: Partial < Location > | string } ) => {
185+ const isMountRenderPass = React . useRef ( true ) ;
184186 const { routes, locationArg } = props ;
185187
186188 const Routes = origUseRoutes ( routes , locationArg ) ;
@@ -198,11 +200,15 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
198200 const normalizedLocation =
199201 typeof stableLocationParam === 'string' ? { pathname : stableLocationParam } : stableLocationParam ;
200202
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 ;
204210 } else {
205- handleNavigation ( normalizedLocation , routes , navigationType , version ) ;
211+ handleNavigation ( normalizedLocation , routes , navigationType , version , undefined , undefined , allRoutes ) ;
206212 }
207213 } , [ navigationType , stableLocationParam ] ) ;
208214
@@ -222,6 +228,7 @@ export function handleNavigation(
222228 version : V6CompatibleVersion ,
223229 matches ?: AgnosticDataRouteMatch ,
224230 basename ?: string ,
231+ allRoutes ?: RouteObject [ ] ,
225232) : void {
226233 const branches = Array . isArray ( matches ) ? matches : _matchRoutes ( routes , location , basename ) ;
227234
@@ -233,8 +240,14 @@ export function handleNavigation(
233240 if ( ( navigationType === 'PUSH' || navigationType === 'POP' ) && branches ) {
234241 const [ name , source ] = getNormalizedName ( routes , location , branches , basename ) ;
235242
243+ let txnName = name ;
244+
245+ if ( locationIsInsideDescendantRoute ( location , allRoutes || routes ) ) {
246+ txnName = prefixWithSlash ( rebuildRoutePathFromAllRoutes ( allRoutes || routes , location ) ) ;
247+ }
248+
236249 startBrowserTracingNavigationSpan ( client , {
237- name,
250+ name : txnName ,
238251 attributes : {
239252 [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : source ,
240253 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'navigation' ,
@@ -286,12 +299,93 @@ function sendIndexPath(pathBuilder: string, pathname: string, basename: string):
286299 return [ formattedPath , 'route' ] ;
287300}
288301
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 ( '*' ) ;
291304}
292305
293306function 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 '' ;
295389}
296390
297391function getNormalizedName (
@@ -321,7 +415,10 @@ function getNormalizedName(
321415 pathBuilder += newPath ;
322416
323417 // 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+ ) {
325422 if (
326423 // If the route defined on the element is something like
327424 // <Route path="/stores/:storeId/products/:productId" element={<div>Product</div>} />
@@ -330,13 +427,13 @@ function getNormalizedName(
330427 // eslint-disable-next-line deprecation/deprecation
331428 getNumberOfUrlSegments ( pathBuilder ) !== getNumberOfUrlSegments ( branch . pathname ) &&
332429 // We should not count wildcard operators in the url segments calculation
333- pathBuilder . slice ( - 2 ) !== '/*'
430+ ! pathEndsWithWildcard ( pathBuilder )
334431 ) {
335432 return [ ( _stripBasename ? '' : basename ) + newPath , 'route' ] ;
336433 }
337434
338435 // 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 ) ) {
340437 pathBuilder = pathBuilder . slice ( 0 , - 1 ) ;
341438 }
342439
@@ -347,7 +444,11 @@ function getNormalizedName(
347444 }
348445 }
349446
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' ] ;
351452}
352453
353454function updatePageloadTransaction (
@@ -356,6 +457,7 @@ function updatePageloadTransaction(
356457 routes : RouteObject [ ] ,
357458 matches ?: AgnosticDataRouteMatch ,
358459 basename ?: string ,
460+ allRoutes ?: RouteObject [ ] ,
359461) : void {
360462 const branches = Array . isArray ( matches )
361463 ? matches
@@ -364,10 +466,16 @@ function updatePageloadTransaction(
364466 if ( branches ) {
365467 const [ name , source ] = getNormalizedName ( routes , location , branches , basename ) ;
366468
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 ) ;
368476
369477 if ( activeRootSpan ) {
370- activeRootSpan . updateName ( name ) ;
478+ activeRootSpan . updateName ( txnName ) ;
371479 activeRootSpan . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_SOURCE , source ) ;
372480 }
373481 }
@@ -387,21 +495,27 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
387495 return Routes ;
388496 }
389497
390- let isMountRenderPass : boolean = true ;
498+ const allRoutes : RouteObject [ ] = [ ] ;
391499
392500 const SentryRoutes : React . FC < P > = ( props : P ) => {
501+ const isMountRenderPass = React . useRef ( true ) ;
502+
393503 const location = _useLocation ( ) ;
394504 const navigationType = _useNavigationType ( ) ;
395505
396506 _useEffect (
397507 ( ) => {
398508 const routes = _createRoutesFromChildren ( props . children ) as RouteObject [ ] ;
399509
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 ;
403517 } else {
404- handleNavigation ( location , routes , navigationType , version ) ;
518+ handleNavigation ( location , routes , navigationType , version , undefined , undefined , allRoutes ) ;
405519 }
406520 } ,
407521 // `props.children` is purposely not included in the dependency array, because we do not want to re-run this effect
0 commit comments