Skip to content

Commit

Permalink
feat(react): Add stripBasename option for React Router 6. (#10314)
Browse files Browse the repository at this point in the history
This PR adds a new option for React Router 6 integration,
`stripBasename` for leaving out the `basename` from transaction names.
  • Loading branch information
onurtemizkan committed Jan 24, 2024
1 parent 656b737 commit b67d9b4
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 5 deletions.
40 changes: 35 additions & 5 deletions packages/react/src/reactrouterv6.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -46,6 +47,7 @@ export function reactRouterV6Instrumentation(
useNavigationType: UseNavigationType,
createRoutesFromChildren: CreateRoutesFromChildren,
matchRoutes: MatchRoutes,
stripBasename?: boolean,
) {
return (
customStartTransaction: (context: TransactionContext) => Transaction | undefined,
Expand All @@ -70,20 +72,48 @@ 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,
branches: RouteMatch[],
basename: string = '',
): [string, TransactionSource] {
if (!routes || routes.length === 0) {
return [location.pathname, 'url'];
return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
}

let pathBuilder = '';
Expand All @@ -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;
Expand All @@ -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(
Expand Down
90 changes: 90 additions & 0 deletions packages/react/test/reactrouterv6.4.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -46,6 +47,7 @@ describe('React Router v6.4', () => {
useNavigationType,
createRoutesFromChildren,
matchRoutes,
options.stripBasename,
)(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange);
return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }];
}
Expand Down Expand Up @@ -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: <Navigate to="/some-org-id/users/some-user-id" />,
},
{
path: ':orgId',
children: [
{
path: 'users',
children: [
{
path: ':userId',
element: <div>User</div>,
},
],
},
],
},
],
{
initialEntries: ['/admin'],
basename: '/admin',
},
);

// @ts-expect-error router is fine
render(<RouterProvider router={router} />);

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: <Navigate to="/about/us" />,
},
{
path: 'about',
element: <div>About</div>,
children: [
{
path: 'us',
element: <div>Us</div>,
},
],
},
],
{
initialEntries: ['/app'],
basename: '/app',
},
);

// @ts-expect-error router is fine
render(<RouterProvider router={router} />);

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' },
});
});
});
});

0 comments on commit b67d9b4

Please sign in to comment.