Skip to content

Commit ef9196b

Browse files
authored
feat(react): Add reactRouterV4/V5BrowserTracingIntegration for react router v4 & v5 (#10488)
This adds new `reactRouterV4BrowserTracingIntegration()` and `reactRouterV5BrowserTracingIntegration()` exports, deprecating these old routing instrumentations. I opted to leave as much as possible as-is for now, except for streamlining the attributes/tags we use for the instrumentation. Tests lifted from #10430
1 parent 8176f01 commit ef9196b

File tree

4 files changed

+934
-106
lines changed

4 files changed

+934
-106
lines changed

packages/react/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ export type { ErrorBoundaryProps, FallbackRender } from './errorboundary';
66
export { ErrorBoundary, withErrorBoundary } from './errorboundary';
77
export { createReduxEnhancer } from './redux';
88
export { reactRouterV3Instrumentation } from './reactrouterv3';
9-
export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter';
9+
export {
10+
// eslint-disable-next-line deprecation/deprecation
11+
reactRouterV4Instrumentation,
12+
// eslint-disable-next-line deprecation/deprecation
13+
reactRouterV5Instrumentation,
14+
withSentryRouting,
15+
reactRouterV4BrowserTracingIntegration,
16+
reactRouterV5BrowserTracingIntegration,
17+
} from './reactrouter';
1018
export {
1119
reactRouterV6Instrumentation,
1220
withSentryReactRouterV6Routing,

packages/react/src/reactrouter.tsx

Lines changed: 144 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1-
import { WINDOW } from '@sentry/browser';
2-
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
3-
import type { Transaction, TransactionSource } from '@sentry/types';
1+
import {
2+
WINDOW,
3+
browserTracingIntegration,
4+
startBrowserTracingNavigationSpan,
5+
startBrowserTracingPageLoadSpan,
6+
} from '@sentry/browser';
7+
import {
8+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
9+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
10+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
11+
getActiveSpan,
12+
getRootSpan,
13+
spanToJSON,
14+
} from '@sentry/core';
15+
import type { Integration, Span, StartSpanOptions, Transaction, TransactionSource } from '@sentry/types';
416
import hoistNonReactStatics from 'hoist-non-react-statics';
517
import * as React from 'react';
618

@@ -23,29 +35,121 @@ export type RouteConfig = {
2335
routes?: RouteConfig[];
2436
};
2537

26-
type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any
38+
export type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any
39+
40+
interface ReactRouterOptions {
41+
history: RouterHistory;
42+
routes?: RouteConfig[];
43+
matchPath?: MatchPath;
44+
}
2745

2846
let activeTransaction: Transaction | undefined;
2947

48+
/**
49+
* A browser tracing integration that uses React Router v4 to instrument navigations.
50+
* Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
51+
*/
52+
export function reactRouterV4BrowserTracingIntegration(
53+
options: Parameters<typeof browserTracingIntegration>[0] & ReactRouterOptions,
54+
): Integration {
55+
const integration = browserTracingIntegration({
56+
...options,
57+
instrumentPageLoad: false,
58+
instrumentNavigation: false,
59+
});
60+
61+
const { history, routes, matchPath, instrumentPageLoad = true, instrumentNavigation = true } = options;
62+
63+
return {
64+
...integration,
65+
afterAllSetup(client) {
66+
integration.afterAllSetup(client);
67+
68+
const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => {
69+
startBrowserTracingPageLoadSpan(client, startSpanOptions);
70+
return undefined;
71+
};
72+
73+
const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => {
74+
startBrowserTracingNavigationSpan(client, startSpanOptions);
75+
return undefined;
76+
};
77+
78+
// eslint-disable-next-line deprecation/deprecation
79+
const instrumentation = reactRouterV4Instrumentation(history, routes, matchPath);
80+
81+
// Now instrument page load & navigation with correct settings
82+
instrumentation(startPageloadCallback, instrumentPageLoad, false);
83+
instrumentation(startNavigationCallback, false, instrumentNavigation);
84+
},
85+
};
86+
}
87+
88+
/**
89+
* A browser tracing integration that uses React Router v5 to instrument navigations.
90+
* Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
91+
*/
92+
export function reactRouterV5BrowserTracingIntegration(
93+
options: Parameters<typeof browserTracingIntegration>[0] & ReactRouterOptions,
94+
): Integration {
95+
const integration = browserTracingIntegration({
96+
...options,
97+
instrumentPageLoad: false,
98+
instrumentNavigation: false,
99+
});
100+
101+
const { history, routes, matchPath } = options;
102+
103+
return {
104+
...integration,
105+
afterAllSetup(client) {
106+
integration.afterAllSetup(client);
107+
108+
const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => {
109+
startBrowserTracingPageLoadSpan(client, startSpanOptions);
110+
return undefined;
111+
};
112+
113+
const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => {
114+
startBrowserTracingNavigationSpan(client, startSpanOptions);
115+
return undefined;
116+
};
117+
118+
// eslint-disable-next-line deprecation/deprecation
119+
const instrumentation = reactRouterV5Instrumentation(history, routes, matchPath);
120+
121+
// Now instrument page load & navigation with correct settings
122+
instrumentation(startPageloadCallback, options.instrumentPageLoad, false);
123+
instrumentation(startNavigationCallback, false, options.instrumentNavigation);
124+
},
125+
};
126+
}
127+
128+
/**
129+
* @deprecated Use `browserTracingReactRouterV4()` instead.
130+
*/
30131
export function reactRouterV4Instrumentation(
31132
history: RouterHistory,
32133
routes?: RouteConfig[],
33134
matchPath?: MatchPath,
34135
): ReactRouterInstrumentation {
35-
return createReactRouterInstrumentation(history, 'react-router-v4', routes, matchPath);
136+
return createReactRouterInstrumentation(history, 'reactrouter_v4', routes, matchPath);
36137
}
37138

139+
/**
140+
* @deprecated Use `browserTracingReactRouterV5()` instead.
141+
*/
38142
export function reactRouterV5Instrumentation(
39143
history: RouterHistory,
40144
routes?: RouteConfig[],
41145
matchPath?: MatchPath,
42146
): ReactRouterInstrumentation {
43-
return createReactRouterInstrumentation(history, 'react-router-v5', routes, matchPath);
147+
return createReactRouterInstrumentation(history, 'reactrouter_v5', routes, matchPath);
44148
}
45149

46150
function createReactRouterInstrumentation(
47151
history: RouterHistory,
48-
name: string,
152+
instrumentationName: string,
49153
allRoutes: RouteConfig[] = [],
50154
matchPath?: MatchPath,
51155
): ReactRouterInstrumentation {
@@ -83,21 +187,17 @@ function createReactRouterInstrumentation(
83187
return [pathname, 'url'];
84188
}
85189

86-
const tags = {
87-
'routing.instrumentation': name,
88-
};
89-
90190
return (customStartTransaction, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true): void => {
91191
const initPathName = getInitPathName();
192+
92193
if (startTransactionOnPageLoad && initPathName) {
93194
const [name, source] = normalizeTransactionName(initPathName);
94195
activeTransaction = customStartTransaction({
95196
name,
96-
op: 'pageload',
97-
origin: 'auto.pageload.react.reactrouter',
98-
tags,
99-
metadata: {
100-
source,
197+
attributes: {
198+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
199+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`,
200+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
101201
},
102202
});
103203
}
@@ -112,11 +212,10 @@ function createReactRouterInstrumentation(
112212
const [name, source] = normalizeTransactionName(location.pathname);
113213
activeTransaction = customStartTransaction({
114214
name,
115-
op: 'navigation',
116-
origin: 'auto.navigation.react.reactrouter',
117-
tags,
118-
metadata: {
119-
source,
215+
attributes: {
216+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
217+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`,
218+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
120219
},
121220
});
122221
}
@@ -164,10 +263,12 @@ function computeRootMatch(pathname: string): Match {
164263
export function withSentryRouting<P extends Record<string, any>, R extends React.ComponentType<P>>(Route: R): R {
165264
const componentDisplayName = (Route as any).displayName || (Route as any).name;
166265

266+
const activeRootSpan = getActiveRootSpan();
267+
167268
const WrappedRoute: React.FC<P> = (props: P) => {
168-
if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) {
169-
activeTransaction.updateName(props.computedMatch.path);
170-
activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
269+
if (activeRootSpan && props && props.computedMatch && props.computedMatch.isExact) {
270+
activeRootSpan.updateName(props.computedMatch.path);
271+
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
171272
}
172273

173274
// @ts-expect-error Setting more specific React Component typing for `R` generic above
@@ -184,3 +285,22 @@ export function withSentryRouting<P extends Record<string, any>, R extends React
184285
return WrappedRoute;
185286
}
186287
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
288+
289+
function getActiveRootSpan(): Span | undefined {
290+
// Legacy behavior for "old" react router instrumentation
291+
if (activeTransaction) {
292+
return activeTransaction;
293+
}
294+
295+
const span = getActiveSpan();
296+
const rootSpan = span ? getRootSpan(span) : undefined;
297+
298+
if (!rootSpan) {
299+
return undefined;
300+
}
301+
302+
const op = spanToJSON(rootSpan).op;
303+
304+
// Only use this root span if it is a pageload or navigation span
305+
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
306+
}

0 commit comments

Comments
 (0)