1
|
|
|
import { browserTracingIntegration, WINDOW, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan } from '@sentry/browser'; |
2
|
|
|
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, debug, spanToJSON, getActiveSpan, getRootSpan, getCurrentScope, getClient, addNonEnumerableProperty } from '@sentry/core'; |
3
|
|
|
import * as React from 'react'; |
4
|
|
|
import { DEBUG_BUILD } from '../debug-build.js'; |
5
|
|
|
import { hoistNonReactStatics } from '../hoist-non-react-statics.js'; |
6
|
|
|
import { checkRouteForAsyncHandler } from './lazy-routes.js'; |
7
|
|
|
import { locationIsInsideDescendantRoute, prefixWithSlash, rebuildRoutePathFromAllRoutes, getNormalizedName, resolveRouteNameAndSource, isLikelyLazyRouteContext, initializeRouterUtils } from './utils.js'; |
8
|
|
|
|
9
|
|
|
/* eslint-disable max-lines */ |
10
|
|
|
// Inspired from Donnie McNeal's solution: |
11
|
|
|
// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
let _useEffect; |
15
|
|
|
let _useLocation; |
16
|
|
|
let _useNavigationType; |
17
|
|
|
let _createRoutesFromChildren; |
18
|
|
|
let _matchRoutes; |
19
|
|
|
let _enableAsyncRouteHandlers = false; |
20
|
|
|
|
21
|
|
|
const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet(); |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Adds resolved routes as children to the parent route. |
25
|
|
|
* Prevents duplicate routes by checking if they already exist. |
26
|
|
|
*/ |
27
|
|
|
function addResolvedRoutesToParent(resolvedRoutes, parentRoute) { |
28
|
|
|
const existingChildren = parentRoute.children || []; |
29
|
|
|
|
30
|
|
|
const newRoutes = resolvedRoutes.filter( |
31
|
|
|
newRoute => |
32
|
|
|
!existingChildren.some( |
33
|
|
|
existing => |
34
|
|
|
existing === newRoute || |
35
|
|
|
(newRoute.path && existing.path === newRoute.path) || |
36
|
|
|
(newRoute.id && existing.id === newRoute.id), |
37
|
|
|
), |
38
|
|
|
); |
39
|
|
|
|
40
|
|
|
if (newRoutes.length > 0) { |
41
|
|
|
parentRoute.children = [...existingChildren, ...newRoutes]; |
42
|
|
|
} |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
// Keeping as a global variable for cross-usage in multiple functions |
46
|
|
|
const allRoutes = new Set(); |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers. |
50
|
|
|
*/ |
51
|
|
|
function processResolvedRoutes( |
52
|
|
|
resolvedRoutes, |
53
|
|
|
parentRoute, |
54
|
|
|
currentLocation = null, |
55
|
|
|
) { |
56
|
|
|
resolvedRoutes.forEach(child => { |
57
|
|
|
allRoutes.add(child); |
58
|
|
|
// Only check for async handlers if the feature is enabled |
59
|
|
|
if (_enableAsyncRouteHandlers) { |
60
|
|
|
checkRouteForAsyncHandler(child, processResolvedRoutes); |
61
|
|
|
} |
62
|
|
|
}); |
63
|
|
|
|
64
|
|
|
if (parentRoute) { |
65
|
|
|
// If a parent route is provided, add the resolved routes as children to the parent route |
66
|
|
|
addResolvedRoutesToParent(resolvedRoutes, parentRoute); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
// After processing lazy routes, check if we need to update an active transaction |
70
|
|
|
const activeRootSpan = getActiveRootSpan(); |
71
|
|
|
if (activeRootSpan) { |
72
|
|
|
const spanOp = spanToJSON(activeRootSpan).op; |
73
|
|
|
|
74
|
|
|
// Try to use the provided location first, then fall back to global window location if needed |
75
|
|
|
let location = currentLocation; |
76
|
|
|
if (!location) { |
77
|
|
|
if (typeof WINDOW !== 'undefined') { |
78
|
|
|
const globalLocation = WINDOW.location; |
79
|
|
|
if (globalLocation) { |
80
|
|
|
location = { pathname: globalLocation.pathname }; |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
if (location) { |
86
|
|
|
if (spanOp === 'pageload') { |
87
|
|
|
// Re-run the pageload transaction update with the newly loaded routes |
88
|
|
|
updatePageloadTransaction({ |
89
|
|
|
activeRootSpan, |
90
|
|
|
location: { pathname: location.pathname }, |
91
|
|
|
routes: Array.from(allRoutes), |
92
|
|
|
allRoutes: Array.from(allRoutes), |
93
|
|
|
}); |
94
|
|
|
} else if (spanOp === 'navigation') { |
95
|
|
|
// For navigation spans, update the name with the newly loaded routes |
96
|
|
|
updateNavigationSpan(activeRootSpan, location, Array.from(allRoutes), false, _matchRoutes); |
97
|
|
|
} |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Updates a navigation span with the correct route name after lazy routes have been loaded. |
104
|
|
|
*/ |
105
|
|
|
function updateNavigationSpan( |
106
|
|
|
activeRootSpan, |
107
|
|
|
location, |
108
|
|
|
allRoutes, |
109
|
|
|
forceUpdate = false, |
110
|
|
|
matchRoutes, |
111
|
|
|
) { |
112
|
|
|
// Check if this span has already been named to avoid multiple updates |
113
|
|
|
// But allow updates if this is a forced update (e.g., when lazy routes are loaded) |
114
|
|
|
const hasBeenNamed = |
115
|
|
|
!forceUpdate && |
116
|
|
|
( |
117
|
|
|
activeRootSpan |
118
|
|
|
|
119
|
|
|
)?.__sentry_navigation_name_set__; |
120
|
|
|
|
121
|
|
|
if (!hasBeenNamed) { |
122
|
|
|
// Get fresh branches for the current location with all loaded routes |
123
|
|
|
const currentBranches = matchRoutes(allRoutes, location); |
124
|
|
|
const [name, source] = resolveRouteNameAndSource( |
125
|
|
|
location, |
126
|
|
|
allRoutes, |
127
|
|
|
allRoutes, |
128
|
|
|
(currentBranches ) || [], |
129
|
|
|
'', |
130
|
|
|
); |
131
|
|
|
|
132
|
|
|
// Only update if we have a valid name and the span hasn't finished |
133
|
|
|
const spanJson = spanToJSON(activeRootSpan); |
134
|
|
|
if (name && !spanJson.timestamp) { |
135
|
|
|
activeRootSpan.updateName(name); |
136
|
|
|
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); |
137
|
|
|
|
138
|
|
|
// Mark this span as having its name set to prevent future updates |
139
|
|
|
addNonEnumerableProperty( |
140
|
|
|
activeRootSpan , |
141
|
|
|
'__sentry_navigation_name_set__', |
142
|
|
|
true, |
143
|
|
|
); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions. |
150
|
|
|
*/ |
151
|
|
|
function createV6CompatibleWrapCreateBrowserRouter |
152
|
|
|
|
153
|
|
|
( |
154
|
|
|
createRouterFunction, |
155
|
|
|
version, |
156
|
|
|
) { |
157
|
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { |
158
|
|
|
DEBUG_BUILD && |
159
|
|
|
debug.warn( |
160
|
|
|
`reactRouterV${version}Instrumentation was unable to wrap the \`createRouter\` function because of one or more missing parameters.`, |
161
|
|
|
); |
162
|
|
|
|
163
|
|
|
return createRouterFunction; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return function (routes, opts) { |
167
|
|
|
addRoutesToAllRoutes(routes); |
168
|
|
|
|
169
|
|
|
// Check for async handlers that might contain sub-route declarations (only if enabled) |
170
|
|
|
if (_enableAsyncRouteHandlers) { |
171
|
|
|
for (const route of routes) { |
172
|
|
|
checkRouteForAsyncHandler(route, processResolvedRoutes); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded |
177
|
|
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts); |
178
|
|
|
|
179
|
|
|
const router = createRouterFunction(routes, wrappedOpts); |
180
|
|
|
const basename = opts?.basename; |
181
|
|
|
|
182
|
|
|
const activeRootSpan = getActiveRootSpan(); |
183
|
|
|
|
184
|
|
|
// The initial load ends when `createBrowserRouter` is called. |
185
|
|
|
// This is the earliest convenient time to update the transaction name. |
186
|
|
|
// Callbacks to `router.subscribe` are not called for the initial load. |
187
|
|
|
if (router.state.historyAction === 'POP' && activeRootSpan) { |
188
|
|
|
updatePageloadTransaction({ |
189
|
|
|
activeRootSpan, |
190
|
|
|
location: router.state.location, |
191
|
|
|
routes, |
192
|
|
|
basename, |
193
|
|
|
allRoutes: Array.from(allRoutes), |
194
|
|
|
}); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
router.subscribe((state) => { |
198
|
|
|
if (state.historyAction === 'PUSH' || state.historyAction === 'POP') { |
199
|
|
|
// Wait for the next render if loading an unsettled route |
200
|
|
|
if (state.navigation.state !== 'idle') { |
201
|
|
|
requestAnimationFrame(() => { |
202
|
|
|
handleNavigation({ |
203
|
|
|
location: state.location, |
204
|
|
|
routes, |
205
|
|
|
navigationType: state.historyAction, |
206
|
|
|
version, |
207
|
|
|
basename, |
208
|
|
|
allRoutes: Array.from(allRoutes), |
209
|
|
|
}); |
210
|
|
|
}); |
211
|
|
|
} else { |
212
|
|
|
handleNavigation({ |
213
|
|
|
location: state.location, |
214
|
|
|
routes, |
215
|
|
|
navigationType: state.historyAction, |
216
|
|
|
version, |
217
|
|
|
basename, |
218
|
|
|
allRoutes: Array.from(allRoutes), |
219
|
|
|
}); |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
}); |
223
|
|
|
|
224
|
|
|
return router; |
225
|
|
|
}; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Creates a wrapCreateMemoryRouter function that can be used with all React Router v6 compatible versions. |
230
|
|
|
*/ |
231
|
|
|
function createV6CompatibleWrapCreateMemoryRouter |
232
|
|
|
|
233
|
|
|
( |
234
|
|
|
createRouterFunction, |
235
|
|
|
version, |
236
|
|
|
) { |
237
|
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { |
238
|
|
|
DEBUG_BUILD && |
239
|
|
|
debug.warn( |
240
|
|
|
`reactRouterV${version}Instrumentation was unable to wrap the \`createMemoryRouter\` function because of one or more missing parameters.`, |
241
|
|
|
); |
242
|
|
|
|
243
|
|
|
return createRouterFunction; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return function ( |
247
|
|
|
routes, |
248
|
|
|
opts |
249
|
|
|
|
250
|
|
|
, |
251
|
|
|
) { |
252
|
|
|
addRoutesToAllRoutes(routes); |
253
|
|
|
|
254
|
|
|
// Check for async handlers that might contain sub-route declarations (only if enabled) |
255
|
|
|
if (_enableAsyncRouteHandlers) { |
256
|
|
|
for (const route of routes) { |
257
|
|
|
checkRouteForAsyncHandler(route, processResolvedRoutes); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded |
262
|
|
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true); |
263
|
|
|
|
264
|
|
|
const router = createRouterFunction(routes, wrappedOpts); |
265
|
|
|
const basename = opts?.basename; |
266
|
|
|
|
267
|
|
|
const activeRootSpan = getActiveRootSpan(); |
268
|
|
|
let initialEntry = undefined; |
269
|
|
|
|
270
|
|
|
const initialEntries = opts?.initialEntries; |
271
|
|
|
const initialIndex = opts?.initialIndex; |
272
|
|
|
|
273
|
|
|
const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; |
274
|
|
|
const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; |
275
|
|
|
|
276
|
|
|
initialEntry = hasOnlyOneInitialEntry |
277
|
|
|
? initialEntries[0] |
278
|
|
|
: hasIndexedEntry |
279
|
|
|
? initialEntries[initialIndex] |
280
|
|
|
: undefined; |
281
|
|
|
|
282
|
|
|
const location = initialEntry |
283
|
|
|
? typeof initialEntry === 'string' |
284
|
|
|
? { pathname: initialEntry } |
285
|
|
|
: initialEntry |
286
|
|
|
: router.state.location; |
287
|
|
|
|
288
|
|
|
if (router.state.historyAction === 'POP' && activeRootSpan) { |
289
|
|
|
updatePageloadTransaction({ |
290
|
|
|
activeRootSpan, |
291
|
|
|
location, |
292
|
|
|
routes, |
293
|
|
|
basename, |
294
|
|
|
allRoutes: Array.from(allRoutes), |
295
|
|
|
}); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
router.subscribe((state) => { |
299
|
|
|
const location = state.location; |
300
|
|
|
if (state.historyAction === 'PUSH' || state.historyAction === 'POP') { |
301
|
|
|
handleNavigation({ |
302
|
|
|
location, |
303
|
|
|
routes, |
304
|
|
|
navigationType: state.historyAction, |
305
|
|
|
version, |
306
|
|
|
basename, |
307
|
|
|
allRoutes: Array.from(allRoutes), |
308
|
|
|
}); |
309
|
|
|
} |
310
|
|
|
}); |
311
|
|
|
|
312
|
|
|
return router; |
313
|
|
|
}; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Creates a browser tracing integration that can be used with all React Router v6 compatible versions. |
318
|
|
|
*/ |
319
|
|
|
function createReactRouterV6CompatibleTracingIntegration( |
320
|
|
|
options, |
321
|
|
|
version, |
322
|
|
|
) { |
323
|
|
|
const integration = browserTracingIntegration({ |
324
|
|
|
...options, |
325
|
|
|
instrumentPageLoad: false, |
326
|
|
|
instrumentNavigation: false, |
327
|
|
|
}); |
328
|
|
|
|
329
|
|
|
const { |
330
|
|
|
useEffect, |
331
|
|
|
useLocation, |
332
|
|
|
useNavigationType, |
333
|
|
|
createRoutesFromChildren, |
334
|
|
|
matchRoutes, |
335
|
|
|
stripBasename, |
336
|
|
|
enableAsyncRouteHandlers = false, |
337
|
|
|
instrumentPageLoad = true, |
338
|
|
|
instrumentNavigation = true, |
339
|
|
|
} = options; |
340
|
|
|
|
341
|
|
|
return { |
342
|
|
|
...integration, |
343
|
|
|
setup(client) { |
344
|
|
|
integration.setup(client); |
345
|
|
|
|
346
|
|
|
_useEffect = useEffect; |
347
|
|
|
_useLocation = useLocation; |
348
|
|
|
_useNavigationType = useNavigationType; |
349
|
|
|
_matchRoutes = matchRoutes; |
350
|
|
|
_createRoutesFromChildren = createRoutesFromChildren; |
351
|
|
|
_enableAsyncRouteHandlers = enableAsyncRouteHandlers; |
352
|
|
|
|
353
|
|
|
// Initialize the router utils with the required dependencies |
354
|
|
|
initializeRouterUtils(matchRoutes, stripBasename || false); |
355
|
|
|
}, |
356
|
|
|
afterAllSetup(client) { |
357
|
|
|
integration.afterAllSetup(client); |
358
|
|
|
|
359
|
|
|
const initPathName = WINDOW.location?.pathname; |
360
|
|
|
if (instrumentPageLoad && initPathName) { |
361
|
|
|
startBrowserTracingPageLoadSpan(client, { |
362
|
|
|
name: initPathName, |
363
|
|
|
attributes: { |
364
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', |
365
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', |
366
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.reactrouter_v${version}`, |
367
|
|
|
}, |
368
|
|
|
}); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
if (instrumentNavigation) { |
372
|
|
|
CLIENTS_WITH_INSTRUMENT_NAVIGATION.add(client); |
373
|
|
|
} |
374
|
|
|
}, |
375
|
|
|
}; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
function createV6CompatibleWrapUseRoutes(origUseRoutes, version) { |
379
|
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { |
380
|
|
|
DEBUG_BUILD && |
381
|
|
|
debug.warn( |
382
|
|
|
'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.', |
383
|
|
|
); |
384
|
|
|
|
385
|
|
|
return origUseRoutes; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
const SentryRoutes |
389
|
|
|
|
390
|
|
|
= (props) => { |
391
|
|
|
const isMountRenderPass = React.useRef(true); |
392
|
|
|
const { routes, locationArg } = props; |
393
|
|
|
|
394
|
|
|
const Routes = origUseRoutes(routes, locationArg); |
395
|
|
|
|
396
|
|
|
const location = _useLocation(); |
397
|
|
|
const navigationType = _useNavigationType(); |
398
|
|
|
|
399
|
|
|
// A value with stable identity to either pick `locationArg` if available or `location` if not |
400
|
|
|
const stableLocationParam = |
401
|
|
|
typeof locationArg === 'string' || locationArg?.pathname ? (locationArg ) : location; |
402
|
|
|
|
403
|
|
|
_useEffect(() => { |
404
|
|
|
const normalizedLocation = |
405
|
|
|
typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; |
406
|
|
|
|
407
|
|
|
if (isMountRenderPass.current) { |
408
|
|
|
addRoutesToAllRoutes(routes); |
409
|
|
|
|
410
|
|
|
updatePageloadTransaction({ |
411
|
|
|
activeRootSpan: getActiveRootSpan(), |
412
|
|
|
location: normalizedLocation, |
413
|
|
|
routes, |
414
|
|
|
allRoutes: Array.from(allRoutes), |
415
|
|
|
}); |
416
|
|
|
isMountRenderPass.current = false; |
417
|
|
|
} else { |
418
|
|
|
handleNavigation({ |
419
|
|
|
location: normalizedLocation, |
420
|
|
|
routes, |
421
|
|
|
navigationType, |
422
|
|
|
version, |
423
|
|
|
allRoutes: Array.from(allRoutes), |
424
|
|
|
}); |
425
|
|
|
} |
426
|
|
|
}, [navigationType, stableLocationParam]); |
427
|
|
|
|
428
|
|
|
return Routes; |
429
|
|
|
}; |
430
|
|
|
|
431
|
|
|
// eslint-disable-next-line react/display-name |
432
|
|
|
return (routes, locationArg) => { |
433
|
|
|
return React.createElement(SentryRoutes, { routes: routes, locationArg: locationArg,} ); |
434
|
|
|
}; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
function wrapPatchRoutesOnNavigation( |
438
|
|
|
opts, |
439
|
|
|
isMemoryRouter = false, |
440
|
|
|
) { |
441
|
|
|
if (!opts || !('patchRoutesOnNavigation' in opts) || typeof opts.patchRoutesOnNavigation !== 'function') { |
442
|
|
|
return opts || {}; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
const originalPatchRoutes = opts.patchRoutesOnNavigation; |
446
|
|
|
return { |
447
|
|
|
...opts, |
448
|
|
|
patchRoutesOnNavigation: async (args) => { |
449
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access |
450
|
|
|
const targetPath = (args )?.path; |
451
|
|
|
|
452
|
|
|
// For browser router, wrap the patch function to update span during patching |
453
|
|
|
if (!isMemoryRouter) { |
454
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access |
455
|
|
|
const originalPatch = (args )?.patch; |
456
|
|
|
if (originalPatch) { |
457
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access |
458
|
|
|
(args ).patch = (routeId, children) => { |
459
|
|
|
addRoutesToAllRoutes(children); |
460
|
|
|
const activeRootSpan = getActiveRootSpan(); |
461
|
|
|
if (activeRootSpan && (spanToJSON(activeRootSpan) ).op === 'navigation') { |
462
|
|
|
updateNavigationSpan( |
463
|
|
|
activeRootSpan, |
464
|
|
|
{ |
465
|
|
|
pathname: targetPath, |
466
|
|
|
search: '', |
467
|
|
|
hash: '', |
468
|
|
|
state: null, |
469
|
|
|
key: 'default', |
470
|
|
|
}, |
471
|
|
|
Array.from(allRoutes), |
472
|
|
|
true, // forceUpdate = true since we're loading lazy routes |
473
|
|
|
_matchRoutes, |
474
|
|
|
); |
475
|
|
|
} |
476
|
|
|
return originalPatch(routeId, children); |
477
|
|
|
}; |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
const result = await originalPatchRoutes(args); |
482
|
|
|
|
483
|
|
|
// Update navigation span after routes are patched |
484
|
|
|
const activeRootSpan = getActiveRootSpan(); |
485
|
|
|
if (activeRootSpan && (spanToJSON(activeRootSpan) ).op === 'navigation') { |
486
|
|
|
// For memory routers, we should not access window.location; use targetPath only |
487
|
|
|
const pathname = isMemoryRouter ? targetPath : targetPath || WINDOW.location?.pathname; |
488
|
|
|
if (pathname) { |
489
|
|
|
updateNavigationSpan( |
490
|
|
|
activeRootSpan, |
491
|
|
|
{ |
492
|
|
|
pathname, |
493
|
|
|
search: '', |
494
|
|
|
hash: '', |
495
|
|
|
state: null, |
496
|
|
|
key: 'default', |
497
|
|
|
}, |
498
|
|
|
Array.from(allRoutes), |
499
|
|
|
false, // forceUpdate = false since this is after lazy routes are loaded |
500
|
|
|
_matchRoutes, |
501
|
|
|
); |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
return result; |
506
|
|
|
}, |
507
|
|
|
}; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
function handleNavigation(opts |
511
|
|
|
|
512
|
|
|
) { |
513
|
|
|
const { location, routes, navigationType, version, matches, basename, allRoutes } = opts; |
514
|
|
|
const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename); |
515
|
|
|
|
516
|
|
|
const client = getClient(); |
517
|
|
|
if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) { |
518
|
|
|
return; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) { |
522
|
|
|
const [name, source] = resolveRouteNameAndSource( |
523
|
|
|
location, |
524
|
|
|
routes, |
525
|
|
|
allRoutes || routes, |
526
|
|
|
branches , |
527
|
|
|
basename, |
528
|
|
|
); |
529
|
|
|
|
530
|
|
|
// Check if this might be a lazy route context |
531
|
|
|
const isLazyRouteContext = isLikelyLazyRouteContext(allRoutes || routes, location); |
532
|
|
|
|
533
|
|
|
const activeSpan = getActiveSpan(); |
534
|
|
|
const spanJson = activeSpan && spanToJSON(activeSpan); |
535
|
|
|
const isAlreadyInNavigationSpan = spanJson?.op === 'navigation'; |
536
|
|
|
|
537
|
|
|
// Cross usage can result in multiple navigation spans being created without this check |
538
|
|
|
if (isAlreadyInNavigationSpan && activeSpan && spanJson) { |
539
|
|
|
handleExistingNavigationSpan(activeSpan, spanJson, name, source, isLazyRouteContext); |
540
|
|
|
} else { |
541
|
|
|
createNewNavigationSpan(client, name, source, version, isLazyRouteContext); |
542
|
|
|
} |
543
|
|
|
} |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
function addRoutesToAllRoutes(routes) { |
547
|
|
|
routes.forEach(route => { |
548
|
|
|
const extractedChildRoutes = getChildRoutesRecursively(route); |
549
|
|
|
|
550
|
|
|
extractedChildRoutes.forEach(r => { |
551
|
|
|
allRoutes.add(r); |
552
|
|
|
}); |
553
|
|
|
}); |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
function getChildRoutesRecursively(route, allRoutes = new Set()) { |
557
|
|
|
if (!allRoutes.has(route)) { |
558
|
|
|
allRoutes.add(route); |
559
|
|
|
|
560
|
|
|
if (route.children && !route.index) { |
561
|
|
|
route.children.forEach(child => { |
562
|
|
|
const childRoutes = getChildRoutesRecursively(child, allRoutes); |
563
|
|
|
|
564
|
|
|
childRoutes.forEach(r => { |
565
|
|
|
allRoutes.add(r); |
566
|
|
|
}); |
567
|
|
|
}); |
568
|
|
|
} |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
return allRoutes; |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
function updatePageloadTransaction({ |
575
|
|
|
activeRootSpan, |
576
|
|
|
location, |
577
|
|
|
routes, |
578
|
|
|
matches, |
579
|
|
|
basename, |
580
|
|
|
allRoutes, |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
) { |
584
|
|
|
const branches = Array.isArray(matches) |
585
|
|
|
? matches |
586
|
|
|
: (_matchRoutes(allRoutes || routes, location, basename) ); |
587
|
|
|
|
588
|
|
|
if (branches) { |
589
|
|
|
let name, |
590
|
|
|
source = 'url'; |
591
|
|
|
|
592
|
|
|
const isInDescendantRoute = locationIsInsideDescendantRoute(location, allRoutes || routes); |
593
|
|
|
|
594
|
|
|
if (isInDescendantRoute) { |
595
|
|
|
name = prefixWithSlash(rebuildRoutePathFromAllRoutes(allRoutes || routes, location)); |
596
|
|
|
source = 'route'; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
if (!isInDescendantRoute || !name) { |
600
|
|
|
[name, source] = getNormalizedName(routes, location, branches, basename); |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
getCurrentScope().setTransactionName(name || '/'); |
604
|
|
|
|
605
|
|
|
if (activeRootSpan) { |
606
|
|
|
activeRootSpan.updateName(name); |
607
|
|
|
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); |
608
|
|
|
} |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
613
|
|
|
function createV6CompatibleWithSentryReactRouterRouting( |
614
|
|
|
Routes, |
615
|
|
|
version, |
616
|
|
|
) { |
617
|
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_createRoutesFromChildren || !_matchRoutes) { |
618
|
|
|
DEBUG_BUILD && |
619
|
|
|
debug.warn(`reactRouterV6Instrumentation was unable to wrap Routes because of one or more missing parameters. |
620
|
|
|
useEffect: ${_useEffect}. useLocation: ${_useLocation}. useNavigationType: ${_useNavigationType}. |
621
|
|
|
createRoutesFromChildren: ${_createRoutesFromChildren}. matchRoutes: ${_matchRoutes}.`); |
622
|
|
|
|
623
|
|
|
return Routes; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
const SentryRoutes = (props) => { |
627
|
|
|
const isMountRenderPass = React.useRef(true); |
628
|
|
|
|
629
|
|
|
const location = _useLocation(); |
630
|
|
|
const navigationType = _useNavigationType(); |
631
|
|
|
|
632
|
|
|
_useEffect( |
633
|
|
|
() => { |
634
|
|
|
const routes = _createRoutesFromChildren(props.children) ; |
635
|
|
|
|
636
|
|
|
if (isMountRenderPass.current) { |
637
|
|
|
addRoutesToAllRoutes(routes); |
638
|
|
|
|
639
|
|
|
updatePageloadTransaction({ |
640
|
|
|
activeRootSpan: getActiveRootSpan(), |
641
|
|
|
location, |
642
|
|
|
routes, |
643
|
|
|
allRoutes: Array.from(allRoutes), |
644
|
|
|
}); |
645
|
|
|
isMountRenderPass.current = false; |
646
|
|
|
} else { |
647
|
|
|
handleNavigation({ |
648
|
|
|
location, |
649
|
|
|
routes, |
650
|
|
|
navigationType, |
651
|
|
|
version, |
652
|
|
|
allRoutes: Array.from(allRoutes), |
653
|
|
|
}); |
654
|
|
|
} |
655
|
|
|
}, |
656
|
|
|
// `props.children` is purposely not included in the dependency array, because we do not want to re-run this effect |
657
|
|
|
// when the children change. We only want to start transactions when the location or navigation type change. |
658
|
|
|
[location, navigationType], |
659
|
|
|
); |
660
|
|
|
|
661
|
|
|
// @ts-expect-error Setting more specific React Component typing for `R` generic above |
662
|
|
|
// will break advanced type inference done by react router params |
663
|
|
|
return React.createElement(Routes, { ...props,} ); |
664
|
|
|
}; |
665
|
|
|
|
666
|
|
|
hoistNonReactStatics(SentryRoutes, Routes); |
667
|
|
|
|
668
|
|
|
// @ts-expect-error Setting more specific React Component typing for `R` generic above |
669
|
|
|
// will break advanced type inference done by react router params |
670
|
|
|
return SentryRoutes; |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
function getActiveRootSpan() { |
674
|
|
|
const span = getActiveSpan(); |
675
|
|
|
const rootSpan = span ? getRootSpan(span) : undefined; |
676
|
|
|
|
677
|
|
|
if (!rootSpan) { |
678
|
|
|
return undefined; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
const op = spanToJSON(rootSpan).op; |
682
|
|
|
|
683
|
|
|
// Only use this root span if it is a pageload or navigation span |
684
|
|
|
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Handles updating an existing navigation span |
689
|
|
|
*/ |
690
|
|
|
function handleExistingNavigationSpan( |
691
|
|
|
activeSpan, |
692
|
|
|
spanJson, |
693
|
|
|
name, |
694
|
|
|
source, |
695
|
|
|
isLikelyLazyRoute, |
696
|
|
|
) { |
697
|
|
|
// Check if we've already set the name for this span using a custom property |
698
|
|
|
const hasBeenNamed = ( |
699
|
|
|
activeSpan |
700
|
|
|
|
701
|
|
|
)?.__sentry_navigation_name_set__; |
702
|
|
|
|
703
|
|
|
if (!hasBeenNamed) { |
704
|
|
|
// This is the first time we're setting the name for this span |
705
|
|
|
if (!spanJson.timestamp) { |
706
|
|
|
activeSpan?.updateName(name); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
// For lazy routes, don't mark as named yet so it can be updated later |
710
|
|
|
if (!isLikelyLazyRoute) { |
711
|
|
|
addNonEnumerableProperty( |
712
|
|
|
activeSpan , |
713
|
|
|
'__sentry_navigation_name_set__', |
714
|
|
|
true, |
715
|
|
|
); |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
// Always set the source attribute to keep it consistent with the current route |
720
|
|
|
activeSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
/** |
724
|
|
|
* Creates a new navigation span |
725
|
|
|
*/ |
726
|
|
|
function createNewNavigationSpan( |
727
|
|
|
client, |
728
|
|
|
name, |
729
|
|
|
source, |
730
|
|
|
version, |
731
|
|
|
isLikelyLazyRoute, |
732
|
|
|
) { |
733
|
|
|
const newSpan = startBrowserTracingNavigationSpan(client, { |
734
|
|
|
name, |
735
|
|
|
attributes: { |
736
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, |
737
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', |
738
|
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`, |
739
|
|
|
}, |
740
|
|
|
}); |
741
|
|
|
|
742
|
|
|
// For lazy routes, don't mark as named yet so it can be updated later when the route loads |
743
|
|
|
if (!isLikelyLazyRoute && newSpan) { |
744
|
|
|
addNonEnumerableProperty( |
745
|
|
|
newSpan , |
746
|
|
|
'__sentry_navigation_name_set__', |
747
|
|
|
true, |
748
|
|
|
); |
749
|
|
|
} |
750
|
|
|
} |
751
|
|
|
|
752
|
|
|
export { addResolvedRoutesToParent, createNewNavigationSpan, createReactRouterV6CompatibleTracingIntegration, createV6CompatibleWithSentryReactRouterRouting, createV6CompatibleWrapCreateBrowserRouter, createV6CompatibleWrapCreateMemoryRouter, createV6CompatibleWrapUseRoutes, handleExistingNavigationSpan, handleNavigation, processResolvedRoutes, updateNavigationSpan }; |
753
|
|
|
//# sourceMappingURL=instrumentation.js.map |
754
|
|
|
|