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' ;
4
16
import hoistNonReactStatics from 'hoist-non-react-statics' ;
5
17
import * as React from 'react' ;
6
18
@@ -23,29 +35,121 @@ export type RouteConfig = {
23
35
routes ?: RouteConfig [ ] ;
24
36
} ;
25
37
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
+ }
27
45
28
46
let activeTransaction : Transaction | undefined ;
29
47
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
+ */
30
131
export function reactRouterV4Instrumentation (
31
132
history : RouterHistory ,
32
133
routes ?: RouteConfig [ ] ,
33
134
matchPath ?: MatchPath ,
34
135
) : ReactRouterInstrumentation {
35
- return createReactRouterInstrumentation ( history , 'react-router-v4 ' , routes , matchPath ) ;
136
+ return createReactRouterInstrumentation ( history , 'reactrouter_v4 ' , routes , matchPath ) ;
36
137
}
37
138
139
+ /**
140
+ * @deprecated Use `browserTracingReactRouterV5()` instead.
141
+ */
38
142
export function reactRouterV5Instrumentation (
39
143
history : RouterHistory ,
40
144
routes ?: RouteConfig [ ] ,
41
145
matchPath ?: MatchPath ,
42
146
) : ReactRouterInstrumentation {
43
- return createReactRouterInstrumentation ( history , 'react-router-v5 ' , routes , matchPath ) ;
147
+ return createReactRouterInstrumentation ( history , 'reactrouter_v5 ' , routes , matchPath ) ;
44
148
}
45
149
46
150
function createReactRouterInstrumentation (
47
151
history : RouterHistory ,
48
- name : string ,
152
+ instrumentationName : string ,
49
153
allRoutes : RouteConfig [ ] = [ ] ,
50
154
matchPath ?: MatchPath ,
51
155
) : ReactRouterInstrumentation {
@@ -83,21 +187,17 @@ function createReactRouterInstrumentation(
83
187
return [ pathname , 'url' ] ;
84
188
}
85
189
86
- const tags = {
87
- 'routing.instrumentation' : name ,
88
- } ;
89
-
90
190
return ( customStartTransaction , startTransactionOnPageLoad = true , startTransactionOnLocationChange = true ) : void => {
91
191
const initPathName = getInitPathName ( ) ;
192
+
92
193
if ( startTransactionOnPageLoad && initPathName ) {
93
194
const [ name , source ] = normalizeTransactionName ( initPathName ) ;
94
195
activeTransaction = customStartTransaction ( {
95
196
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 ,
101
201
} ,
102
202
} ) ;
103
203
}
@@ -112,11 +212,10 @@ function createReactRouterInstrumentation(
112
212
const [ name , source ] = normalizeTransactionName ( location . pathname ) ;
113
213
activeTransaction = customStartTransaction ( {
114
214
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 ,
120
219
} ,
121
220
} ) ;
122
221
}
@@ -164,10 +263,12 @@ function computeRootMatch(pathname: string): Match {
164
263
export function withSentryRouting < P extends Record < string , any > , R extends React . ComponentType < P > > ( Route : R ) : R {
165
264
const componentDisplayName = ( Route as any ) . displayName || ( Route as any ) . name ;
166
265
266
+ const activeRootSpan = getActiveRootSpan ( ) ;
267
+
167
268
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' ) ;
171
272
}
172
273
173
274
// @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
184
285
return WrappedRoute ;
185
286
}
186
287
/* 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