diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 1ec1ae4b7d35..2576ba664f40 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // Inspired from Donnie McNeal's solution: // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 @@ -141,6 +142,29 @@ function stripBasenameFromPathname(pathname: string, basename: string): string { return pathname.slice(startIndex) || '/'; } +function sendIndexPath(pathBuilder: string, pathname: string, basename: string): [string, TransactionSource] { + const reconstructedPath = pathBuilder || _stripBasename ? stripBasenameFromPathname(pathname, basename) : pathname; + + const formattedPath = + // If the path ends with a slash, remove it + reconstructedPath[reconstructedPath.length - 1] === '/' + ? reconstructedPath.slice(0, -1) + : // If the path ends with a wildcard, remove it + reconstructedPath.slice(-2) === '/*' + ? reconstructedPath.slice(0, -1) + : reconstructedPath; + + return [formattedPath, 'route']; +} + +function pathEndsWithWildcard(path: string, branch: RouteMatch): boolean { + return (path.slice(-2) === '/*' && branch.route.children && branch.route.children.length > 0) || false; +} + +function pathIsWildcardAndHasChildren(path: string, branch: RouteMatch): boolean { + return (path === '*' && branch.route.children && branch.route.children.length > 0) || false; +} + function getNormalizedName( routes: RouteObject[], location: Location, @@ -158,14 +182,16 @@ function getNormalizedName( if (route) { // Early return if index route if (route.index) { - return [_stripBasename ? stripBasenameFromPathname(branch.pathname, basename) : branch.pathname, 'route']; + return sendIndexPath(pathBuilder, branch.pathname, basename); } - const path = route.path; - if (path) { + + // If path is not a wildcard and has no child routes, append the path + if (path && !pathIsWildcardAndHasChildren(path, branch)) { const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`; pathBuilder += newPath; + // If the path matches the current location, return the path if (basename + branch.pathname === location.pathname) { if ( // If the route defined on the element is something like @@ -177,6 +203,12 @@ function getNormalizedName( ) { return [(_stripBasename ? '' : basename) + newPath, 'route']; } + + // if the last character of the pathbuilder is a wildcard and there are children, remove the wildcard + if (pathEndsWithWildcard(pathBuilder, branch)) { + pathBuilder = pathBuilder.slice(0, -1); + } + return [(_stripBasename ? '' : basename) + pathBuilder, 'route']; } } diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index fbb135a5449c..de65159c56e4 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -391,6 +391,106 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it('works with wildcard routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + }> + } /> + Account Page} /> + + }> + Project Page}> + Project Page Root} /> + Editor}> + View Canvas} /> + Space Canvas} /> + + + + + No Match Page} /> + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested wildcard routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + }> + } /> + Account Page} /> + + }> + Project Page}> + Project Page Root} /> + Editor}> + }> + View Canvas} /> + Space Canvas} /> + + + + + + No Match Page} /> + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + it("updates the scope's `transactionName` on a navigation", () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -849,6 +949,84 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it('works with wildcard routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + index: true, + element: , + }, + { + path: '*', + element: <>, + children: [ + { + path: 'profile', + element: <>, + }, + { + path: 'param-page', + element: , + children: [ + { + path: '*', + element: , + children: [ + { + path: ':id', + element: <>, + children: [ + { + element: <>, + path: 'details', + children: [ + { + element: <>, + path: ':superId', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/param-page/:id/details/:superId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + it('does not add double slashes to URLS', () => { const client = createMockBrowserClient(); setCurrentClient(client);