Skip to content

Commit

Permalink
feat(react): Add tracing support for React Router 6.4 `createBrowserR…
Browse files Browse the repository at this point in the history
…outer`. (#6172)

Adds tracing support for React Router 6.4's [`createBrowserRouter`](https://reactrouter.com/en/main/routers/create-browser-router#createbrowserrouter) (and seamlessly [`createMemoryRouter`](https://reactrouter.com/en/main/routers/create-memory-router#creatememoryrouter)).

React Router 6.4 allows us to reach the router state itself, without having to use React hooks. So, the implementation is much simpler.
  • Loading branch information
onurtemizkan committed Nov 22, 2022
1 parent 77dd704 commit daacd38
Show file tree
Hide file tree
Showing 6 changed files with 574 additions and 73 deletions.
3 changes: 3 additions & 0 deletions packages/react/package.json
Expand Up @@ -31,6 +31,7 @@
"@types/history-4": "npm:@types/history@4.7.8",
"@types/history-5": "npm:@types/history@4.7.8",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/node-fetch": "^2.6.0",
"@types/react": "^17.0.3",
"@types/react-router-3": "npm:@types/react-router@3.0.24",
"@types/react-router-4": "npm:@types/react-router@5.1.14",
Expand All @@ -39,12 +40,14 @@
"eslint-plugin-react-hooks": "^4.0.8",
"history-4": "npm:history@4.6.0",
"history-5": "npm:history@4.9.0",
"node-fetch": "^2.6.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-3": "npm:react-router@3.2.0",
"react-router-4": "npm:react-router@4.1.0",
"react-router-5": "npm:react-router@5.0.0",
"react-router-6": "npm:react-router@6.3.0",
"react-router-6.4": "npm:react-router@6.4.2",
"redux": "^4.0.5"
},
"scripts": {
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/index.ts
Expand Up @@ -7,4 +7,9 @@ export { ErrorBoundary, withErrorBoundary } from './errorboundary';
export { createReduxEnhancer } from './redux';
export { reactRouterV3Instrumentation } from './reactrouterv3';
export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter';
export { reactRouterV6Instrumentation, withSentryReactRouterV6Routing, wrapUseRoutes } from './reactrouterv6';
export {
reactRouterV6Instrumentation,
withSentryReactRouterV6Routing,
wrapUseRoutes,
wrapCreateBrowserRouter,
} from './reactrouterv6';
128 changes: 57 additions & 71 deletions packages/react/src/reactrouterv6.tsx
Expand Up @@ -7,68 +7,22 @@ import { getNumberOfUrlSegments, logger } from '@sentry/utils';
import hoistNonReactStatics from 'hoist-non-react-statics';
import React from 'react';

import { Action, Location } from './types';

interface NonIndexRouteObject {
caseSensitive?: boolean;
children?: RouteObject[];
element?: React.ReactNode | null;
index?: false;
path?: string;
}

interface IndexRouteObject {
caseSensitive?: boolean;
children?: undefined;
element?: React.ReactNode | null;
index?: true;
path?: string;
}

// This type was originally just `type RouteObject = IndexRouteObject`, but this was changed
// in https://github.com/remix-run/react-router/pull/9366, which was released with `6.4.2`
// See https://github.com/remix-run/react-router/issues/9427 for a discussion on this.
type RouteObject = IndexRouteObject | NonIndexRouteObject;

type Params<Key extends string = string> = {
readonly [key in Key]: string | undefined;
};

type UseRoutes = (routes: RouteObject[], locationArg?: Partial<Location> | string) => React.ReactElement | null;

// https://github.com/remix-run/react-router/blob/9fa54d643134cd75a0335581a75db8100ed42828/packages/react-router/lib/router.ts#L114-L134
interface RouteMatch<ParamKey extends string = string> {
/**
* The names and values of dynamic parameters in the URL.
*/
params: Params<ParamKey>;
/**
* The portion of the URL pathname that was matched.
*/
pathname: string;
/**
* The portion of the URL pathname that was matched before child routes.
*/
pathnameBase: string;
/**
* The route object that was used to match.
*/
route: RouteObject;
}

type UseEffect = (cb: () => void, deps: unknown[]) => void;
type UseLocation = () => Location;
type UseNavigationType = () => Action;

// For both of these types, use `any` instead of `RouteObject[]` or `RouteMatch[]`.
// Have to do this so we maintain backwards compatability between
// react-router > 6.0.0 and >= 6.4.2.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RouteObjectArrayAlias = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RouteMatchAlias = any;
type CreateRoutesFromChildren = (children: JSX.Element[]) => RouteObjectArrayAlias;
type MatchRoutes = (routes: RouteObjectArrayAlias, location: Location) => RouteMatchAlias[] | null;
import {
Action,
AgnosticDataRouteMatch,
CreateRouterFunction,
CreateRoutesFromChildren,
Location,
MatchRoutes,
RouteMatch,
RouteObject,
Router,
RouterState,
UseEffect,
UseLocation,
UseNavigationType,
UseRoutes,
} from './types';

let activeTransaction: Transaction | undefined;

Expand Down Expand Up @@ -122,14 +76,12 @@ export function reactRouterV6Instrumentation(
function getNormalizedName(
routes: RouteObject[],
location: Location,
matchRoutes: MatchRoutes,
branches: RouteMatch[],
): [string, TransactionSource] {
if (!routes || routes.length === 0 || !matchRoutes) {
if (!routes || routes.length === 0) {
return [location.pathname, 'url'];
}

const branches = matchRoutes(routes, location) as unknown as RouteMatch[];

let pathBuilder = '';
if (branches) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
Expand Down Expand Up @@ -167,9 +119,11 @@ function getNormalizedName(
return [location.pathname, 'url'];
}

function updatePageloadTransaction(location: Location, routes: RouteObject[]): void {
if (activeTransaction) {
activeTransaction.setName(...getNormalizedName(routes, location, _matchRoutes));
function updatePageloadTransaction(location: Location, routes: RouteObject[], matches?: AgnosticDataRouteMatch): void {
const branches = Array.isArray(matches) ? matches : (_matchRoutes(routes, location) as unknown as RouteMatch[]);

if (activeTransaction && branches) {
activeTransaction.setName(...getNormalizedName(routes, location, branches));
}
}

Expand All @@ -178,6 +132,7 @@ function handleNavigation(
routes: RouteObject[],
navigationType: Action,
isBaseLocation: boolean,
matches?: AgnosticDataRouteMatch,
): void {
if (isBaseLocation) {
if (activeTransaction) {
Expand All @@ -187,12 +142,14 @@ function handleNavigation(
return;
}

if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) {
const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location);

if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP') && branches) {
if (activeTransaction) {
activeTransaction.finish();
}

const [name, source] = getNormalizedName(routes, location, _matchRoutes);
const [name, source] = getNormalizedName(routes, location, branches);
activeTransaction = _customStartTransaction({
name,
op: 'navigation',
Expand Down Expand Up @@ -294,3 +251,32 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes {
return <SentryRoutes />;
};
}

export function wrapCreateBrowserRouter(createRouterFunction: CreateRouterFunction): CreateRouterFunction {
// `opts` for createBrowserHistory and createMemoryHistory are different, but also not relevant for us at the moment.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (routes: RouteObject[], opts?: any): Router {
const router = createRouterFunction(routes, opts);

// The initial load ends when `createBrowserRouter` is called.
// This is the earliest convenient time to update the transaction name.
// Callbacks to `router.subscribe` are not called for the initial load.
if (router.state.historyAction === 'POP' && activeTransaction) {
updatePageloadTransaction(router.state.location, routes);
}

router.subscribe((state: RouterState) => {
const location = state.location;

if (
_startTransactionOnLocationChange &&
(state.historyAction === 'PUSH' || state.historyAction === 'POP') &&
activeTransaction
) {
handleNavigation(location, routes, state.historyAction, false);
}
});

return router;
};
}

0 comments on commit daacd38

Please sign in to comment.