Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): Add tracing support for React Router 6.4 createBrowserRouter. #6172

Merged
merged 5 commits into from Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
};
}