Skip to content

Commit 8ae7fee

Browse files
committed
feat: React Router v4/v5 instrumentation
1 parent a1d8375 commit 8ae7fee

File tree

8 files changed

+813
-30
lines changed

8 files changed

+813
-30
lines changed

packages/react/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,25 @@
3030
"devDependencies": {
3131
"@testing-library/react": "^10.0.6",
3232
"@testing-library/react-hooks": "^3.3.0",
33+
"@types/history-4": "npm:@types/history@4.7.7",
34+
"@types/history-5": "npm:@types/history@4.7.7",
3335
"@types/hoist-non-react-statics": "^3.3.1",
3436
"@types/react": "^16.9.35",
3537
"@types/react-router-3": "npm:@types/react-router@3.0.20",
38+
"@types/react-router-4": "npm:@types/react-router@5.0.0",
39+
"@types/react-router-5": "npm:@types/react-router@5.0.0",
40+
"history-4": "npm:history@4.6.0",
41+
"history-5": "npm:history@4.9.0",
3642
"jest": "^24.7.1",
3743
"jsdom": "^16.2.2",
3844
"npm-run-all": "^4.1.2",
3945
"prettier": "^1.17.0",
4046
"prettier-check": "^2.0.0",
4147
"react": "^16.0.0",
4248
"react-dom": "^16.0.0",
43-
"react-router-3": "npm:react-router@^3.2.0",
49+
"react-router-3": "npm:react-router@3.2.0",
50+
"react-router-4": "npm:react-router@4.1.0",
51+
"react-router-5": "npm:react-router@5.0.0",
4452
"react-test-renderer": "^16.13.1",
4553
"redux": "^4.0.5",
4654
"rimraf": "^2.6.3",

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ export { Profiler, withProfiler, useProfiler } from './profiler';
2727
export { ErrorBoundary, withErrorBoundary } from './errorboundary';
2828
export { createReduxEnhancer } from './redux';
2929
export { reactRouterV3Instrumentation } from './reactrouterv3';
30+
export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter';
3031

3132
createReactEventProcessor();

packages/react/src/reactrouter.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Transaction } from '@sentry/types';
2+
import { getGlobalObject } from '@sentry/utils';
3+
import * as React from 'react';
4+
5+
import { Action, Location, ReactRouterInstrumentation } from './types';
6+
7+
type Match = { path: string; url: string; params: Record<string, any>; isExact: boolean };
8+
9+
export type RouterHistory = {
10+
location?: Location;
11+
listen?(cb: (location: Location, action: Action) => void): void;
12+
} & Record<string, any>;
13+
14+
export type RouteConfig = {
15+
path?: string | string[];
16+
exact?: boolean;
17+
component?: JSX.Element;
18+
routes?: RouteConfig[];
19+
[propName: string]: any;
20+
};
21+
22+
type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null;
23+
24+
const global = getGlobalObject<Window>();
25+
26+
let activeTransaction: Transaction | undefined;
27+
28+
export function reactRouterV4Instrumentation(
29+
history: RouterHistory,
30+
routes?: RouteConfig[],
31+
matchPath?: MatchPath,
32+
): ReactRouterInstrumentation {
33+
return reactRouterInstrumentation(history, 'react-router-v4', routes, matchPath);
34+
}
35+
36+
export function reactRouterV5Instrumentation(
37+
history: RouterHistory,
38+
routes?: RouteConfig[],
39+
matchPath?: MatchPath,
40+
): ReactRouterInstrumentation {
41+
return reactRouterInstrumentation(history, 'react-router-v5', routes, matchPath);
42+
}
43+
44+
function reactRouterInstrumentation(
45+
history: RouterHistory,
46+
name: string,
47+
allRoutes: RouteConfig[] = [],
48+
matchPath?: MatchPath,
49+
): ReactRouterInstrumentation {
50+
function getName(pathname: string): string {
51+
if (allRoutes === [] || !matchPath) {
52+
return pathname;
53+
}
54+
55+
const branches = matchRoutes(allRoutes, pathname, matchPath);
56+
// tslint:disable-next-line: prefer-for-of
57+
for (let x = 0; x < branches.length; x++) {
58+
if (branches[x].match.isExact) {
59+
return branches[x].match.path;
60+
}
61+
}
62+
63+
return pathname;
64+
}
65+
66+
return (startTransaction, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true) => {
67+
if (startTransactionOnPageLoad && global && global.location) {
68+
activeTransaction = startTransaction({
69+
name: getName(global.location.pathname),
70+
op: 'pageload',
71+
tags: {
72+
'routing.instrumentation': name,
73+
},
74+
});
75+
}
76+
77+
if (startTransactionOnLocationChange && history.listen) {
78+
history.listen((location, action) => {
79+
// console.log(location, action);
80+
if (action && action === 'PUSH') {
81+
if (activeTransaction) {
82+
activeTransaction.finish();
83+
}
84+
const tags = {
85+
'routing.instrumentation': name,
86+
};
87+
88+
activeTransaction = startTransaction({
89+
name: getName(location.pathname),
90+
op: 'navigation',
91+
tags,
92+
});
93+
}
94+
});
95+
}
96+
};
97+
}
98+
99+
/**
100+
* Matches a set of routes to a pathname
101+
* Based on implementation from
102+
*/
103+
function matchRoutes(
104+
routes: RouteConfig[],
105+
pathname: string,
106+
matchPath: MatchPath,
107+
branch: Array<{ route: RouteConfig; match: Match }> = [],
108+
): Array<{ route: RouteConfig; match: Match }> {
109+
routes.some(route => {
110+
const match = route.path
111+
? matchPath(pathname, route)
112+
: branch.length
113+
? branch[branch.length - 1].match // use parent match
114+
: computeRootMatch(pathname); // use default "root" match
115+
116+
if (match) {
117+
branch.push({ route, match });
118+
119+
if (route.routes) {
120+
matchRoutes(route.routes, pathname, matchPath, branch);
121+
}
122+
}
123+
124+
return !!match;
125+
});
126+
127+
return branch;
128+
}
129+
130+
function computeRootMatch(pathname: string): Match {
131+
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
132+
}
133+
134+
export const withSentryRouting = (Route: React.ElementType) => (props: any) => {
135+
// tslint:disable: no-unsafe-any
136+
if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) {
137+
activeTransaction.setName(props.computedMatch.path);
138+
}
139+
return <Route {...props} />;
140+
// tslint:enable: no-unsafe-any
141+
};

packages/react/src/reactrouterv3.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
import { Transaction, TransactionContext } from '@sentry/types';
22
import { getGlobalObject } from '@sentry/utils';
33

4-
type ReactRouterInstrumentation = <T extends Transaction>(
5-
startTransaction: (context: TransactionContext) => T | undefined,
6-
startTransactionOnPageLoad?: boolean,
7-
startTransactionOnLocationChange?: boolean,
8-
) => void;
4+
import { Location, ReactRouterInstrumentation } from './types';
95

106
// Many of the types below had to be mocked out to prevent typescript issues
117
// these types are required for correct functionality.
128

9+
type HistoryV3 = {
10+
location?: Location;
11+
listen?(cb: (location: Location) => void): void;
12+
} & Record<string, any>;
13+
1314
export type Route = { path?: string; childRoutes?: Route[] };
1415

1516
export type Match = (
1617
props: { location: Location; routes: Route[] },
1718
cb: (error?: Error, _redirectLocation?: Location, renderProps?: { routes?: Route[] }) => void,
1819
) => void;
1920

20-
type Location = {
21-
pathname: string;
22-
action?: 'PUSH' | 'REPLACE' | 'POP';
23-
} & Record<string, any>;
24-
25-
type History = {
26-
location?: Location;
27-
listen?(cb: (location: Location) => void): void;
28-
} & Record<string, any>;
29-
3021
const global = getGlobalObject<Window>();
3122

3223
/**
@@ -38,7 +29,7 @@ const global = getGlobalObject<Window>();
3829
* @param match `Router.match` utility
3930
*/
4031
export function reactRouterV3Instrumentation(
41-
history: History,
32+
history: HistoryV3,
4233
routes: Route[],
4334
match: Match,
4435
): ReactRouterInstrumentation {

packages/react/src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Transaction, TransactionContext } from '@sentry/types';
2+
3+
export type Action = 'PUSH' | 'REPLACE' | 'POP';
4+
5+
export type Location = {
6+
pathname: string;
7+
action?: Action;
8+
} & Record<string, any>;
9+
10+
export type ReactRouterInstrumentation = <T extends Transaction>(
11+
startTransaction: (context: TransactionContext) => T | undefined,
12+
startTransactionOnPageLoad?: boolean,
13+
startTransactionOnLocationChange?: boolean,
14+
) => void;

0 commit comments

Comments
 (0)