@sentry/react 10.26.0 → 10.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/reactrouter-compat-utils/instrumentation.js +516 -288
- package/build/cjs/reactrouter-compat-utils/instrumentation.js.map +1 -1
- package/build/cjs/reactrouter-compat-utils/utils.js +6 -0
- package/build/cjs/reactrouter-compat-utils/utils.js.map +1 -1
- package/build/esm/package.json +1 -1
- package/build/esm/reactrouter-compat-utils/instrumentation.js +516 -290
- package/build/esm/reactrouter-compat-utils/instrumentation.js.map +1 -1
- package/build/esm/reactrouter-compat-utils/utils.js +6 -1
- package/build/esm/reactrouter-compat-utils/utils.js.map +1 -1
- package/build/types/reactrouter-compat-utils/index.d.ts +1 -1
- package/build/types/reactrouter-compat-utils/index.d.ts.map +1 -1
- package/build/types/reactrouter-compat-utils/instrumentation.d.ts +37 -2
- package/build/types/reactrouter-compat-utils/instrumentation.d.ts.map +1 -1
- package/build/types/reactrouter-compat-utils/utils.d.ts +2 -0
- package/build/types/reactrouter-compat-utils/utils.d.ts.map +1 -1
- package/build/types-ts3.8/reactrouter-compat-utils/index.d.ts +1 -1
- package/build/types-ts3.8/reactrouter-compat-utils/instrumentation.d.ts +37 -2
- package/build/types-ts3.8/reactrouter-compat-utils/utils.d.ts +2 -0
- package/package.json +3 -5
|
@@ -18,15 +18,111 @@ let _useLocation;
|
|
|
18
18
|
let _useNavigationType;
|
|
19
19
|
let _createRoutesFromChildren;
|
|
20
20
|
let _matchRoutes;
|
|
21
|
+
|
|
21
22
|
let _enableAsyncRouteHandlers = false;
|
|
23
|
+
let _lazyRouteTimeout = 3000;
|
|
22
24
|
|
|
23
25
|
const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet();
|
|
24
26
|
|
|
27
|
+
// Prevents duplicate spans when router.subscribe fires multiple times
|
|
28
|
+
const activeNavigationSpans = new WeakMap
|
|
29
|
+
|
|
30
|
+
();
|
|
31
|
+
|
|
32
|
+
// Exported for testing only
|
|
33
|
+
const allRoutes = new Set();
|
|
34
|
+
|
|
35
|
+
// Tracks lazy route loads to wait before finalizing span names
|
|
36
|
+
const pendingLazyRouteLoads = new WeakMap();
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
39
|
+
* Schedules a callback using requestAnimationFrame when available (browser),
|
|
40
|
+
* or falls back to setTimeout for SSR environments (Node.js, createMemoryRouter tests).
|
|
28
41
|
*/
|
|
29
|
-
|
|
42
|
+
function scheduleCallback(callback) {
|
|
43
|
+
if (browser.WINDOW?.requestAnimationFrame) {
|
|
44
|
+
return browser.WINDOW.requestAnimationFrame(callback);
|
|
45
|
+
}
|
|
46
|
+
return setTimeout(callback, 0) ;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Cancels a scheduled callback, handling both RAF (browser) and timeout (SSR) IDs.
|
|
51
|
+
*/
|
|
52
|
+
function cancelScheduledCallback(id) {
|
|
53
|
+
if (browser.WINDOW?.cancelAnimationFrame) {
|
|
54
|
+
browser.WINDOW.cancelAnimationFrame(id);
|
|
55
|
+
} else {
|
|
56
|
+
clearTimeout(id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Computes location key for duplicate detection. Normalizes undefined/null to empty strings.
|
|
62
|
+
* Exported for testing.
|
|
63
|
+
*/
|
|
64
|
+
function computeLocationKey(location) {
|
|
65
|
+
return `${location.pathname}${location.search || ''}${location.hash || ''}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a route name is parameterized (contains route parameters like :id or wildcards like *)
|
|
70
|
+
* vs a raw URL path.
|
|
71
|
+
*/
|
|
72
|
+
function isParameterizedRoute(routeName) {
|
|
73
|
+
return routeName.includes(':') || routeName.includes('*');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Determines if a navigation should be skipped as a duplicate, and if an existing span should be updated.
|
|
78
|
+
* Exported for testing.
|
|
79
|
+
*
|
|
80
|
+
* @returns An object with:
|
|
81
|
+
* - skip: boolean - Whether to skip creating a new span
|
|
82
|
+
* - shouldUpdate: boolean - Whether to update the existing span name (wildcard upgrade)
|
|
83
|
+
*/
|
|
84
|
+
function shouldSkipNavigation(
|
|
85
|
+
trackedNav
|
|
86
|
+
|
|
87
|
+
,
|
|
88
|
+
locationKey,
|
|
89
|
+
proposedName,
|
|
90
|
+
spanHasEnded,
|
|
91
|
+
) {
|
|
92
|
+
if (!trackedNav) {
|
|
93
|
+
return { skip: false, shouldUpdate: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if this is a duplicate navigation (same location)
|
|
97
|
+
// 1. If it's a placeholder, it's always a duplicate (we're waiting for the real one)
|
|
98
|
+
// 2. If it's a real span, it's a duplicate only if it hasn't ended yet
|
|
99
|
+
const isDuplicate = trackedNav.locationKey === locationKey && (trackedNav.isPlaceholder || !spanHasEnded);
|
|
100
|
+
|
|
101
|
+
if (isDuplicate) {
|
|
102
|
+
// Check if we should update the span name with a better route
|
|
103
|
+
// Allow updates if:
|
|
104
|
+
// 1. Current has wildcard and new doesn't (wildcard → parameterized upgrade)
|
|
105
|
+
// 2. Current is raw path and new is parameterized (raw → parameterized upgrade)
|
|
106
|
+
// 3. New name is different and more specific (longer, indicating nested routes resolved)
|
|
107
|
+
const currentHasWildcard = !!trackedNav.routeName && utils.transactionNameHasWildcard(trackedNav.routeName);
|
|
108
|
+
const proposedHasWildcard = utils.transactionNameHasWildcard(proposedName);
|
|
109
|
+
const currentIsParameterized = !!trackedNav.routeName && isParameterizedRoute(trackedNav.routeName);
|
|
110
|
+
const proposedIsParameterized = isParameterizedRoute(proposedName);
|
|
111
|
+
|
|
112
|
+
const isWildcardUpgrade = currentHasWildcard && !proposedHasWildcard;
|
|
113
|
+
const isRawToParameterized = !currentIsParameterized && proposedIsParameterized;
|
|
114
|
+
const isMoreSpecific =
|
|
115
|
+
proposedName !== trackedNav.routeName &&
|
|
116
|
+
proposedName.length > (trackedNav.routeName?.length || 0) &&
|
|
117
|
+
!proposedHasWildcard;
|
|
118
|
+
|
|
119
|
+
const shouldUpdate = !!(trackedNav.routeName && (isWildcardUpgrade || isRawToParameterized || isMoreSpecific));
|
|
120
|
+
|
|
121
|
+
return { skip: true, shouldUpdate };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { skip: false, shouldUpdate: false };
|
|
125
|
+
}
|
|
30
126
|
|
|
31
127
|
function addResolvedRoutesToParent(resolvedRoutes, parentRoute) {
|
|
32
128
|
const existingChildren = parentRoute.children || [];
|
|
@@ -46,29 +142,23 @@ function addResolvedRoutesToParent(resolvedRoutes, parentRoute) {
|
|
|
46
142
|
}
|
|
47
143
|
}
|
|
48
144
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* which would cause us to create a span for the wrong route.
|
|
58
|
-
*/
|
|
59
|
-
function shouldHandleNavigation(
|
|
60
|
-
state,
|
|
61
|
-
isInitialPageloadComplete,
|
|
62
|
-
) {
|
|
63
|
-
return (
|
|
64
|
-
(state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete)) &&
|
|
65
|
-
state.navigation.state === 'idle'
|
|
66
|
-
);
|
|
67
|
-
}
|
|
145
|
+
/** Registers a pending lazy route load promise for a span. */
|
|
146
|
+
function trackLazyRouteLoad(span, promise) {
|
|
147
|
+
let promises = pendingLazyRouteLoads.get(span);
|
|
148
|
+
if (!promises) {
|
|
149
|
+
promises = new Set();
|
|
150
|
+
pendingLazyRouteLoads.set(span, promises);
|
|
151
|
+
}
|
|
152
|
+
promises.add(promise);
|
|
68
153
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
const
|
|
154
|
+
// Clean up when promise resolves/rejects
|
|
155
|
+
promise.finally(() => {
|
|
156
|
+
const currentPromises = pendingLazyRouteLoads.get(span);
|
|
157
|
+
if (currentPromises) {
|
|
158
|
+
currentPromises.delete(promise);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
72
162
|
|
|
73
163
|
/**
|
|
74
164
|
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
|
|
@@ -134,13 +224,14 @@ function updateNavigationSpan(
|
|
|
134
224
|
forceUpdate = false,
|
|
135
225
|
matchRoutes,
|
|
136
226
|
) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
227
|
+
const spanJson = core.spanToJSON(activeRootSpan);
|
|
228
|
+
const currentName = spanJson.description;
|
|
229
|
+
|
|
230
|
+
const hasBeenNamed = (activeRootSpan )?.__sentry_navigation_name_set__;
|
|
231
|
+
const currentNameHasWildcard = currentName && utils.transactionNameHasWildcard(currentName);
|
|
232
|
+
const shouldUpdate = !hasBeenNamed || forceUpdate || currentNameHasWildcard;
|
|
141
233
|
|
|
142
|
-
if (!
|
|
143
|
-
// Get fresh branches for the current location with all loaded routes
|
|
234
|
+
if (shouldUpdate && !spanJson.timestamp) {
|
|
144
235
|
const currentBranches = matchRoutes(allRoutes, location);
|
|
145
236
|
const [name, source] = utils.resolveRouteNameAndSource(
|
|
146
237
|
location,
|
|
@@ -150,22 +241,105 @@ function updateNavigationSpan(
|
|
|
150
241
|
'',
|
|
151
242
|
);
|
|
152
243
|
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
244
|
+
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
245
|
+
const isImprovement =
|
|
246
|
+
name &&
|
|
247
|
+
(!currentName || // No current name - always set
|
|
248
|
+
(!hasBeenNamed && (currentSource !== 'route' || source === 'route')) || // Not finalized - allow unless downgrading route→url
|
|
249
|
+
(currentSource !== 'route' && source === 'route') || // URL → route upgrade
|
|
250
|
+
(currentSource === 'route' && source === 'route' && currentNameHasWildcard)); // Route → better route (only if current has wildcard)
|
|
251
|
+
if (isImprovement) {
|
|
156
252
|
activeRootSpan.updateName(name);
|
|
157
253
|
activeRootSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
158
254
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
255
|
+
// Only mark as finalized for non-wildcard route names (allows URL→route upgrades).
|
|
256
|
+
if (!utils.transactionNameHasWildcard(name) && source === 'route') {
|
|
257
|
+
core.addNonEnumerableProperty(
|
|
258
|
+
activeRootSpan ,
|
|
259
|
+
'__sentry_navigation_name_set__',
|
|
260
|
+
true,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
165
263
|
}
|
|
166
264
|
}
|
|
167
265
|
}
|
|
168
266
|
|
|
267
|
+
function setupRouterSubscription(
|
|
268
|
+
router,
|
|
269
|
+
routes,
|
|
270
|
+
version,
|
|
271
|
+
basename,
|
|
272
|
+
activeRootSpan,
|
|
273
|
+
) {
|
|
274
|
+
let isInitialPageloadComplete = false;
|
|
275
|
+
let hasSeenPageloadSpan = !!activeRootSpan && core.spanToJSON(activeRootSpan).op === 'pageload';
|
|
276
|
+
let hasSeenPopAfterPageload = false;
|
|
277
|
+
let scheduledNavigationHandler = null;
|
|
278
|
+
let lastHandledPathname = null;
|
|
279
|
+
|
|
280
|
+
router.subscribe((state) => {
|
|
281
|
+
if (!isInitialPageloadComplete) {
|
|
282
|
+
const currentRootSpan = getActiveRootSpan();
|
|
283
|
+
const isCurrentlyInPageload = currentRootSpan && core.spanToJSON(currentRootSpan).op === 'pageload';
|
|
284
|
+
|
|
285
|
+
if (isCurrentlyInPageload) {
|
|
286
|
+
hasSeenPageloadSpan = true;
|
|
287
|
+
} else if (hasSeenPageloadSpan) {
|
|
288
|
+
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
289
|
+
hasSeenPopAfterPageload = true;
|
|
290
|
+
} else {
|
|
291
|
+
isInitialPageloadComplete = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const shouldHandleNavigation =
|
|
297
|
+
state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete);
|
|
298
|
+
|
|
299
|
+
if (shouldHandleNavigation) {
|
|
300
|
+
// Include search and hash to allow query/hash-only navigations
|
|
301
|
+
// Use computeLocationKey() to ensure undefined/null values are normalized to empty strings
|
|
302
|
+
const currentLocationKey = computeLocationKey(state.location);
|
|
303
|
+
const navigationHandler = () => {
|
|
304
|
+
// Prevent multiple calls for the same location within the same navigation cycle
|
|
305
|
+
if (lastHandledPathname === currentLocationKey) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
lastHandledPathname = currentLocationKey;
|
|
309
|
+
scheduledNavigationHandler = null;
|
|
310
|
+
handleNavigation({
|
|
311
|
+
location: state.location,
|
|
312
|
+
routes,
|
|
313
|
+
navigationType: state.historyAction,
|
|
314
|
+
version,
|
|
315
|
+
basename,
|
|
316
|
+
allRoutes: Array.from(allRoutes),
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (state.navigation.state !== 'idle') {
|
|
321
|
+
// Navigation in progress - reset if location changed
|
|
322
|
+
if (lastHandledPathname !== currentLocationKey) {
|
|
323
|
+
lastHandledPathname = null;
|
|
324
|
+
}
|
|
325
|
+
// Cancel any previously scheduled handler to avoid duplicates
|
|
326
|
+
if (scheduledNavigationHandler !== null) {
|
|
327
|
+
cancelScheduledCallback(scheduledNavigationHandler);
|
|
328
|
+
}
|
|
329
|
+
scheduledNavigationHandler = scheduleCallback(navigationHandler);
|
|
330
|
+
} else {
|
|
331
|
+
// Navigation completed - cancel scheduled handler if any, then call immediately
|
|
332
|
+
if (scheduledNavigationHandler !== null) {
|
|
333
|
+
cancelScheduledCallback(scheduledNavigationHandler);
|
|
334
|
+
scheduledNavigationHandler = null;
|
|
335
|
+
}
|
|
336
|
+
navigationHandler();
|
|
337
|
+
// Don't reset - next navigation cycle resets to prevent duplicates within same cycle.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
169
343
|
/**
|
|
170
344
|
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions.
|
|
171
345
|
*/
|
|
@@ -187,30 +361,17 @@ function createV6CompatibleWrapCreateBrowserRouter
|
|
|
187
361
|
return function (routes, opts) {
|
|
188
362
|
addRoutesToAllRoutes(routes);
|
|
189
363
|
|
|
190
|
-
// Check for async handlers that might contain sub-route declarations (only if enabled)
|
|
191
364
|
if (_enableAsyncRouteHandlers) {
|
|
192
365
|
for (const route of routes) {
|
|
193
366
|
lazyRoutes.checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
194
367
|
}
|
|
195
368
|
}
|
|
196
369
|
|
|
197
|
-
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded
|
|
198
370
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts);
|
|
199
|
-
|
|
200
371
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
201
372
|
const basename = opts?.basename;
|
|
202
|
-
|
|
203
373
|
const activeRootSpan = getActiveRootSpan();
|
|
204
374
|
|
|
205
|
-
// Track whether we've completed the initial pageload to properly distinguish
|
|
206
|
-
// between POPs that occur during pageload vs. legitimate back/forward navigation.
|
|
207
|
-
let isInitialPageloadComplete = false;
|
|
208
|
-
let hasSeenPageloadSpan = !!activeRootSpan && core.spanToJSON(activeRootSpan).op === 'pageload';
|
|
209
|
-
let hasSeenPopAfterPageload = false;
|
|
210
|
-
|
|
211
|
-
// The initial load ends when `createBrowserRouter` is called.
|
|
212
|
-
// This is the earliest convenient time to update the transaction name.
|
|
213
|
-
// Callbacks to `router.subscribe` are not called for the initial load.
|
|
214
375
|
if (router.state.historyAction === 'POP' && activeRootSpan) {
|
|
215
376
|
updatePageloadTransaction({
|
|
216
377
|
activeRootSpan,
|
|
@@ -221,38 +382,7 @@ function createV6CompatibleWrapCreateBrowserRouter
|
|
|
221
382
|
});
|
|
222
383
|
}
|
|
223
384
|
|
|
224
|
-
router
|
|
225
|
-
// Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation
|
|
226
|
-
if (!isInitialPageloadComplete) {
|
|
227
|
-
const currentRootSpan = getActiveRootSpan();
|
|
228
|
-
const isCurrentlyInPageload = currentRootSpan && core.spanToJSON(currentRootSpan).op === 'pageload';
|
|
229
|
-
|
|
230
|
-
if (isCurrentlyInPageload) {
|
|
231
|
-
hasSeenPageloadSpan = true;
|
|
232
|
-
} else if (hasSeenPageloadSpan) {
|
|
233
|
-
// Pageload span was active but is now gone - pageload has completed
|
|
234
|
-
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
235
|
-
// Pageload ended: ignore the first POP after pageload
|
|
236
|
-
hasSeenPopAfterPageload = true;
|
|
237
|
-
} else {
|
|
238
|
-
// Pageload ended: either non-POP action or subsequent POP
|
|
239
|
-
isInitialPageloadComplete = true;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// If we haven't seen a pageload span yet, keep waiting (don't mark as complete)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (shouldHandleNavigation(state, isInitialPageloadComplete)) {
|
|
246
|
-
handleNavigation({
|
|
247
|
-
location: state.location,
|
|
248
|
-
routes,
|
|
249
|
-
navigationType: state.historyAction,
|
|
250
|
-
version,
|
|
251
|
-
basename,
|
|
252
|
-
allRoutes: Array.from(allRoutes),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
});
|
|
385
|
+
setupRouterSubscription(router, routes, version, basename, activeRootSpan);
|
|
256
386
|
|
|
257
387
|
return router;
|
|
258
388
|
};
|
|
@@ -284,14 +414,12 @@ function createV6CompatibleWrapCreateMemoryRouter
|
|
|
284
414
|
) {
|
|
285
415
|
addRoutesToAllRoutes(routes);
|
|
286
416
|
|
|
287
|
-
// Check for async handlers that might contain sub-route declarations (only if enabled)
|
|
288
417
|
if (_enableAsyncRouteHandlers) {
|
|
289
418
|
for (const route of routes) {
|
|
290
419
|
lazyRoutes.checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
291
420
|
}
|
|
292
421
|
}
|
|
293
422
|
|
|
294
|
-
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded
|
|
295
423
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true);
|
|
296
424
|
|
|
297
425
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
@@ -329,44 +457,7 @@ function createV6CompatibleWrapCreateMemoryRouter
|
|
|
329
457
|
});
|
|
330
458
|
}
|
|
331
459
|
|
|
332
|
-
|
|
333
|
-
// between POPs that occur during pageload vs. legitimate back/forward navigation.
|
|
334
|
-
let isInitialPageloadComplete = false;
|
|
335
|
-
let hasSeenPageloadSpan = !!memoryActiveRootSpan && core.spanToJSON(memoryActiveRootSpan).op === 'pageload';
|
|
336
|
-
let hasSeenPopAfterPageload = false;
|
|
337
|
-
|
|
338
|
-
router.subscribe((state) => {
|
|
339
|
-
// Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation
|
|
340
|
-
if (!isInitialPageloadComplete) {
|
|
341
|
-
const currentRootSpan = getActiveRootSpan();
|
|
342
|
-
const isCurrentlyInPageload = currentRootSpan && core.spanToJSON(currentRootSpan).op === 'pageload';
|
|
343
|
-
|
|
344
|
-
if (isCurrentlyInPageload) {
|
|
345
|
-
hasSeenPageloadSpan = true;
|
|
346
|
-
} else if (hasSeenPageloadSpan) {
|
|
347
|
-
// Pageload span was active but is now gone - pageload has completed
|
|
348
|
-
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
349
|
-
// Pageload ended: ignore the first POP after pageload
|
|
350
|
-
hasSeenPopAfterPageload = true;
|
|
351
|
-
} else {
|
|
352
|
-
// Pageload ended: either non-POP action or subsequent POP
|
|
353
|
-
isInitialPageloadComplete = true;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// If we haven't seen a pageload span yet, keep waiting (don't mark as complete)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (shouldHandleNavigation(state, isInitialPageloadComplete)) {
|
|
360
|
-
handleNavigation({
|
|
361
|
-
location: state.location,
|
|
362
|
-
routes,
|
|
363
|
-
navigationType: state.historyAction,
|
|
364
|
-
version,
|
|
365
|
-
basename,
|
|
366
|
-
allRoutes: Array.from(allRoutes),
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
});
|
|
460
|
+
setupRouterSubscription(router, routes, version, basename, memoryActiveRootSpan);
|
|
370
461
|
|
|
371
462
|
return router;
|
|
372
463
|
};
|
|
@@ -391,6 +482,7 @@ function createReactRouterV6CompatibleTracingIntegration(
|
|
|
391
482
|
enableAsyncRouteHandlers = false,
|
|
392
483
|
instrumentPageLoad = true,
|
|
393
484
|
instrumentNavigation = true,
|
|
485
|
+
lazyRouteTimeout,
|
|
394
486
|
} = options;
|
|
395
487
|
|
|
396
488
|
return {
|
|
@@ -398,6 +490,36 @@ function createReactRouterV6CompatibleTracingIntegration(
|
|
|
398
490
|
setup(client) {
|
|
399
491
|
integration.setup(client);
|
|
400
492
|
|
|
493
|
+
const finalTimeout = options.finalTimeout ?? 30000;
|
|
494
|
+
const defaultMaxWait = (options.idleTimeout ?? 1000) * 3;
|
|
495
|
+
const configuredMaxWait = lazyRouteTimeout ?? defaultMaxWait;
|
|
496
|
+
|
|
497
|
+
// Cap Infinity at finalTimeout to prevent indefinite hangs
|
|
498
|
+
if (configuredMaxWait === Infinity) {
|
|
499
|
+
_lazyRouteTimeout = finalTimeout;
|
|
500
|
+
debugBuild.DEBUG_BUILD &&
|
|
501
|
+
core.debug.log(
|
|
502
|
+
'[React Router] lazyRouteTimeout set to Infinity, capping at finalTimeout:',
|
|
503
|
+
finalTimeout,
|
|
504
|
+
'ms to prevent indefinite hangs',
|
|
505
|
+
);
|
|
506
|
+
} else if (Number.isNaN(configuredMaxWait)) {
|
|
507
|
+
debugBuild.DEBUG_BUILD &&
|
|
508
|
+
core.debug.warn('[React Router] lazyRouteTimeout must be a number, falling back to default:', defaultMaxWait);
|
|
509
|
+
_lazyRouteTimeout = defaultMaxWait;
|
|
510
|
+
} else if (configuredMaxWait < 0) {
|
|
511
|
+
debugBuild.DEBUG_BUILD &&
|
|
512
|
+
core.debug.warn(
|
|
513
|
+
'[React Router] lazyRouteTimeout must be non-negative or Infinity, got:',
|
|
514
|
+
configuredMaxWait,
|
|
515
|
+
'falling back to:',
|
|
516
|
+
defaultMaxWait,
|
|
517
|
+
);
|
|
518
|
+
_lazyRouteTimeout = defaultMaxWait;
|
|
519
|
+
} else {
|
|
520
|
+
_lazyRouteTimeout = configuredMaxWait;
|
|
521
|
+
}
|
|
522
|
+
|
|
401
523
|
_useEffect = useEffect;
|
|
402
524
|
_useLocation = useLocation;
|
|
403
525
|
_useNavigationType = useNavigationType;
|
|
@@ -470,6 +592,9 @@ function createV6CompatibleWrapUseRoutes(origUseRoutes, version) {
|
|
|
470
592
|
});
|
|
471
593
|
isMountRenderPass.current = false;
|
|
472
594
|
} else {
|
|
595
|
+
// Note: Component-based routes don't support lazy route tracking via lazyRouteTimeout
|
|
596
|
+
// because React.lazy() loads happen at the component level, not the router level.
|
|
597
|
+
// Use createBrowserRouter with patchRoutesOnNavigation for lazy route tracking.
|
|
473
598
|
handleNavigation({
|
|
474
599
|
location: normalizedLocation,
|
|
475
600
|
routes,
|
|
@@ -504,7 +629,8 @@ function wrapPatchRoutesOnNavigation(
|
|
|
504
629
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
505
630
|
const targetPath = (args )?.path;
|
|
506
631
|
|
|
507
|
-
|
|
632
|
+
const activeRootSpan = getActiveRootSpan();
|
|
633
|
+
|
|
508
634
|
if (!isMemoryRouter) {
|
|
509
635
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
510
636
|
const originalPatch = (args )?.patch;
|
|
@@ -512,13 +638,13 @@ function wrapPatchRoutesOnNavigation(
|
|
|
512
638
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
513
639
|
(args ).patch = (routeId, children) => {
|
|
514
640
|
addRoutesToAllRoutes(children);
|
|
515
|
-
const
|
|
516
|
-
if (
|
|
641
|
+
const currentActiveRootSpan = getActiveRootSpan();
|
|
642
|
+
if (currentActiveRootSpan && (core.spanToJSON(currentActiveRootSpan) ).op === 'navigation') {
|
|
517
643
|
updateNavigationSpan(
|
|
518
|
-
|
|
644
|
+
currentActiveRootSpan,
|
|
519
645
|
{ pathname: targetPath, search: '', hash: '', state: null, key: 'default' },
|
|
520
646
|
Array.from(allRoutes),
|
|
521
|
-
true,
|
|
647
|
+
true,
|
|
522
648
|
_matchRoutes,
|
|
523
649
|
);
|
|
524
650
|
}
|
|
@@ -527,94 +653,37 @@ function wrapPatchRoutesOnNavigation(
|
|
|
527
653
|
}
|
|
528
654
|
}
|
|
529
655
|
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
656
|
+
const lazyLoadPromise = (async () => {
|
|
657
|
+
const result = await originalPatchRoutes(args);
|
|
658
|
+
|
|
659
|
+
const currentActiveRootSpan = getActiveRootSpan();
|
|
660
|
+
if (currentActiveRootSpan && (core.spanToJSON(currentActiveRootSpan) ).op === 'navigation') {
|
|
661
|
+
const pathname = isMemoryRouter ? targetPath : targetPath || browser.WINDOW.location?.pathname;
|
|
662
|
+
|
|
663
|
+
if (pathname) {
|
|
664
|
+
updateNavigationSpan(
|
|
665
|
+
currentActiveRootSpan,
|
|
666
|
+
{ pathname, search: '', hash: '', state: null, key: 'default' },
|
|
667
|
+
Array.from(allRoutes),
|
|
668
|
+
false,
|
|
669
|
+
_matchRoutes,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
543
672
|
}
|
|
544
673
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
false, // forceUpdate = false since this is after lazy routes are loaded
|
|
551
|
-
_matchRoutes,
|
|
552
|
-
);
|
|
553
|
-
}
|
|
674
|
+
return result;
|
|
675
|
+
})();
|
|
676
|
+
|
|
677
|
+
if (activeRootSpan) {
|
|
678
|
+
trackLazyRouteLoad(activeRootSpan, lazyLoadPromise);
|
|
554
679
|
}
|
|
555
680
|
|
|
556
|
-
return
|
|
681
|
+
return lazyLoadPromise;
|
|
557
682
|
},
|
|
558
683
|
};
|
|
559
684
|
}
|
|
560
685
|
|
|
561
|
-
|
|
562
|
-
return `${location.pathname}${location.search}${location.hash}`;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function tryUpdateSpanName(
|
|
566
|
-
activeSpan,
|
|
567
|
-
currentSpanName,
|
|
568
|
-
newName,
|
|
569
|
-
newSource,
|
|
570
|
-
) {
|
|
571
|
-
// Check if the new name contains React Router parameter syntax (/:param/)
|
|
572
|
-
const isReactRouterParam = /\/:[a-zA-Z0-9_]+/.test(newName);
|
|
573
|
-
const isNewNameParameterized = newName !== currentSpanName && isReactRouterParam;
|
|
574
|
-
if (isNewNameParameterized) {
|
|
575
|
-
activeSpan.updateName(newName);
|
|
576
|
-
activeSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, newSource );
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function isDuplicateNavigation(client, navigationKey) {
|
|
581
|
-
const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client);
|
|
582
|
-
return lastKey === navigationKey;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function createNavigationSpan(opts
|
|
586
|
-
|
|
587
|
-
) {
|
|
588
|
-
const { client, name, source, version, location, routes, basename, allRoutes, navigationKey } = opts;
|
|
589
|
-
|
|
590
|
-
const navigationSpan = browser.startBrowserTracingNavigationSpan(client, {
|
|
591
|
-
name,
|
|
592
|
-
attributes: {
|
|
593
|
-
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source ,
|
|
594
|
-
[core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
|
|
595
|
-
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`,
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
if (navigationSpan) {
|
|
600
|
-
LAST_NAVIGATION_PER_CLIENT.set(client, navigationKey);
|
|
601
|
-
patchNavigationSpanEnd(navigationSpan, location, routes, basename, allRoutes);
|
|
602
|
-
|
|
603
|
-
const unsubscribe = client.on('spanEnd', endedSpan => {
|
|
604
|
-
if (endedSpan === navigationSpan) {
|
|
605
|
-
// Clear key only if it's still our key (handles overlapping navigations)
|
|
606
|
-
const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client);
|
|
607
|
-
if (lastKey === navigationKey) {
|
|
608
|
-
LAST_NAVIGATION_PER_CLIENT.delete(client);
|
|
609
|
-
}
|
|
610
|
-
unsubscribe(); // Prevent memory leak
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return navigationSpan;
|
|
616
|
-
}
|
|
617
|
-
|
|
686
|
+
// eslint-disable-next-line complexity
|
|
618
687
|
function handleNavigation(opts
|
|
619
688
|
|
|
620
689
|
) {
|
|
@@ -640,33 +709,84 @@ function handleNavigation(opts
|
|
|
640
709
|
basename,
|
|
641
710
|
);
|
|
642
711
|
|
|
643
|
-
const
|
|
644
|
-
const
|
|
712
|
+
const locationKey = computeLocationKey(location);
|
|
713
|
+
const trackedNav = activeNavigationSpans.get(client);
|
|
714
|
+
|
|
715
|
+
// Determine if this navigation should be skipped as a duplicate
|
|
716
|
+
const trackedSpanHasEnded =
|
|
717
|
+
trackedNav && !trackedNav.isPlaceholder ? !!core.spanToJSON(trackedNav.span).timestamp : false;
|
|
718
|
+
const { skip, shouldUpdate } = shouldSkipNavigation(trackedNav, locationKey, name, trackedSpanHasEnded);
|
|
719
|
+
|
|
720
|
+
if (skip) {
|
|
721
|
+
if (shouldUpdate && trackedNav) {
|
|
722
|
+
const oldName = trackedNav.routeName;
|
|
723
|
+
|
|
724
|
+
if (trackedNav.isPlaceholder) {
|
|
725
|
+
// Update placeholder's route name - the real span will be created with this name
|
|
726
|
+
trackedNav.routeName = name;
|
|
727
|
+
debugBuild.DEBUG_BUILD &&
|
|
728
|
+
core.debug.log(
|
|
729
|
+
`[Tracing] Updated placeholder navigation name from "${oldName}" to "${name}" (will apply to real span)`,
|
|
730
|
+
);
|
|
731
|
+
} else {
|
|
732
|
+
// Update existing real span from wildcard to parameterized route name
|
|
733
|
+
trackedNav.span.updateName(name);
|
|
734
|
+
trackedNav.span.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source );
|
|
735
|
+
core.addNonEnumerableProperty(
|
|
736
|
+
trackedNav.span ,
|
|
737
|
+
'__sentry_navigation_name_set__',
|
|
738
|
+
true,
|
|
739
|
+
);
|
|
740
|
+
trackedNav.routeName = name;
|
|
741
|
+
debugBuild.DEBUG_BUILD && core.debug.log(`[Tracing] Updated navigation span name from "${oldName}" to "${name}"`);
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
debugBuild.DEBUG_BUILD && core.debug.log(`[Tracing] Skipping duplicate navigation for location: ${locationKey}`);
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
645
748
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
749
|
+
// Create new navigation span (first navigation or legitimate new navigation)
|
|
750
|
+
// Reserve the spot in the map first to prevent race conditions
|
|
751
|
+
// Mark as placeholder to prevent concurrent handleNavigation calls from creating duplicates
|
|
752
|
+
const placeholderSpan = { end: () => {} } ;
|
|
753
|
+
const placeholderEntry = {
|
|
754
|
+
span: placeholderSpan,
|
|
755
|
+
routeName: name,
|
|
756
|
+
pathname: location.pathname,
|
|
757
|
+
locationKey,
|
|
758
|
+
isPlaceholder: true ,
|
|
759
|
+
};
|
|
760
|
+
activeNavigationSpans.set(client, placeholderEntry);
|
|
761
|
+
|
|
762
|
+
let navigationSpan;
|
|
763
|
+
try {
|
|
764
|
+
navigationSpan = browser.startBrowserTracingNavigationSpan(client, {
|
|
765
|
+
name: placeholderEntry.routeName, // Use placeholder's routeName in case it was updated
|
|
766
|
+
attributes: {
|
|
767
|
+
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
|
|
768
|
+
[core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
|
|
769
|
+
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
} catch (e) {
|
|
773
|
+
// If span creation fails, remove the placeholder so we don't block future navigations
|
|
774
|
+
activeNavigationSpans.delete(client);
|
|
775
|
+
throw e;
|
|
776
|
+
}
|
|
651
777
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
createNavigationSpan({
|
|
660
|
-
client,
|
|
661
|
-
name,
|
|
662
|
-
source,
|
|
663
|
-
version,
|
|
664
|
-
location,
|
|
665
|
-
routes,
|
|
666
|
-
basename,
|
|
667
|
-
allRoutes,
|
|
668
|
-
navigationKey: currentNavigationKey,
|
|
778
|
+
if (navigationSpan) {
|
|
779
|
+
// Update the map with the real span (isPlaceholder omitted, defaults to false)
|
|
780
|
+
activeNavigationSpans.set(client, {
|
|
781
|
+
span: navigationSpan,
|
|
782
|
+
routeName: placeholderEntry.routeName, // Use the (potentially updated) placeholder routeName
|
|
783
|
+
pathname: location.pathname,
|
|
784
|
+
locationKey,
|
|
669
785
|
});
|
|
786
|
+
patchSpanEnd(navigationSpan, location, routes, basename, allRoutes, 'navigation');
|
|
787
|
+
} else {
|
|
788
|
+
// If no span was created, remove the placeholder
|
|
789
|
+
activeNavigationSpans.delete(client);
|
|
670
790
|
}
|
|
671
791
|
}
|
|
672
792
|
}
|
|
@@ -730,8 +850,90 @@ function updatePageloadTransaction({
|
|
|
730
850
|
activeRootSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
731
851
|
|
|
732
852
|
// Patch span.end() to ensure we update the name one last time before the span is sent
|
|
733
|
-
|
|
853
|
+
patchSpanEnd(activeRootSpan, location, routes, basename, allRoutes, 'pageload');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Determines if a span name should be updated during wildcard route resolution.
|
|
860
|
+
*
|
|
861
|
+
* Update conditions (in priority order):
|
|
862
|
+
* 1. No current name + allowNoCurrentName: true → always update (pageload spans)
|
|
863
|
+
* 2. Current name has wildcard + new is route without wildcard → upgrade (e.g., "/users/*" → "/users/:id")
|
|
864
|
+
* 3. Current source is not 'route' + new source is 'route' → upgrade (e.g., URL → parameterized route)
|
|
865
|
+
*
|
|
866
|
+
* @param currentName - The current span name (may be undefined)
|
|
867
|
+
* @param currentSource - The current span source ('route', 'url', or undefined)
|
|
868
|
+
* @param newName - The proposed new span name
|
|
869
|
+
* @param newSource - The proposed new span source
|
|
870
|
+
* @param allowNoCurrentName - If true, allow updates when there's no current name (for pageload spans)
|
|
871
|
+
* @returns true if the span name should be updated
|
|
872
|
+
*/
|
|
873
|
+
function shouldUpdateWildcardSpanName(
|
|
874
|
+
currentName,
|
|
875
|
+
currentSource,
|
|
876
|
+
newName,
|
|
877
|
+
newSource,
|
|
878
|
+
allowNoCurrentName = false,
|
|
879
|
+
) {
|
|
880
|
+
if (!newName) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!currentName && allowNoCurrentName) {
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const hasWildcard = currentName && utils.transactionNameHasWildcard(currentName);
|
|
889
|
+
|
|
890
|
+
if (hasWildcard && newSource === 'route' && !utils.transactionNameHasWildcard(newName)) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (currentSource !== 'route' && newSource === 'route') {
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function tryUpdateSpanNameBeforeEnd(
|
|
902
|
+
span,
|
|
903
|
+
spanJson,
|
|
904
|
+
currentName,
|
|
905
|
+
location,
|
|
906
|
+
routes,
|
|
907
|
+
basename,
|
|
908
|
+
spanType,
|
|
909
|
+
allRoutes,
|
|
910
|
+
) {
|
|
911
|
+
try {
|
|
912
|
+
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
913
|
+
|
|
914
|
+
if (currentSource === 'route' && currentName && !utils.transactionNameHasWildcard(currentName)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const currentAllRoutes = Array.from(allRoutes);
|
|
919
|
+
const routesToUse = currentAllRoutes.length > 0 ? currentAllRoutes : routes;
|
|
920
|
+
const branches = _matchRoutes(routesToUse, location, basename) ;
|
|
921
|
+
|
|
922
|
+
if (!branches) {
|
|
923
|
+
return;
|
|
734
924
|
}
|
|
925
|
+
|
|
926
|
+
const [name, source] = utils.resolveRouteNameAndSource(location, routesToUse, routesToUse, branches, basename);
|
|
927
|
+
|
|
928
|
+
const isImprovement = shouldUpdateWildcardSpanName(currentName, currentSource, name, source, true);
|
|
929
|
+
const spanNotEnded = spanType === 'pageload' || !spanJson.timestamp;
|
|
930
|
+
|
|
931
|
+
if (isImprovement && spanNotEnded) {
|
|
932
|
+
span.updateName(name);
|
|
933
|
+
span.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
934
|
+
}
|
|
935
|
+
} catch (error) {
|
|
936
|
+
debugBuild.DEBUG_BUILD && core.debug.warn(`Error updating span details before ending: ${error}`);
|
|
735
937
|
}
|
|
736
938
|
}
|
|
737
939
|
|
|
@@ -754,71 +956,93 @@ function patchSpanEnd(
|
|
|
754
956
|
return;
|
|
755
957
|
}
|
|
756
958
|
|
|
959
|
+
// Use the passed route context, or fall back to global Set
|
|
960
|
+
const allRoutesSet = _allRoutes ? new Set(_allRoutes) : allRoutes;
|
|
961
|
+
|
|
757
962
|
const originalEnd = span.end.bind(span);
|
|
963
|
+
let endCalled = false;
|
|
758
964
|
|
|
759
965
|
span.end = function patchedEnd(...args) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
966
|
+
if (endCalled) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
endCalled = true;
|
|
970
|
+
|
|
971
|
+
// Capture timestamp immediately to avoid delay from async operations
|
|
972
|
+
// If no timestamp was provided, capture the current time now
|
|
973
|
+
const endTimestamp = args.length > 0 ? args[0] : Date.now() / 1000;
|
|
974
|
+
|
|
975
|
+
const spanJson = core.spanToJSON(span);
|
|
976
|
+
const currentName = spanJson.description;
|
|
977
|
+
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
978
|
+
|
|
979
|
+
// Helper to clean up activeNavigationSpans after span ends
|
|
980
|
+
const cleanupNavigationSpan = () => {
|
|
981
|
+
const client = core.getClient();
|
|
982
|
+
if (client && spanType === 'navigation') {
|
|
983
|
+
const trackedNav = activeNavigationSpans.get(client);
|
|
984
|
+
if (trackedNav && trackedNav.span === span) {
|
|
985
|
+
activeNavigationSpans.delete(client);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const pendingPromises = pendingLazyRouteLoads.get(span);
|
|
991
|
+
// Wait for lazy routes if:
|
|
992
|
+
// 1. There are pending promises AND
|
|
993
|
+
// 2. Current name exists AND
|
|
994
|
+
// 3. Either the name has a wildcard OR the source is not 'route' (URL-based names)
|
|
995
|
+
const shouldWaitForLazyRoutes =
|
|
996
|
+
pendingPromises &&
|
|
997
|
+
pendingPromises.size > 0 &&
|
|
998
|
+
currentName &&
|
|
999
|
+
(utils.transactionNameHasWildcard(currentName) || currentSource !== 'route');
|
|
1000
|
+
|
|
1001
|
+
if (shouldWaitForLazyRoutes) {
|
|
1002
|
+
if (_lazyRouteTimeout === 0) {
|
|
1003
|
+
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutesSet);
|
|
1004
|
+
cleanupNavigationSpan();
|
|
1005
|
+
originalEnd(endTimestamp);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
773
1008
|
|
|
774
|
-
|
|
775
|
-
|
|
1009
|
+
const allSettled = Promise.allSettled(pendingPromises).then(() => {});
|
|
1010
|
+
const waitPromise =
|
|
1011
|
+
_lazyRouteTimeout === Infinity
|
|
1012
|
+
? allSettled
|
|
1013
|
+
: Promise.race([allSettled, new Promise(r => setTimeout(r, _lazyRouteTimeout))]);
|
|
1014
|
+
|
|
1015
|
+
waitPromise
|
|
1016
|
+
.then(() => {
|
|
1017
|
+
const updatedSpanJson = core.spanToJSON(span);
|
|
1018
|
+
tryUpdateSpanNameBeforeEnd(
|
|
1019
|
+
span,
|
|
1020
|
+
updatedSpanJson,
|
|
1021
|
+
updatedSpanJson.description,
|
|
776
1022
|
location,
|
|
777
|
-
|
|
778
|
-
currentAllRoutes.length > 0 ? currentAllRoutes : routes,
|
|
779
|
-
branches,
|
|
1023
|
+
routes,
|
|
780
1024
|
basename,
|
|
1025
|
+
spanType,
|
|
1026
|
+
allRoutesSet,
|
|
781
1027
|
);
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
} catch (error) {
|
|
791
|
-
// Silently catch errors to ensure span.end() is always called
|
|
792
|
-
debugBuild.DEBUG_BUILD && core.debug.warn(`Error updating span details before ending: ${error}`);
|
|
1028
|
+
cleanupNavigationSpan();
|
|
1029
|
+
originalEnd(endTimestamp);
|
|
1030
|
+
})
|
|
1031
|
+
.catch(() => {
|
|
1032
|
+
cleanupNavigationSpan();
|
|
1033
|
+
originalEnd(endTimestamp);
|
|
1034
|
+
});
|
|
1035
|
+
return;
|
|
793
1036
|
}
|
|
794
1037
|
|
|
795
|
-
|
|
1038
|
+
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutesSet);
|
|
1039
|
+
cleanupNavigationSpan();
|
|
1040
|
+
originalEnd(endTimestamp);
|
|
796
1041
|
};
|
|
797
1042
|
|
|
798
|
-
// Mark this span as having its end() method patched to prevent duplicate patching
|
|
799
1043
|
core.addNonEnumerableProperty(span , patchedPropertyName, true);
|
|
800
1044
|
}
|
|
801
1045
|
|
|
802
|
-
function patchPageloadSpanEnd(
|
|
803
|
-
span,
|
|
804
|
-
location,
|
|
805
|
-
routes,
|
|
806
|
-
basename,
|
|
807
|
-
_allRoutes,
|
|
808
|
-
) {
|
|
809
|
-
patchSpanEnd(span, location, routes, basename, _allRoutes, 'pageload');
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function patchNavigationSpanEnd(
|
|
813
|
-
span,
|
|
814
|
-
location,
|
|
815
|
-
routes,
|
|
816
|
-
basename,
|
|
817
|
-
_allRoutes,
|
|
818
|
-
) {
|
|
819
|
-
patchSpanEnd(span, location, routes, basename, _allRoutes, 'navigation');
|
|
820
|
-
}
|
|
821
|
-
|
|
822
1046
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
823
1047
|
function createV6CompatibleWithSentryReactRouterRouting(
|
|
824
1048
|
Routes,
|
|
@@ -854,11 +1078,13 @@ function createV6CompatibleWithSentryReactRouterRouting(
|
|
|
854
1078
|
});
|
|
855
1079
|
isMountRenderPass.current = false;
|
|
856
1080
|
} else {
|
|
1081
|
+
// Note: Component-based routes don't support lazy route tracking via lazyRouteTimeout
|
|
1082
|
+
// because React.lazy() loads happen at the component level, not the router level.
|
|
1083
|
+
// Use createBrowserRouter with patchRoutesOnNavigation for lazy route tracking.
|
|
857
1084
|
handleNavigation({ location, routes, navigationType, version, allRoutes: Array.from(allRoutes) });
|
|
858
1085
|
}
|
|
859
1086
|
},
|
|
860
|
-
//
|
|
861
|
-
// when the children change. We only want to start transactions when the location or navigation type change.
|
|
1087
|
+
// Re-run only on location/navigation changes, not children changes
|
|
862
1088
|
[location, navigationType],
|
|
863
1089
|
);
|
|
864
1090
|
|
|
@@ -891,6 +1117,7 @@ function getActiveRootSpan() {
|
|
|
891
1117
|
exports.addResolvedRoutesToParent = addResolvedRoutesToParent;
|
|
892
1118
|
exports.addRoutesToAllRoutes = addRoutesToAllRoutes;
|
|
893
1119
|
exports.allRoutes = allRoutes;
|
|
1120
|
+
exports.computeLocationKey = computeLocationKey;
|
|
894
1121
|
exports.createReactRouterV6CompatibleTracingIntegration = createReactRouterV6CompatibleTracingIntegration;
|
|
895
1122
|
exports.createV6CompatibleWithSentryReactRouterRouting = createV6CompatibleWithSentryReactRouterRouting;
|
|
896
1123
|
exports.createV6CompatibleWrapCreateBrowserRouter = createV6CompatibleWrapCreateBrowserRouter;
|
|
@@ -898,5 +1125,6 @@ exports.createV6CompatibleWrapCreateMemoryRouter = createV6CompatibleWrapCreateM
|
|
|
898
1125
|
exports.createV6CompatibleWrapUseRoutes = createV6CompatibleWrapUseRoutes;
|
|
899
1126
|
exports.handleNavigation = handleNavigation;
|
|
900
1127
|
exports.processResolvedRoutes = processResolvedRoutes;
|
|
1128
|
+
exports.shouldSkipNavigation = shouldSkipNavigation;
|
|
901
1129
|
exports.updateNavigationSpan = updateNavigationSpan;
|
|
902
1130
|
//# sourceMappingURL=instrumentation.js.map
|