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