Skip to content

Commit dcd0ea9

Browse files
committed
feat(react): Add browserTracingIntegration for react router v6 & v6.4
feat(react): Add browserTracingIntegrations for router v4 & v5 This adds a new `browserTracingReactRouterV6Integration()` exports deprecating the old routing instrumentation. I opted to leave as much as possible as-is for now, except for streamlining the attributes/tags we use for the instrumentation.
1 parent 028f4d5 commit dcd0ea9

File tree

4 files changed

+1689
-131
lines changed

4 files changed

+1689
-131
lines changed

packages/react/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export { createReduxEnhancer } from './redux';
88
export { reactRouterV3Instrumentation } from './reactrouterv3';
99
export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter';
1010
export {
11+
// eslint-disable-next-line deprecation/deprecation
1112
reactRouterV6Instrumentation,
13+
browserTracingReactRouterV6Integration,
1214
withSentryReactRouterV6Routing,
1315
wrapUseRoutes,
1416
wrapCreateBrowserRouter,

packages/react/src/reactrouterv6.tsx

Lines changed: 131 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
1+
/* eslint-disable max-lines */
12
// Inspired from Donnie McNeal's solution:
23
// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536
34

4-
import { WINDOW } from '@sentry/browser';
5-
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
6-
import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types';
5+
import {
6+
WINDOW,
7+
browserTracingIntegration,
8+
startBrowserTracingNavigationSpan,
9+
startBrowserTracingPageLoadSpan,
10+
} from '@sentry/browser';
11+
import {
12+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
13+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
14+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
15+
getActiveSpan,
16+
getRootSpan,
17+
spanToJSON,
18+
} from '@sentry/core';
19+
import type {
20+
Integration,
21+
Span,
22+
StartSpanOptions,
23+
Transaction,
24+
TransactionContext,
25+
TransactionSource,
26+
} from '@sentry/types';
727
import { getNumberOfUrlSegments, logger } from '@sentry/utils';
828
import hoistNonReactStatics from 'hoist-non-react-statics';
929
import * as React from 'react';
@@ -37,10 +57,77 @@ let _customStartTransaction: (context: TransactionContext) => Transaction | unde
3757
let _startTransactionOnLocationChange: boolean;
3858
let _stripBasename: boolean = false;
3959

40-
const SENTRY_TAGS = {
41-
'routing.instrumentation': 'react-router-v6',
42-
};
60+
interface ReactRouterOptions {
61+
useEffect: UseEffect;
62+
useLocation: UseLocation;
63+
useNavigationType: UseNavigationType;
64+
createRoutesFromChildren: CreateRoutesFromChildren;
65+
matchRoutes: MatchRoutes;
66+
stripBasename?: boolean;
67+
}
68+
69+
/**
70+
* A browser tracing integration that uses React Router v3 to instrument navigations.
71+
* Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
72+
*/
73+
export function browserTracingReactRouterV6Integration(
74+
options: Parameters<typeof browserTracingIntegration>[0] & ReactRouterOptions,
75+
): Integration {
76+
const integration = browserTracingIntegration({
77+
...options,
78+
instrumentPageLoad: false,
79+
instrumentNavigation: false,
80+
});
81+
82+
const {
83+
useEffect,
84+
useLocation,
85+
useNavigationType,
86+
createRoutesFromChildren,
87+
matchRoutes,
88+
stripBasename,
89+
instrumentPageLoad = true,
90+
instrumentNavigation = true,
91+
} = options;
92+
93+
return {
94+
...integration,
95+
afterAllSetup(client) {
96+
integration.afterAllSetup(client);
97+
98+
const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => {
99+
startBrowserTracingNavigationSpan(client, startSpanOptions);
100+
return undefined;
101+
};
102+
103+
const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
104+
if (instrumentPageLoad && initPathName) {
105+
startBrowserTracingPageLoadSpan(client, {
106+
name: initPathName,
107+
attributes: {
108+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
109+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
110+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
111+
},
112+
});
113+
}
43114

115+
_useEffect = useEffect;
116+
_useLocation = useLocation;
117+
_useNavigationType = useNavigationType;
118+
_matchRoutes = matchRoutes;
119+
_createRoutesFromChildren = createRoutesFromChildren;
120+
_stripBasename = stripBasename || false;
121+
122+
_customStartTransaction = startNavigationCallback;
123+
_startTransactionOnLocationChange = instrumentNavigation;
124+
},
125+
};
126+
}
127+
128+
/**
129+
* @deprecated Use `browserTracingReactRouterV6Integration()` instead.
130+
*/
44131
export function reactRouterV6Instrumentation(
45132
useEffect: UseEffect,
46133
useLocation: UseLocation,
@@ -58,11 +145,10 @@ export function reactRouterV6Instrumentation(
58145
if (startTransactionOnPageLoad && initPathName) {
59146
activeTransaction = customStartTransaction({
60147
name: initPathName,
61-
op: 'pageload',
62-
origin: 'auto.pageload.react.reactrouterv6',
63-
tags: SENTRY_TAGS,
64-
metadata: {
65-
source: 'url',
148+
attributes: {
149+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
150+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
151+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
66152
},
67153
});
68154
}
@@ -155,6 +241,7 @@ function getNormalizedName(
155241
}
156242

157243
function updatePageloadTransaction(
244+
activeRootSpan: Span | undefined,
158245
location: Location,
159246
routes: RouteObject[],
160247
matches?: AgnosticDataRouteMatch,
@@ -164,10 +251,10 @@ function updatePageloadTransaction(
164251
? matches
165252
: (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]);
166253

167-
if (activeTransaction && branches) {
254+
if (activeRootSpan && branches) {
168255
const [name, source] = getNormalizedName(routes, location, branches, basename);
169-
activeTransaction.updateName(name);
170-
activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
256+
activeRootSpan.updateName(name);
257+
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
171258
}
172259
}
173260

@@ -188,11 +275,10 @@ function handleNavigation(
188275
const [name, source] = getNormalizedName(routes, location, branches, basename);
189276
activeTransaction = _customStartTransaction({
190277
name,
191-
op: 'navigation',
192-
origin: 'auto.navigation.react.reactrouterv6',
193-
tags: SENTRY_TAGS,
194-
metadata: {
195-
source,
278+
attributes: {
279+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
280+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
281+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
196282
},
197283
});
198284
}
@@ -227,7 +313,7 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R
227313
const routes = _createRoutesFromChildren(props.children) as RouteObject[];
228314

229315
if (isMountRenderPass) {
230-
updatePageloadTransaction(location, routes);
316+
updatePageloadTransaction(getActiveRootSpan(), location, routes);
231317
isMountRenderPass = false;
232318
} else {
233319
handleNavigation(location, routes, navigationType);
@@ -285,7 +371,7 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes {
285371
typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam;
286372

287373
if (isMountRenderPass) {
288-
updatePageloadTransaction(normalizedLocation, routes);
374+
updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes);
289375
isMountRenderPass = false;
290376
} else {
291377
handleNavigation(normalizedLocation, routes, navigationType);
@@ -312,25 +398,41 @@ export function wrapCreateBrowserRouter<
312398
const router = createRouterFunction(routes, opts);
313399
const basename = opts && opts.basename;
314400

401+
const activeRootSpan = getActiveRootSpan();
402+
315403
// The initial load ends when `createBrowserRouter` is called.
316404
// This is the earliest convenient time to update the transaction name.
317405
// Callbacks to `router.subscribe` are not called for the initial load.
318-
if (router.state.historyAction === 'POP' && activeTransaction) {
319-
updatePageloadTransaction(router.state.location, routes, undefined, basename);
406+
if (router.state.historyAction === 'POP' && activeRootSpan) {
407+
updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename);
320408
}
321409

322410
router.subscribe((state: RouterState) => {
323411
const location = state.location;
324-
325-
if (
326-
_startTransactionOnLocationChange &&
327-
(state.historyAction === 'PUSH' || state.historyAction === 'POP') &&
328-
activeTransaction
329-
) {
412+
if (_startTransactionOnLocationChange && (state.historyAction === 'PUSH' || state.historyAction === 'POP')) {
330413
handleNavigation(location, routes, state.historyAction, undefined, basename);
331414
}
332415
});
333416

334417
return router;
335418
};
336419
}
420+
421+
function getActiveRootSpan(): Span | undefined {
422+
// Legacy behavior for "old" react router instrumentation
423+
if (activeTransaction) {
424+
return activeTransaction;
425+
}
426+
427+
const span = getActiveSpan();
428+
const rootSpan = span ? getRootSpan(span) : undefined;
429+
430+
if (!rootSpan) {
431+
return undefined;
432+
}
433+
434+
const op = spanToJSON(rootSpan).op;
435+
436+
// Only use this root span if it is a pageload or navigation span
437+
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
438+
}

0 commit comments

Comments
 (0)