@sentry/react 10.26.0 → 10.28.0-alpha.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
|
@@ -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,15 +16,111 @@ 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
|
*/
|
|
27
|
-
|
|
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
|
+
}
|
|
28
124
|
|
|
29
125
|
function addResolvedRoutesToParent(resolvedRoutes, parentRoute) {
|
|
30
126
|
const existingChildren = parentRoute.children || [];
|
|
@@ -44,29 +140,23 @@ function addResolvedRoutesToParent(resolvedRoutes, parentRoute) {
|
|
|
44
140
|
}
|
|
45
141
|
}
|
|
46
142
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* which would cause us to create a span for the wrong route.
|
|
56
|
-
*/
|
|
57
|
-
function shouldHandleNavigation(
|
|
58
|
-
state,
|
|
59
|
-
isInitialPageloadComplete,
|
|
60
|
-
) {
|
|
61
|
-
return (
|
|
62
|
-
(state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete)) &&
|
|
63
|
-
state.navigation.state === 'idle'
|
|
64
|
-
);
|
|
65
|
-
}
|
|
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);
|
|
66
151
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
const
|
|
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
|
+
}
|
|
70
160
|
|
|
71
161
|
/**
|
|
72
162
|
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
|
|
@@ -132,13 +222,14 @@ function updateNavigationSpan(
|
|
|
132
222
|
forceUpdate = false,
|
|
133
223
|
matchRoutes,
|
|
134
224
|
) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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;
|
|
139
231
|
|
|
140
|
-
if (!
|
|
141
|
-
// Get fresh branches for the current location with all loaded routes
|
|
232
|
+
if (shouldUpdate && !spanJson.timestamp) {
|
|
142
233
|
const currentBranches = matchRoutes(allRoutes, location);
|
|
143
234
|
const [name, source] = resolveRouteNameAndSource(
|
|
144
235
|
location,
|
|
@@ -148,22 +239,105 @@ function updateNavigationSpan(
|
|
|
148
239
|
'',
|
|
149
240
|
);
|
|
150
241
|
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
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) {
|
|
154
250
|
activeRootSpan.updateName(name);
|
|
155
251
|
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
156
252
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
261
|
}
|
|
164
262
|
}
|
|
165
263
|
}
|
|
166
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
|
+
|
|
167
341
|
/**
|
|
168
342
|
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions.
|
|
169
343
|
*/
|
|
@@ -185,30 +359,17 @@ function createV6CompatibleWrapCreateBrowserRouter
|
|
|
185
359
|
return function (routes, opts) {
|
|
186
360
|
addRoutesToAllRoutes(routes);
|
|
187
361
|
|
|
188
|
-
// Check for async handlers that might contain sub-route declarations (only if enabled)
|
|
189
362
|
if (_enableAsyncRouteHandlers) {
|
|
190
363
|
for (const route of routes) {
|
|
191
364
|
checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
192
365
|
}
|
|
193
366
|
}
|
|
194
367
|
|
|
195
|
-
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded
|
|
196
368
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts);
|
|
197
|
-
|
|
198
369
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
199
370
|
const basename = opts?.basename;
|
|
200
|
-
|
|
201
371
|
const activeRootSpan = getActiveRootSpan();
|
|
202
372
|
|
|
203
|
-
// Track whether we've completed the initial pageload to properly distinguish
|
|
204
|
-
// between POPs that occur during pageload vs. legitimate back/forward navigation.
|
|
205
|
-
let isInitialPageloadComplete = false;
|
|
206
|
-
let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload';
|
|
207
|
-
let hasSeenPopAfterPageload = false;
|
|
208
|
-
|
|
209
|
-
// The initial load ends when `createBrowserRouter` is called.
|
|
210
|
-
// This is the earliest convenient time to update the transaction name.
|
|
211
|
-
// Callbacks to `router.subscribe` are not called for the initial load.
|
|
212
373
|
if (router.state.historyAction === 'POP' && activeRootSpan) {
|
|
213
374
|
updatePageloadTransaction({
|
|
214
375
|
activeRootSpan,
|
|
@@ -219,38 +380,7 @@ function createV6CompatibleWrapCreateBrowserRouter
|
|
|
219
380
|
});
|
|
220
381
|
}
|
|
221
382
|
|
|
222
|
-
router
|
|
223
|
-
// Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation
|
|
224
|
-
if (!isInitialPageloadComplete) {
|
|
225
|
-
const currentRootSpan = getActiveRootSpan();
|
|
226
|
-
const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload';
|
|
227
|
-
|
|
228
|
-
if (isCurrentlyInPageload) {
|
|
229
|
-
hasSeenPageloadSpan = true;
|
|
230
|
-
} else if (hasSeenPageloadSpan) {
|
|
231
|
-
// Pageload span was active but is now gone - pageload has completed
|
|
232
|
-
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
233
|
-
// Pageload ended: ignore the first POP after pageload
|
|
234
|
-
hasSeenPopAfterPageload = true;
|
|
235
|
-
} else {
|
|
236
|
-
// Pageload ended: either non-POP action or subsequent POP
|
|
237
|
-
isInitialPageloadComplete = true;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// If we haven't seen a pageload span yet, keep waiting (don't mark as complete)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (shouldHandleNavigation(state, isInitialPageloadComplete)) {
|
|
244
|
-
handleNavigation({
|
|
245
|
-
location: state.location,
|
|
246
|
-
routes,
|
|
247
|
-
navigationType: state.historyAction,
|
|
248
|
-
version,
|
|
249
|
-
basename,
|
|
250
|
-
allRoutes: Array.from(allRoutes),
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
});
|
|
383
|
+
setupRouterSubscription(router, routes, version, basename, activeRootSpan);
|
|
254
384
|
|
|
255
385
|
return router;
|
|
256
386
|
};
|
|
@@ -282,14 +412,12 @@ function createV6CompatibleWrapCreateMemoryRouter
|
|
|
282
412
|
) {
|
|
283
413
|
addRoutesToAllRoutes(routes);
|
|
284
414
|
|
|
285
|
-
// Check for async handlers that might contain sub-route declarations (only if enabled)
|
|
286
415
|
if (_enableAsyncRouteHandlers) {
|
|
287
416
|
for (const route of routes) {
|
|
288
417
|
checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
289
418
|
}
|
|
290
419
|
}
|
|
291
420
|
|
|
292
|
-
// Wrap patchRoutesOnNavigation to detect when lazy routes are loaded
|
|
293
421
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true);
|
|
294
422
|
|
|
295
423
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
@@ -327,44 +455,7 @@ function createV6CompatibleWrapCreateMemoryRouter
|
|
|
327
455
|
});
|
|
328
456
|
}
|
|
329
457
|
|
|
330
|
-
|
|
331
|
-
// between POPs that occur during pageload vs. legitimate back/forward navigation.
|
|
332
|
-
let isInitialPageloadComplete = false;
|
|
333
|
-
let hasSeenPageloadSpan = !!memoryActiveRootSpan && spanToJSON(memoryActiveRootSpan).op === 'pageload';
|
|
334
|
-
let hasSeenPopAfterPageload = false;
|
|
335
|
-
|
|
336
|
-
router.subscribe((state) => {
|
|
337
|
-
// Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation
|
|
338
|
-
if (!isInitialPageloadComplete) {
|
|
339
|
-
const currentRootSpan = getActiveRootSpan();
|
|
340
|
-
const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload';
|
|
341
|
-
|
|
342
|
-
if (isCurrentlyInPageload) {
|
|
343
|
-
hasSeenPageloadSpan = true;
|
|
344
|
-
} else if (hasSeenPageloadSpan) {
|
|
345
|
-
// Pageload span was active but is now gone - pageload has completed
|
|
346
|
-
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
347
|
-
// Pageload ended: ignore the first POP after pageload
|
|
348
|
-
hasSeenPopAfterPageload = true;
|
|
349
|
-
} else {
|
|
350
|
-
// Pageload ended: either non-POP action or subsequent POP
|
|
351
|
-
isInitialPageloadComplete = true;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// If we haven't seen a pageload span yet, keep waiting (don't mark as complete)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (shouldHandleNavigation(state, isInitialPageloadComplete)) {
|
|
358
|
-
handleNavigation({
|
|
359
|
-
location: state.location,
|
|
360
|
-
routes,
|
|
361
|
-
navigationType: state.historyAction,
|
|
362
|
-
version,
|
|
363
|
-
basename,
|
|
364
|
-
allRoutes: Array.from(allRoutes),
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
});
|
|
458
|
+
setupRouterSubscription(router, routes, version, basename, memoryActiveRootSpan);
|
|
368
459
|
|
|
369
460
|
return router;
|
|
370
461
|
};
|
|
@@ -389,6 +480,7 @@ function createReactRouterV6CompatibleTracingIntegration(
|
|
|
389
480
|
enableAsyncRouteHandlers = false,
|
|
390
481
|
instrumentPageLoad = true,
|
|
391
482
|
instrumentNavigation = true,
|
|
483
|
+
lazyRouteTimeout,
|
|
392
484
|
} = options;
|
|
393
485
|
|
|
394
486
|
return {
|
|
@@ -396,6 +488,36 @@ function createReactRouterV6CompatibleTracingIntegration(
|
|
|
396
488
|
setup(client) {
|
|
397
489
|
integration.setup(client);
|
|
398
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
|
+
|
|
399
521
|
_useEffect = useEffect;
|
|
400
522
|
_useLocation = useLocation;
|
|
401
523
|
_useNavigationType = useNavigationType;
|
|
@@ -468,6 +590,9 @@ function createV6CompatibleWrapUseRoutes(origUseRoutes, version) {
|
|
|
468
590
|
});
|
|
469
591
|
isMountRenderPass.current = false;
|
|
470
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.
|
|
471
596
|
handleNavigation({
|
|
472
597
|
location: normalizedLocation,
|
|
473
598
|
routes,
|
|
@@ -502,7 +627,8 @@ function wrapPatchRoutesOnNavigation(
|
|
|
502
627
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
503
628
|
const targetPath = (args )?.path;
|
|
504
629
|
|
|
505
|
-
|
|
630
|
+
const activeRootSpan = getActiveRootSpan();
|
|
631
|
+
|
|
506
632
|
if (!isMemoryRouter) {
|
|
507
633
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
508
634
|
const originalPatch = (args )?.patch;
|
|
@@ -510,13 +636,13 @@ function wrapPatchRoutesOnNavigation(
|
|
|
510
636
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
511
637
|
(args ).patch = (routeId, children) => {
|
|
512
638
|
addRoutesToAllRoutes(children);
|
|
513
|
-
const
|
|
514
|
-
if (
|
|
639
|
+
const currentActiveRootSpan = getActiveRootSpan();
|
|
640
|
+
if (currentActiveRootSpan && (spanToJSON(currentActiveRootSpan) ).op === 'navigation') {
|
|
515
641
|
updateNavigationSpan(
|
|
516
|
-
|
|
642
|
+
currentActiveRootSpan,
|
|
517
643
|
{ pathname: targetPath, search: '', hash: '', state: null, key: 'default' },
|
|
518
644
|
Array.from(allRoutes),
|
|
519
|
-
true,
|
|
645
|
+
true,
|
|
520
646
|
_matchRoutes,
|
|
521
647
|
);
|
|
522
648
|
}
|
|
@@ -525,94 +651,37 @@ function wrapPatchRoutesOnNavigation(
|
|
|
525
651
|
}
|
|
526
652
|
}
|
|
527
653
|
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
}
|
|
541
670
|
}
|
|
542
671
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
false, // forceUpdate = false since this is after lazy routes are loaded
|
|
549
|
-
_matchRoutes,
|
|
550
|
-
);
|
|
551
|
-
}
|
|
672
|
+
return result;
|
|
673
|
+
})();
|
|
674
|
+
|
|
675
|
+
if (activeRootSpan) {
|
|
676
|
+
trackLazyRouteLoad(activeRootSpan, lazyLoadPromise);
|
|
552
677
|
}
|
|
553
678
|
|
|
554
|
-
return
|
|
679
|
+
return lazyLoadPromise;
|
|
555
680
|
},
|
|
556
681
|
};
|
|
557
682
|
}
|
|
558
683
|
|
|
559
|
-
|
|
560
|
-
return `${location.pathname}${location.search}${location.hash}`;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function tryUpdateSpanName(
|
|
564
|
-
activeSpan,
|
|
565
|
-
currentSpanName,
|
|
566
|
-
newName,
|
|
567
|
-
newSource,
|
|
568
|
-
) {
|
|
569
|
-
// Check if the new name contains React Router parameter syntax (/:param/)
|
|
570
|
-
const isReactRouterParam = /\/:[a-zA-Z0-9_]+/.test(newName);
|
|
571
|
-
const isNewNameParameterized = newName !== currentSpanName && isReactRouterParam;
|
|
572
|
-
if (isNewNameParameterized) {
|
|
573
|
-
activeSpan.updateName(newName);
|
|
574
|
-
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, newSource );
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function isDuplicateNavigation(client, navigationKey) {
|
|
579
|
-
const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client);
|
|
580
|
-
return lastKey === navigationKey;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function createNavigationSpan(opts
|
|
584
|
-
|
|
585
|
-
) {
|
|
586
|
-
const { client, name, source, version, location, routes, basename, allRoutes, navigationKey } = opts;
|
|
587
|
-
|
|
588
|
-
const navigationSpan = startBrowserTracingNavigationSpan(client, {
|
|
589
|
-
name,
|
|
590
|
-
attributes: {
|
|
591
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source ,
|
|
592
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
|
|
593
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`,
|
|
594
|
-
},
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
if (navigationSpan) {
|
|
598
|
-
LAST_NAVIGATION_PER_CLIENT.set(client, navigationKey);
|
|
599
|
-
patchNavigationSpanEnd(navigationSpan, location, routes, basename, allRoutes);
|
|
600
|
-
|
|
601
|
-
const unsubscribe = client.on('spanEnd', endedSpan => {
|
|
602
|
-
if (endedSpan === navigationSpan) {
|
|
603
|
-
// Clear key only if it's still our key (handles overlapping navigations)
|
|
604
|
-
const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client);
|
|
605
|
-
if (lastKey === navigationKey) {
|
|
606
|
-
LAST_NAVIGATION_PER_CLIENT.delete(client);
|
|
607
|
-
}
|
|
608
|
-
unsubscribe(); // Prevent memory leak
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return navigationSpan;
|
|
614
|
-
}
|
|
615
|
-
|
|
684
|
+
// eslint-disable-next-line complexity
|
|
616
685
|
function handleNavigation(opts
|
|
617
686
|
|
|
618
687
|
) {
|
|
@@ -638,33 +707,84 @@ function handleNavigation(opts
|
|
|
638
707
|
basename,
|
|
639
708
|
);
|
|
640
709
|
|
|
641
|
-
const
|
|
642
|
-
const
|
|
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
|
+
}
|
|
643
746
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
764
|
+
attributes: {
|
|
765
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
|
|
766
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
|
|
767
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`,
|
|
768
|
+
},
|
|
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
|
+
}
|
|
649
775
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
createNavigationSpan({
|
|
658
|
-
client,
|
|
659
|
-
name,
|
|
660
|
-
source,
|
|
661
|
-
version,
|
|
662
|
-
location,
|
|
663
|
-
routes,
|
|
664
|
-
basename,
|
|
665
|
-
allRoutes,
|
|
666
|
-
navigationKey: currentNavigationKey,
|
|
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,
|
|
667
783
|
});
|
|
784
|
+
patchSpanEnd(navigationSpan, location, routes, basename, allRoutes, 'navigation');
|
|
785
|
+
} else {
|
|
786
|
+
// If no span was created, remove the placeholder
|
|
787
|
+
activeNavigationSpans.delete(client);
|
|
668
788
|
}
|
|
669
789
|
}
|
|
670
790
|
}
|
|
@@ -728,8 +848,90 @@ function updatePageloadTransaction({
|
|
|
728
848
|
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
729
849
|
|
|
730
850
|
// Patch span.end() to ensure we update the name one last time before the span is sent
|
|
731
|
-
|
|
851
|
+
patchSpanEnd(activeRootSpan, location, routes, basename, allRoutes, 'pageload');
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
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;
|
|
732
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}`);
|
|
733
935
|
}
|
|
734
936
|
}
|
|
735
937
|
|
|
@@ -752,71 +954,93 @@ function patchSpanEnd(
|
|
|
752
954
|
return;
|
|
753
955
|
}
|
|
754
956
|
|
|
957
|
+
// Use the passed route context, or fall back to global Set
|
|
958
|
+
const allRoutesSet = _allRoutes ? new Set(_allRoutes) : allRoutes;
|
|
959
|
+
|
|
755
960
|
const originalEnd = span.end.bind(span);
|
|
961
|
+
let endCalled = false;
|
|
756
962
|
|
|
757
963
|
span.end = function patchedEnd(...args) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
}
|
|
771
1006
|
|
|
772
|
-
|
|
773
|
-
|
|
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,
|
|
774
1020
|
location,
|
|
775
|
-
|
|
776
|
-
currentAllRoutes.length > 0 ? currentAllRoutes : routes,
|
|
777
|
-
branches,
|
|
1021
|
+
routes,
|
|
778
1022
|
basename,
|
|
1023
|
+
spanType,
|
|
1024
|
+
allRoutesSet,
|
|
779
1025
|
);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
} catch (error) {
|
|
789
|
-
// Silently catch errors to ensure span.end() is always called
|
|
790
|
-
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;
|
|
791
1034
|
}
|
|
792
1035
|
|
|
793
|
-
|
|
1036
|
+
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutesSet);
|
|
1037
|
+
cleanupNavigationSpan();
|
|
1038
|
+
originalEnd(endTimestamp);
|
|
794
1039
|
};
|
|
795
1040
|
|
|
796
|
-
// Mark this span as having its end() method patched to prevent duplicate patching
|
|
797
1041
|
addNonEnumerableProperty(span , patchedPropertyName, true);
|
|
798
1042
|
}
|
|
799
1043
|
|
|
800
|
-
function patchPageloadSpanEnd(
|
|
801
|
-
span,
|
|
802
|
-
location,
|
|
803
|
-
routes,
|
|
804
|
-
basename,
|
|
805
|
-
_allRoutes,
|
|
806
|
-
) {
|
|
807
|
-
patchSpanEnd(span, location, routes, basename, _allRoutes, 'pageload');
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function patchNavigationSpanEnd(
|
|
811
|
-
span,
|
|
812
|
-
location,
|
|
813
|
-
routes,
|
|
814
|
-
basename,
|
|
815
|
-
_allRoutes,
|
|
816
|
-
) {
|
|
817
|
-
patchSpanEnd(span, location, routes, basename, _allRoutes, 'navigation');
|
|
818
|
-
}
|
|
819
|
-
|
|
820
1044
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
821
1045
|
function createV6CompatibleWithSentryReactRouterRouting(
|
|
822
1046
|
Routes,
|
|
@@ -852,11 +1076,13 @@ function createV6CompatibleWithSentryReactRouterRouting(
|
|
|
852
1076
|
});
|
|
853
1077
|
isMountRenderPass.current = false;
|
|
854
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.
|
|
855
1082
|
handleNavigation({ location, routes, navigationType, version, allRoutes: Array.from(allRoutes) });
|
|
856
1083
|
}
|
|
857
1084
|
},
|
|
858
|
-
//
|
|
859
|
-
// 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
|
|
860
1086
|
[location, navigationType],
|
|
861
1087
|
);
|
|
862
1088
|
|
|
@@ -886,5 +1112,5 @@ function getActiveRootSpan() {
|
|
|
886
1112
|
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
|
|
887
1113
|
}
|
|
888
1114
|
|
|
889
|
-
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 };
|
|
890
1116
|
//# sourceMappingURL=instrumentation.js.map
|