Skip to content

Commit 16f57a5

Browse files
committed
feat: React Router v3 instrumentation
1 parent 33a4910 commit 16f57a5

File tree

5 files changed

+307
-7
lines changed

5 files changed

+307
-7
lines changed

packages/react/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@
2828
"react-dom": "15.x || 16.x"
2929
},
3030
"devDependencies": {
31+
"@sentry/tracing": "5.19.2",
3132
"@testing-library/react": "^10.0.6",
3233
"@testing-library/react-hooks": "^3.3.0",
34+
"@types/history": "^4.7.6",
3335
"@types/hoist-non-react-statics": "^3.3.1",
3436
"@types/react": "^16.9.35",
37+
"@types/react-router-3": "npm:@types/[email protected]",
3538
"jest": "^24.7.1",
3639
"jsdom": "^16.2.2",
3740
"npm-run-all": "^4.1.2",
3841
"prettier": "^1.17.0",
3942
"prettier-check": "^2.0.0",
4043
"react": "^16.0.0",
4144
"react-dom": "^16.0.0",
45+
"react-router-3": "npm:[email protected]",
4246
"react-test-renderer": "^16.13.1",
4347
"redux": "^4.0.5",
4448
"rimraf": "^2.6.3",

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ export * from '@sentry/browser';
2626
export { Profiler, withProfiler, useProfiler } from './profiler';
2727
export { ErrorBoundary, withErrorBoundary } from './errorboundary';
2828
export { createReduxEnhancer } from './redux';
29+
export { reactRouterV3Instrumenation } from './reactrouter';
2930

3031
createReactEventProcessor();

packages/react/src/reactrouter.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Transaction, TransactionContext } from '@sentry/types';
2+
3+
type routingInstrumentation = (
4+
startTransaction: (context: TransactionContext) => Transaction,
5+
startTransactionOnPageLoad?: boolean,
6+
startTransactionOnLocationChange?: boolean,
7+
) => void;
8+
9+
// Many of the types below had to be mocked out to lower bundle size.
10+
type PlainRoute = { path: string; childRoutes: PlainRoute[] };
11+
12+
type Match = (
13+
props: { location: Location; routes: PlainRoute[] },
14+
cb: (error?: Error, _redirectLocation?: Location, renderProps?: { routes?: PlainRoute[] }) => void,
15+
) => void;
16+
17+
type Location = {
18+
pathname: string;
19+
action: 'PUSH' | 'REPLACE' | 'POP';
20+
key: string;
21+
} & Record<string, any>;
22+
23+
type History = {
24+
location: Location;
25+
listen(cb: (location: Location) => void): void;
26+
} & Record<string, any>;
27+
28+
/**
29+
* Creates routing instrumentation for React Router v3
30+
*
31+
* @param history object from the `history` library
32+
* @param routes a list of all routes, should be
33+
* @param match `Router.match` utility
34+
*/
35+
export function reactRouterV3Instrumenation(
36+
history: History,
37+
routes: PlainRoute[],
38+
match: Match,
39+
): routingInstrumentation {
40+
return (
41+
startTransaction: (context: TransactionContext) => Transaction | undefined,
42+
startTransactionOnPageLoad: boolean = true,
43+
startTransactionOnLocationChange: boolean = true,
44+
) => {
45+
let activeTransaction: Transaction | undefined;
46+
let name = normalizeTransactionName(routes, history.location, match);
47+
if (startTransactionOnPageLoad) {
48+
activeTransaction = startTransaction({
49+
name,
50+
op: 'pageload',
51+
tags: {
52+
from: name,
53+
routingInstrumentation: 'react-router-v3',
54+
},
55+
});
56+
}
57+
58+
if (startTransactionOnLocationChange) {
59+
history.listen(location => {
60+
if (location.action === 'PUSH') {
61+
if (activeTransaction) {
62+
activeTransaction.finish();
63+
}
64+
const tags = { from: name, routingInstrumentation: 'react-router-v3' };
65+
name = normalizeTransactionName(routes, history.location, match);
66+
activeTransaction = startTransaction({
67+
name,
68+
op: 'navigation',
69+
tags,
70+
});
71+
}
72+
});
73+
}
74+
};
75+
}
76+
77+
export function normalizeTransactionName(appRoutes: PlainRoute[], location: Location, match: Match): string {
78+
const defaultName = location.pathname;
79+
match(
80+
{
81+
location,
82+
routes: appRoutes,
83+
},
84+
(error?: Error, _redirectLocation?: Location, renderProps?: { routes?: PlainRoute[] }) => {
85+
if (error || !renderProps) {
86+
return defaultName;
87+
}
88+
89+
const routePath = getRouteStringFromRoutes(renderProps.routes || []);
90+
91+
if (routePath.length === 0 || routePath === '/*') {
92+
return defaultName;
93+
}
94+
95+
return routePath;
96+
},
97+
);
98+
99+
return defaultName;
100+
}
101+
102+
function getRouteStringFromRoutes(routes: PlainRoute[]): string {
103+
if (!Array.isArray(routes)) {
104+
return '';
105+
}
106+
107+
const routesWithPaths: PlainRoute[] = routes.filter((route: PlainRoute) => !!route.path);
108+
109+
let index = -1;
110+
for (let x = routesWithPaths.length; x >= 0; x--) {
111+
if (routesWithPaths[x].path.startsWith('/')) {
112+
index = x;
113+
break;
114+
}
115+
}
116+
117+
return routesWithPaths
118+
.slice(index)
119+
.filter(({ path }) => !!path)
120+
.map(({ path }) => path)
121+
.join('');
122+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { browserHistory } from 'react-router-3';
2+
import { routes } from './routes';
3+
4+
describe('React Router V3', () => {
5+
it('works', () => {});
6+
});

0 commit comments

Comments
 (0)