diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index de87e5bb6881..c2dc56687571 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -35,6 +35,7 @@ let _createRoutesFromChildren: CreateRoutesFromChildren; let _matchRoutes: MatchRoutes; let _customStartTransaction: (context: TransactionContext) => Transaction | undefined; let _startTransactionOnLocationChange: boolean; +let _stripBasename: boolean = false; const SENTRY_TAGS = { 'routing.instrumentation': 'react-router-v6', @@ -46,6 +47,7 @@ export function reactRouterV6Instrumentation( useNavigationType: UseNavigationType, createRoutesFromChildren: CreateRoutesFromChildren, matchRoutes: MatchRoutes, + stripBasename?: boolean, ) { return ( customStartTransaction: (context: TransactionContext) => Transaction | undefined, @@ -70,12 +72,40 @@ export function reactRouterV6Instrumentation( _useNavigationType = useNavigationType; _matchRoutes = matchRoutes; _createRoutesFromChildren = createRoutesFromChildren; + _stripBasename = stripBasename || false; _customStartTransaction = customStartTransaction; _startTransactionOnLocationChange = startTransactionOnLocationChange; }; } +/** + * Strip the basename from a pathname if exists. + * + * Vendored and modified from `react-router` + * https://github.com/remix-run/react-router/blob/462bb712156a3f739d6139a0f14810b76b002df6/packages/router/utils.ts#L1038 + */ +function stripBasenameFromPathname(pathname: string, basename: string): string { + if (!basename || basename === '/') { + return pathname; + } + + if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) { + return pathname; + } + + // We want to leave trailing slash behavior in the user's control, so if they + // specify a basename with a trailing slash, we should support it + const startIndex = basename.endsWith('/') ? basename.length - 1 : basename.length; + const nextChar = pathname.charAt(startIndex); + if (nextChar && nextChar !== '/') { + // pathname does not start with basename/ + return pathname; + } + + return pathname.slice(startIndex) || '/'; +} + function getNormalizedName( routes: RouteObject[], location: Location, @@ -83,7 +113,7 @@ function getNormalizedName( basename: string = '', ): [string, TransactionSource] { if (!routes || routes.length === 0) { - return [location.pathname, 'url']; + return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; } let pathBuilder = ''; @@ -95,7 +125,7 @@ function getNormalizedName( if (route) { // Early return if index route if (route.index) { - return [branch.pathname, 'route']; + return [_stripBasename ? stripBasenameFromPathname(branch.pathname, basename) : branch.pathname, 'route']; } const path = route.path; @@ -112,16 +142,16 @@ function getNormalizedName( // We should not count wildcard operators in the url segments calculation pathBuilder.slice(-2) !== '/*' ) { - return [basename + newPath, 'route']; + return [(_stripBasename ? '' : basename) + newPath, 'route']; } - return [basename + pathBuilder, 'route']; + return [(_stripBasename ? '' : basename) + pathBuilder, 'route']; } } } } } - return [location.pathname, 'url']; + return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; } function updatePageloadTransaction( diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index d6b9c0c45b49..29fe612f7e97 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -26,6 +26,7 @@ describe('React Router v6.4', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; + stripBasename?: boolean; }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, @@ -46,6 +47,7 @@ describe('React Router v6.4', () => { useNavigationType, createRoutesFromChildren, matchRoutes, + options.stripBasename, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } @@ -359,5 +361,93 @@ describe('React Router v6.4', () => { metadata: { source: 'route' }, }); }); + + it('strips `basename` from transaction names of parameterized paths', () => { + const [mockStartTransaction] = createInstrumentation({ + stripBasename: true, + }); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: ':orgId', + children: [ + { + path: 'users', + children: [ + { + path: ':userId', + element:
User
, + }, + ], + }, + ], + }, + ], + { + initialEntries: ['/admin'], + basename: '/admin', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/:orgId/users/:userId', + op: 'navigation', + origin: 'auto.navigation.react.reactrouterv6', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('strips `basename` from transaction names of non-parameterized paths', () => { + const [mockStartTransaction] = createInstrumentation({ + stripBasename: true, + }); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: 'us', + element:
Us
, + }, + ], + }, + ], + { + initialEntries: ['/app'], + basename: '/app', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/us', + op: 'navigation', + origin: 'auto.navigation.react.reactrouterv6', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); }); });