Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
1295 lines
44 KiB
Plaintext
1295 lines
44 KiB
Plaintext
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
const browser = require('@sentry/browser');
|
|
const core = require('@sentry/core');
|
|
const React = require('react');
|
|
const debugBuild = require('../debug-build.js');
|
|
const hoistNonReactStatics = require('../hoist-non-react-statics.js');
|
|
const lazyRoutes = require('./lazy-routes.js');
|
|
const utils = require('./utils.js');
|
|
|
|
/* eslint-disable max-lines */
|
|
// Inspired from Donnie McNeal's solution:
|
|
// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536
|
|
|
|
|
|
let _useEffect;
|
|
let _useLocation;
|
|
let _useNavigationType;
|
|
let _createRoutesFromChildren;
|
|
let _matchRoutes;
|
|
|
|
let _enableAsyncRouteHandlers = false;
|
|
let _lazyRouteTimeout = 3000;
|
|
let _lazyRouteManifest;
|
|
let _basename = '';
|
|
|
|
const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet();
|
|
|
|
// Prevents duplicate spans when router.subscribe fires multiple times
|
|
const activeNavigationSpans = new WeakMap
|
|
|
|
();
|
|
|
|
// Exported for testing only
|
|
const allRoutes = new Set();
|
|
|
|
// Tracks lazy route loads to wait before finalizing span names
|
|
const pendingLazyRouteLoads = new WeakMap();
|
|
|
|
// Tracks deferred lazy route promises that can be resolved when patchRoutesOnNavigation is called
|
|
const deferredLazyRouteResolvers = new WeakMap();
|
|
|
|
/**
|
|
* Schedules a callback using requestAnimationFrame when available (browser),
|
|
* or falls back to setTimeout for SSR environments (Node.js, createMemoryRouter tests).
|
|
*/
|
|
function scheduleCallback(callback) {
|
|
if (browser.WINDOW?.requestAnimationFrame) {
|
|
return browser.WINDOW.requestAnimationFrame(callback);
|
|
}
|
|
return setTimeout(callback, 0) ;
|
|
}
|
|
|
|
/**
|
|
* Cancels a scheduled callback, handling both RAF (browser) and timeout (SSR) IDs.
|
|
*/
|
|
function cancelScheduledCallback(id) {
|
|
if (browser.WINDOW?.cancelAnimationFrame) {
|
|
browser.WINDOW.cancelAnimationFrame(id);
|
|
} else {
|
|
clearTimeout(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes location key for duplicate detection. Normalizes undefined/null to empty strings.
|
|
* Exported for testing.
|
|
*/
|
|
function computeLocationKey(location) {
|
|
return `${location.pathname}${location.search || ''}${location.hash || ''}`;
|
|
}
|
|
|
|
/**
|
|
* Checks if a route name is parameterized (contains route parameters like :id or wildcards like *)
|
|
* vs a raw URL path.
|
|
*/
|
|
function isParameterizedRoute(routeName) {
|
|
return routeName.includes(':') || routeName.includes('*');
|
|
}
|
|
|
|
/**
|
|
* Determines if a navigation should be skipped as a duplicate, and if an existing span should be updated.
|
|
* Exported for testing.
|
|
*
|
|
* @returns An object with:
|
|
* - skip: boolean - Whether to skip creating a new span
|
|
* - shouldUpdate: boolean - Whether to update the existing span name (wildcard upgrade)
|
|
*/
|
|
function shouldSkipNavigation(
|
|
trackedNav
|
|
|
|
,
|
|
locationKey,
|
|
proposedName,
|
|
spanHasEnded,
|
|
) {
|
|
if (!trackedNav) {
|
|
return { skip: false, shouldUpdate: false };
|
|
}
|
|
|
|
// Check if this is a duplicate navigation (same location)
|
|
// 1. If it's a placeholder, it's always a duplicate (we're waiting for the real one)
|
|
// 2. If it's a real span, it's a duplicate only if it hasn't ended yet
|
|
const isDuplicate = trackedNav.locationKey === locationKey && (trackedNav.isPlaceholder || !spanHasEnded);
|
|
|
|
if (isDuplicate) {
|
|
// Check if we should update the span name with a better route
|
|
// Allow updates if:
|
|
// 1. Current has wildcard and new doesn't (wildcard → parameterized upgrade)
|
|
// 2. Current is raw path and new is parameterized (raw → parameterized upgrade)
|
|
// 3. New name is different and more specific (longer, indicating nested routes resolved)
|
|
const currentHasWildcard = !!trackedNav.routeName && utils.transactionNameHasWildcard(trackedNav.routeName);
|
|
const proposedHasWildcard = utils.transactionNameHasWildcard(proposedName);
|
|
const currentIsParameterized = !!trackedNav.routeName && isParameterizedRoute(trackedNav.routeName);
|
|
const proposedIsParameterized = isParameterizedRoute(proposedName);
|
|
|
|
const isWildcardUpgrade = currentHasWildcard && !proposedHasWildcard;
|
|
const isRawToParameterized = !currentIsParameterized && proposedIsParameterized;
|
|
const isMoreSpecific =
|
|
proposedName !== trackedNav.routeName &&
|
|
proposedName.length > (trackedNav.routeName?.length || 0) &&
|
|
!proposedHasWildcard;
|
|
|
|
const shouldUpdate = !!(trackedNav.routeName && (isWildcardUpgrade || isRawToParameterized || isMoreSpecific));
|
|
|
|
return { skip: true, shouldUpdate };
|
|
}
|
|
|
|
return { skip: false, shouldUpdate: false };
|
|
}
|
|
|
|
function addResolvedRoutesToParent(resolvedRoutes, parentRoute) {
|
|
const existingChildren = parentRoute.children || [];
|
|
|
|
const newRoutes = resolvedRoutes.filter(
|
|
newRoute =>
|
|
!existingChildren.some(
|
|
existing =>
|
|
existing === newRoute ||
|
|
(newRoute.path && existing.path === newRoute.path) ||
|
|
(newRoute.id && existing.id === newRoute.id),
|
|
),
|
|
);
|
|
|
|
if (newRoutes.length > 0) {
|
|
parentRoute.children = [...existingChildren, ...newRoutes];
|
|
}
|
|
}
|
|
|
|
/** Registers a pending lazy route load promise for a span. */
|
|
function trackLazyRouteLoad(span, promise) {
|
|
let promises = pendingLazyRouteLoads.get(span);
|
|
if (!promises) {
|
|
promises = new Set();
|
|
pendingLazyRouteLoads.set(span, promises);
|
|
}
|
|
promises.add(promise);
|
|
|
|
// Clean up when promise resolves/rejects
|
|
promise.finally(() => {
|
|
const currentPromises = pendingLazyRouteLoads.get(span);
|
|
if (currentPromises) {
|
|
currentPromises.delete(promise);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a deferred promise for a span that will be resolved when patchRoutesOnNavigation is called.
|
|
* This ensures that patchedEnd waits for patchRoutesOnNavigation to be called before ending the span.
|
|
*/
|
|
function createDeferredLazyRoutePromise(span) {
|
|
const deferredPromise = new Promise(resolve => {
|
|
deferredLazyRouteResolvers.set(span, resolve);
|
|
});
|
|
|
|
trackLazyRouteLoad(span, deferredPromise);
|
|
}
|
|
|
|
/**
|
|
* Resolves the deferred lazy route promise for a span.
|
|
* Called when patchRoutesOnNavigation is invoked.
|
|
*/
|
|
function resolveDeferredLazyRoutePromise(span) {
|
|
const resolver = deferredLazyRouteResolvers.get(span);
|
|
if (resolver) {
|
|
resolver();
|
|
deferredLazyRouteResolvers.delete(span);
|
|
// Clear the flag so patchSpanEnd doesn't wait unnecessarily for routes that have already loaded
|
|
if ((span ).__sentry_may_have_lazy_routes__) {
|
|
(span ).__sentry_may_have_lazy_routes__ = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
|
|
* When capturedSpan is provided, updates that specific span instead of the current active span.
|
|
* This prevents race conditions where a lazy handler resolves after the user has navigated away.
|
|
*/
|
|
function processResolvedRoutes(
|
|
resolvedRoutes,
|
|
parentRoute,
|
|
currentLocation = null,
|
|
capturedSpan,
|
|
) {
|
|
resolvedRoutes.forEach(child => {
|
|
allRoutes.add(child);
|
|
// Only check for async handlers if the feature is enabled
|
|
if (_enableAsyncRouteHandlers) {
|
|
lazyRoutes.checkRouteForAsyncHandler(child, processResolvedRoutes);
|
|
}
|
|
});
|
|
|
|
if (parentRoute) {
|
|
// If a parent route is provided, add the resolved routes as children to the parent route
|
|
addResolvedRoutesToParent(resolvedRoutes, parentRoute);
|
|
}
|
|
|
|
// Use captured span if provided, otherwise fall back to current active span
|
|
const targetSpan = capturedSpan ?? utils.getActiveRootSpan();
|
|
if (targetSpan) {
|
|
const spanJson = core.spanToJSON(targetSpan);
|
|
|
|
// Skip update if span has already ended (timestamp is set when span.end() is called)
|
|
if (spanJson.timestamp) {
|
|
debugBuild.DEBUG_BUILD && core.debug.warn('[React Router] Lazy handler resolved after span ended - skipping update');
|
|
return;
|
|
}
|
|
|
|
const spanOp = spanJson.op;
|
|
|
|
// Use captured location for route matching (ensures we match against the correct route)
|
|
// Fall back to window.location only if no captured location and no captured span
|
|
// (i.e., this is not from an async handler)
|
|
let location = currentLocation;
|
|
if (!location && !capturedSpan) {
|
|
if (typeof browser.WINDOW !== 'undefined') {
|
|
const globalLocation = browser.WINDOW.location;
|
|
if (globalLocation?.pathname) {
|
|
location = { pathname: globalLocation.pathname };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (location) {
|
|
if (spanOp === 'pageload') {
|
|
// Re-run the pageload transaction update with the newly loaded routes
|
|
updatePageloadTransaction({
|
|
activeRootSpan: targetSpan,
|
|
location: { pathname: location.pathname },
|
|
routes: Array.from(allRoutes),
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
} else if (spanOp === 'navigation') {
|
|
// For navigation spans, update the name with the newly loaded routes
|
|
updateNavigationSpan(targetSpan, location, Array.from(allRoutes), false, _matchRoutes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a navigation span with the correct route name after lazy routes have been loaded.
|
|
*/
|
|
function updateNavigationSpan(
|
|
activeRootSpan,
|
|
location,
|
|
allRoutes,
|
|
forceUpdate = false,
|
|
matchRoutes,
|
|
) {
|
|
const spanJson = core.spanToJSON(activeRootSpan);
|
|
const currentName = spanJson.description;
|
|
|
|
const hasBeenNamed = (activeRootSpan )?.__sentry_navigation_name_set__;
|
|
const currentNameHasWildcard = currentName && utils.transactionNameHasWildcard(currentName);
|
|
const shouldUpdate = !hasBeenNamed || forceUpdate || currentNameHasWildcard;
|
|
|
|
if (shouldUpdate && !spanJson.timestamp) {
|
|
const currentBranches = matchRoutes(allRoutes, location);
|
|
const [name, source] = utils.resolveRouteNameAndSource(
|
|
location,
|
|
allRoutes,
|
|
allRoutes,
|
|
(currentBranches ) || [],
|
|
_basename,
|
|
_lazyRouteManifest,
|
|
_enableAsyncRouteHandlers,
|
|
);
|
|
|
|
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
const isImprovement =
|
|
name &&
|
|
(!currentName || // No current name - always set
|
|
(!hasBeenNamed && (currentSource !== 'route' || source === 'route')) || // Not finalized - allow unless downgrading route→url
|
|
(currentSource !== 'route' && source === 'route') || // URL → route upgrade
|
|
(currentSource === 'route' && source === 'route' && currentNameHasWildcard)); // Route → better route (only if current has wildcard)
|
|
if (isImprovement) {
|
|
activeRootSpan.updateName(name);
|
|
activeRootSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
|
|
// Only mark as finalized for non-wildcard route names (allows URL→route upgrades).
|
|
if (!utils.transactionNameHasWildcard(name) && source === 'route') {
|
|
core.addNonEnumerableProperty(
|
|
activeRootSpan ,
|
|
'__sentry_navigation_name_set__',
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupRouterSubscription(
|
|
router,
|
|
routes,
|
|
version,
|
|
basename,
|
|
activeRootSpan,
|
|
) {
|
|
let isInitialPageloadComplete = false;
|
|
let hasSeenPageloadSpan = !!activeRootSpan && core.spanToJSON(activeRootSpan).op === 'pageload';
|
|
let hasSeenPopAfterPageload = false;
|
|
let scheduledNavigationHandler = null;
|
|
let lastHandledPathname = null;
|
|
|
|
router.subscribe((state) => {
|
|
if (!isInitialPageloadComplete) {
|
|
const currentRootSpan = utils.getActiveRootSpan();
|
|
const isCurrentlyInPageload = currentRootSpan && core.spanToJSON(currentRootSpan).op === 'pageload';
|
|
|
|
if (isCurrentlyInPageload) {
|
|
hasSeenPageloadSpan = true;
|
|
} else if (hasSeenPageloadSpan) {
|
|
if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) {
|
|
hasSeenPopAfterPageload = true;
|
|
} else {
|
|
isInitialPageloadComplete = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const shouldHandleNavigation =
|
|
state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete);
|
|
|
|
if (shouldHandleNavigation) {
|
|
// Include search and hash to allow query/hash-only navigations
|
|
// Use computeLocationKey() to ensure undefined/null values are normalized to empty strings
|
|
const currentLocationKey = computeLocationKey(state.location);
|
|
const navigationHandler = () => {
|
|
// Prevent multiple calls for the same location within the same navigation cycle
|
|
if (lastHandledPathname === currentLocationKey) {
|
|
return;
|
|
}
|
|
lastHandledPathname = currentLocationKey;
|
|
scheduledNavigationHandler = null;
|
|
handleNavigation({
|
|
location: state.location,
|
|
routes,
|
|
navigationType: state.historyAction,
|
|
version,
|
|
basename,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
};
|
|
|
|
if (state.navigation.state !== 'idle') {
|
|
// Navigation in progress - reset if location changed
|
|
if (lastHandledPathname !== currentLocationKey) {
|
|
lastHandledPathname = null;
|
|
}
|
|
// Cancel any previously scheduled handler to avoid duplicates
|
|
if (scheduledNavigationHandler !== null) {
|
|
cancelScheduledCallback(scheduledNavigationHandler);
|
|
}
|
|
scheduledNavigationHandler = scheduleCallback(navigationHandler);
|
|
} else {
|
|
// Navigation completed - cancel scheduled handler if any, then call immediately
|
|
if (scheduledNavigationHandler !== null) {
|
|
cancelScheduledCallback(scheduledNavigationHandler);
|
|
scheduledNavigationHandler = null;
|
|
}
|
|
navigationHandler();
|
|
// Don't reset - next navigation cycle resets to prevent duplicates within same cycle.
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions.
|
|
*/
|
|
function createV6CompatibleWrapCreateBrowserRouter
|
|
|
|
(
|
|
createRouterFunction,
|
|
version,
|
|
) {
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn(
|
|
`reactRouterV${version}Instrumentation was unable to wrap the \`createRouter\` function because of one or more missing parameters.`,
|
|
);
|
|
|
|
return createRouterFunction;
|
|
}
|
|
|
|
return function (routes, opts) {
|
|
addRoutesToAllRoutes(routes);
|
|
|
|
if (_enableAsyncRouteHandlers) {
|
|
for (const route of routes) {
|
|
lazyRoutes.checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
}
|
|
}
|
|
|
|
// Capture the active span BEFORE creating the router.
|
|
// This is important because the span might end (due to idle timeout) before
|
|
// patchRoutesOnNavigation is called by React Router.
|
|
const activeRootSpan = utils.getActiveRootSpan();
|
|
|
|
// If patchRoutesOnNavigation is provided and we have an active span,
|
|
// mark the span as having potential lazy routes and create a deferred promise.
|
|
const hasPatchRoutesOnNavigation =
|
|
opts && 'patchRoutesOnNavigation' in opts && typeof opts.patchRoutesOnNavigation === 'function';
|
|
if (hasPatchRoutesOnNavigation && activeRootSpan) {
|
|
// Mark the span as potentially having lazy routes
|
|
core.addNonEnumerableProperty(
|
|
activeRootSpan ,
|
|
'__sentry_may_have_lazy_routes__',
|
|
true,
|
|
);
|
|
createDeferredLazyRoutePromise(activeRootSpan);
|
|
}
|
|
|
|
// Pass the captured span to wrapPatchRoutesOnNavigation so it uses the same span
|
|
// even if the span has ended by the time patchRoutesOnNavigation is called.
|
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, false, activeRootSpan);
|
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
const basename = opts?.basename;
|
|
|
|
if (router.state.historyAction === 'POP' && activeRootSpan) {
|
|
updatePageloadTransaction({
|
|
activeRootSpan,
|
|
location: router.state.location,
|
|
routes,
|
|
basename,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
}
|
|
|
|
// Store basename for use in updateNavigationSpan
|
|
_basename = basename || '';
|
|
|
|
setupRouterSubscription(router, routes, version, basename, activeRootSpan);
|
|
|
|
return router;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a wrapCreateMemoryRouter function that can be used with all React Router v6 compatible versions.
|
|
*/
|
|
function createV6CompatibleWrapCreateMemoryRouter
|
|
|
|
(
|
|
createRouterFunction,
|
|
version,
|
|
) {
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn(
|
|
`reactRouterV${version}Instrumentation was unable to wrap the \`createMemoryRouter\` function because of one or more missing parameters.`,
|
|
);
|
|
|
|
return createRouterFunction;
|
|
}
|
|
|
|
return function (
|
|
routes,
|
|
opts
|
|
|
|
,
|
|
) {
|
|
addRoutesToAllRoutes(routes);
|
|
|
|
if (_enableAsyncRouteHandlers) {
|
|
for (const route of routes) {
|
|
lazyRoutes.checkRouteForAsyncHandler(route, processResolvedRoutes);
|
|
}
|
|
}
|
|
|
|
// Capture the active span BEFORE creating the router (same as browser router)
|
|
const memoryActiveRootSpanEarly = utils.getActiveRootSpan();
|
|
|
|
// If patchRoutesOnNavigation is provided and we have an active span,
|
|
// mark the span as having potential lazy routes and create a deferred promise.
|
|
const hasPatchRoutesOnNavigation =
|
|
opts && 'patchRoutesOnNavigation' in opts && typeof opts.patchRoutesOnNavigation === 'function';
|
|
if (hasPatchRoutesOnNavigation && memoryActiveRootSpanEarly) {
|
|
core.addNonEnumerableProperty(
|
|
memoryActiveRootSpanEarly ,
|
|
'__sentry_may_have_lazy_routes__',
|
|
true,
|
|
);
|
|
createDeferredLazyRoutePromise(memoryActiveRootSpanEarly);
|
|
}
|
|
|
|
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true, memoryActiveRootSpanEarly);
|
|
|
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
const basename = opts?.basename;
|
|
|
|
let initialEntry = undefined;
|
|
|
|
const initialEntries = opts?.initialEntries;
|
|
const initialIndex = opts?.initialIndex;
|
|
|
|
const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1;
|
|
const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex];
|
|
|
|
initialEntry = hasOnlyOneInitialEntry
|
|
? initialEntries[0]
|
|
: hasIndexedEntry
|
|
? initialEntries[initialIndex]
|
|
: undefined;
|
|
|
|
const location = initialEntry
|
|
? typeof initialEntry === 'string'
|
|
? { pathname: initialEntry }
|
|
: initialEntry
|
|
: router.state.location;
|
|
|
|
const memoryActiveRootSpan = utils.getActiveRootSpan();
|
|
|
|
if (router.state.historyAction === 'POP' && memoryActiveRootSpan) {
|
|
updatePageloadTransaction({
|
|
activeRootSpan: memoryActiveRootSpan,
|
|
location,
|
|
routes,
|
|
basename,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
}
|
|
|
|
// Store basename for use in updateNavigationSpan
|
|
_basename = basename || '';
|
|
|
|
setupRouterSubscription(router, routes, version, basename, memoryActiveRootSpan);
|
|
|
|
return router;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a browser tracing integration that can be used with all React Router v6 compatible versions.
|
|
*/
|
|
function createReactRouterV6CompatibleTracingIntegration(
|
|
options,
|
|
version,
|
|
) {
|
|
const integration = browser.browserTracingIntegration({ ...options, instrumentPageLoad: false, instrumentNavigation: false });
|
|
|
|
const {
|
|
useEffect,
|
|
useLocation,
|
|
useNavigationType,
|
|
createRoutesFromChildren,
|
|
matchRoutes,
|
|
stripBasename,
|
|
enableAsyncRouteHandlers = false,
|
|
instrumentPageLoad = true,
|
|
instrumentNavigation = true,
|
|
lazyRouteTimeout,
|
|
lazyRouteManifest,
|
|
} = options;
|
|
|
|
return {
|
|
...integration,
|
|
setup(client) {
|
|
integration.setup(client);
|
|
|
|
const finalTimeout = options.finalTimeout ?? 30000;
|
|
const defaultMaxWait = (options.idleTimeout ?? 1000) * 3;
|
|
const configuredMaxWait = lazyRouteTimeout ?? defaultMaxWait;
|
|
|
|
// Cap Infinity at finalTimeout to prevent indefinite hangs
|
|
if (configuredMaxWait === Infinity) {
|
|
_lazyRouteTimeout = finalTimeout;
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.log(
|
|
'[React Router] lazyRouteTimeout set to Infinity, capping at finalTimeout:',
|
|
finalTimeout,
|
|
'ms to prevent indefinite hangs',
|
|
);
|
|
} else if (Number.isNaN(configuredMaxWait)) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn('[React Router] lazyRouteTimeout must be a number, falling back to default:', defaultMaxWait);
|
|
_lazyRouteTimeout = defaultMaxWait;
|
|
} else if (configuredMaxWait < 0) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn(
|
|
'[React Router] lazyRouteTimeout must be non-negative or Infinity, got:',
|
|
configuredMaxWait,
|
|
'falling back to:',
|
|
defaultMaxWait,
|
|
);
|
|
_lazyRouteTimeout = defaultMaxWait;
|
|
} else {
|
|
_lazyRouteTimeout = configuredMaxWait;
|
|
}
|
|
|
|
_useEffect = useEffect;
|
|
_useLocation = useLocation;
|
|
_useNavigationType = useNavigationType;
|
|
_matchRoutes = matchRoutes;
|
|
_createRoutesFromChildren = createRoutesFromChildren;
|
|
_enableAsyncRouteHandlers = enableAsyncRouteHandlers;
|
|
_lazyRouteManifest = lazyRouteManifest;
|
|
|
|
// Initialize the router utils with the required dependencies
|
|
utils.initializeRouterUtils(matchRoutes, stripBasename || false);
|
|
},
|
|
afterAllSetup(client) {
|
|
integration.afterAllSetup(client);
|
|
|
|
const initPathName = browser.WINDOW.location?.pathname;
|
|
if (instrumentPageLoad && initPathName) {
|
|
browser.startBrowserTracingPageLoadSpan(client, {
|
|
name: initPathName,
|
|
attributes: {
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.reactrouter_v${version}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (instrumentNavigation) {
|
|
CLIENTS_WITH_INSTRUMENT_NAVIGATION.add(client);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function createV6CompatibleWrapUseRoutes(origUseRoutes, version) {
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn(
|
|
'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.',
|
|
);
|
|
|
|
return origUseRoutes;
|
|
}
|
|
|
|
const SentryRoutes
|
|
|
|
= (props) => {
|
|
const isMountRenderPass = React.useRef(true);
|
|
const { routes, locationArg } = props;
|
|
|
|
const Routes = origUseRoutes(routes, locationArg);
|
|
|
|
const location = _useLocation();
|
|
const navigationType = _useNavigationType();
|
|
|
|
// A value with stable identity to either pick `locationArg` if available or `location` if not
|
|
const stableLocationParam =
|
|
typeof locationArg === 'string' || locationArg?.pathname ? (locationArg ) : location;
|
|
|
|
_useEffect(() => {
|
|
const normalizedLocation =
|
|
typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam;
|
|
|
|
if (isMountRenderPass.current) {
|
|
addRoutesToAllRoutes(routes);
|
|
|
|
updatePageloadTransaction({
|
|
activeRootSpan: utils.getActiveRootSpan(),
|
|
location: normalizedLocation,
|
|
routes,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
isMountRenderPass.current = false;
|
|
} else {
|
|
// Note: Component-based routes don't support lazy route tracking via lazyRouteTimeout
|
|
// because React.lazy() loads happen at the component level, not the router level.
|
|
// Use createBrowserRouter with patchRoutesOnNavigation for lazy route tracking.
|
|
handleNavigation({
|
|
location: normalizedLocation,
|
|
routes,
|
|
navigationType,
|
|
version,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
}
|
|
}, [navigationType, stableLocationParam]);
|
|
|
|
return Routes;
|
|
};
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
return (routes, locationArg) => {
|
|
return React.createElement(SentryRoutes, { routes: routes, locationArg: locationArg,} );
|
|
};
|
|
}
|
|
function wrapPatchRoutesOnNavigation(
|
|
opts,
|
|
isMemoryRouter = false,
|
|
capturedSpan,
|
|
) {
|
|
if (!opts || !('patchRoutesOnNavigation' in opts) || typeof opts.patchRoutesOnNavigation !== 'function') {
|
|
return opts || {};
|
|
}
|
|
|
|
const originalPatchRoutes = opts.patchRoutesOnNavigation;
|
|
return {
|
|
...opts,
|
|
patchRoutesOnNavigation: async (args) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
const targetPath = (args )?.path;
|
|
|
|
// Use current active span if available, otherwise fall back to captured span (from router creation time).
|
|
// This ensures navigation spans use their own span (not the stale pageload span), while still
|
|
// supporting pageload spans that may have ended before patchRoutesOnNavigation is called.
|
|
const activeRootSpan = utils.getActiveRootSpan() ?? capturedSpan;
|
|
|
|
if (!isMemoryRouter) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
const originalPatch = (args )?.patch;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
const matches = (args )?.matches ;
|
|
if (originalPatch) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
(args ).patch = (routeId, children) => {
|
|
addRoutesToAllRoutes(children);
|
|
|
|
// Find the parent route from matches and attach children to it in allRoutes.
|
|
// React Router's patch attaches children to its internal route copies, but we need
|
|
// to update the route objects in our allRoutes Set for proper route matching.
|
|
if (matches && matches.length > 0) {
|
|
const leafMatch = matches[matches.length - 1];
|
|
const leafRoute = leafMatch?.route;
|
|
if (leafRoute) {
|
|
// Find the matching route in allRoutes by id, reference, or path
|
|
const matchingRoute = Array.from(allRoutes).find(route => {
|
|
const idMatches = route.id !== undefined && route.id === routeId;
|
|
const referenceMatches = route === leafRoute;
|
|
const pathMatches =
|
|
route.path !== undefined && leafRoute.path !== undefined && route.path === leafRoute.path;
|
|
|
|
return idMatches || referenceMatches || pathMatches;
|
|
});
|
|
|
|
if (matchingRoute) {
|
|
addResolvedRoutesToParent(children, matchingRoute);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the captured activeRootSpan instead of getActiveRootSpan() to avoid race conditions
|
|
// where user navigates away during lazy route loading and we'd update the wrong span
|
|
const spanJson = activeRootSpan ? core.spanToJSON(activeRootSpan) : undefined;
|
|
// Only update if we have a valid targetPath (patchRoutesOnNavigation can be called without path),
|
|
// the captured span exists, hasn't ended, and is a navigation span
|
|
if (
|
|
targetPath &&
|
|
activeRootSpan &&
|
|
spanJson &&
|
|
!spanJson.timestamp && // Span hasn't ended yet
|
|
spanJson.op === 'navigation'
|
|
) {
|
|
updateNavigationSpan(
|
|
activeRootSpan,
|
|
{ pathname: targetPath, search: '', hash: '', state: null, key: 'default' },
|
|
Array.from(allRoutes),
|
|
true,
|
|
_matchRoutes,
|
|
);
|
|
}
|
|
return originalPatch(routeId, children);
|
|
};
|
|
}
|
|
}
|
|
|
|
const lazyLoadPromise = (async () => {
|
|
// Set context so async handlers can access correct targetPath and span
|
|
const contextToken = utils.setNavigationContext(targetPath, activeRootSpan);
|
|
let result;
|
|
try {
|
|
result = await originalPatchRoutes(args);
|
|
} finally {
|
|
utils.clearNavigationContext(contextToken);
|
|
// Resolve the deferred promise now that patchRoutesOnNavigation has completed.
|
|
// This ensures patchedEnd has waited long enough for the lazy routes to load.
|
|
if (activeRootSpan) {
|
|
resolveDeferredLazyRoutePromise(activeRootSpan);
|
|
}
|
|
}
|
|
|
|
// Use the captured activeRootSpan instead of getActiveRootSpan() to avoid race conditions
|
|
// where user navigates away during lazy route loading and we'd update the wrong span
|
|
const spanJson = activeRootSpan ? core.spanToJSON(activeRootSpan) : undefined;
|
|
if (
|
|
activeRootSpan &&
|
|
spanJson &&
|
|
!spanJson.timestamp && // Span hasn't ended yet
|
|
spanJson.op === 'navigation'
|
|
) {
|
|
// Use targetPath consistently - don't fall back to WINDOW.location which may have changed
|
|
// if the user navigated away during async loading
|
|
const pathname = targetPath;
|
|
|
|
if (pathname) {
|
|
updateNavigationSpan(
|
|
activeRootSpan,
|
|
{ pathname, search: '', hash: '', state: null, key: 'default' },
|
|
Array.from(allRoutes),
|
|
false,
|
|
_matchRoutes,
|
|
);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
})();
|
|
|
|
if (activeRootSpan) {
|
|
trackLazyRouteLoad(activeRootSpan, lazyLoadPromise);
|
|
}
|
|
|
|
return lazyLoadPromise;
|
|
},
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
function handleNavigation(opts
|
|
|
|
) {
|
|
const { location, routes, navigationType, version, matches, basename, allRoutes } = opts;
|
|
const branches = Array.isArray(matches) ? matches : _matchRoutes(allRoutes || routes, location, basename);
|
|
|
|
const client = core.getClient();
|
|
if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) {
|
|
return;
|
|
}
|
|
|
|
const activeRootSpan = utils.getActiveRootSpan();
|
|
if (activeRootSpan && core.spanToJSON(activeRootSpan).op === 'pageload' && navigationType === 'POP') {
|
|
return;
|
|
}
|
|
|
|
if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) {
|
|
const [name, source] = utils.resolveRouteNameAndSource(
|
|
location,
|
|
allRoutes || routes,
|
|
allRoutes || routes,
|
|
branches ,
|
|
basename,
|
|
_lazyRouteManifest,
|
|
_enableAsyncRouteHandlers,
|
|
);
|
|
|
|
const locationKey = computeLocationKey(location);
|
|
const trackedNav = activeNavigationSpans.get(client);
|
|
|
|
// Determine if this navigation should be skipped as a duplicate
|
|
const trackedSpanHasEnded =
|
|
trackedNav && !trackedNav.isPlaceholder ? !!core.spanToJSON(trackedNav.span).timestamp : false;
|
|
const { skip, shouldUpdate } = shouldSkipNavigation(trackedNav, locationKey, name, trackedSpanHasEnded);
|
|
|
|
if (skip) {
|
|
if (shouldUpdate && trackedNav) {
|
|
const oldName = trackedNav.routeName;
|
|
|
|
if (trackedNav.isPlaceholder) {
|
|
// Update placeholder's route name - the real span will be created with this name
|
|
trackedNav.routeName = name;
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.log(
|
|
`[Tracing] Updated placeholder navigation name from "${oldName}" to "${name}" (will apply to real span)`,
|
|
);
|
|
} else {
|
|
// Update existing real span from wildcard to parameterized route name
|
|
trackedNav.span.updateName(name);
|
|
trackedNav.span.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source );
|
|
core.addNonEnumerableProperty(
|
|
trackedNav.span ,
|
|
'__sentry_navigation_name_set__',
|
|
true,
|
|
);
|
|
trackedNav.routeName = name;
|
|
debugBuild.DEBUG_BUILD && core.debug.log(`[Tracing] Updated navigation span name from "${oldName}" to "${name}"`);
|
|
}
|
|
} else {
|
|
debugBuild.DEBUG_BUILD && core.debug.log(`[Tracing] Skipping duplicate navigation for location: ${locationKey}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Create new navigation span (first navigation or legitimate new navigation)
|
|
// Reserve the spot in the map first to prevent race conditions
|
|
// Mark as placeholder to prevent concurrent handleNavigation calls from creating duplicates
|
|
const placeholderSpan = { end: () => {} } ;
|
|
const placeholderEntry = {
|
|
span: placeholderSpan,
|
|
routeName: name,
|
|
pathname: location.pathname,
|
|
locationKey,
|
|
isPlaceholder: true ,
|
|
};
|
|
activeNavigationSpans.set(client, placeholderEntry);
|
|
|
|
let navigationSpan;
|
|
try {
|
|
navigationSpan = browser.startBrowserTracingNavigationSpan(client, {
|
|
name: placeholderEntry.routeName, // Use placeholder's routeName in case it was updated
|
|
attributes: {
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
|
|
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
// If span creation fails, remove the placeholder so we don't block future navigations
|
|
activeNavigationSpans.delete(client);
|
|
throw e;
|
|
}
|
|
|
|
if (navigationSpan) {
|
|
// Update the map with the real span (isPlaceholder omitted, defaults to false)
|
|
activeNavigationSpans.set(client, {
|
|
span: navigationSpan,
|
|
routeName: placeholderEntry.routeName, // Use the (potentially updated) placeholder routeName
|
|
pathname: location.pathname,
|
|
locationKey,
|
|
});
|
|
patchSpanEnd(navigationSpan, location, routes, basename, 'navigation');
|
|
} else {
|
|
// If no span was created, remove the placeholder
|
|
activeNavigationSpans.delete(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Only exported for testing purposes */
|
|
function addRoutesToAllRoutes(routes) {
|
|
routes.forEach(route => {
|
|
const extractedChildRoutes = getChildRoutesRecursively(route);
|
|
|
|
extractedChildRoutes.forEach(r => {
|
|
allRoutes.add(r);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getChildRoutesRecursively(route, allRoutes = new Set()) {
|
|
if (!allRoutes.has(route)) {
|
|
allRoutes.add(route);
|
|
|
|
if (route.children && !route.index) {
|
|
route.children.forEach(child => {
|
|
const childRoutes = getChildRoutesRecursively(child, allRoutes);
|
|
|
|
childRoutes.forEach(r => {
|
|
allRoutes.add(r);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
return allRoutes;
|
|
}
|
|
|
|
function updatePageloadTransaction({
|
|
activeRootSpan,
|
|
location,
|
|
routes,
|
|
matches,
|
|
basename,
|
|
allRoutes,
|
|
}
|
|
|
|
) {
|
|
const branches = Array.isArray(matches)
|
|
? matches
|
|
: (_matchRoutes(allRoutes || routes, location, basename) );
|
|
|
|
if (branches) {
|
|
const [name, source] = utils.resolveRouteNameAndSource(
|
|
location,
|
|
allRoutes || routes,
|
|
allRoutes || routes,
|
|
branches,
|
|
basename,
|
|
_lazyRouteManifest,
|
|
_enableAsyncRouteHandlers,
|
|
);
|
|
|
|
core.getCurrentScope().setTransactionName(name || '/');
|
|
|
|
if (activeRootSpan) {
|
|
activeRootSpan.updateName(name);
|
|
activeRootSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
|
|
// Patch span.end() to ensure we update the name one last time before the span is sent
|
|
patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload');
|
|
}
|
|
} else if (activeRootSpan) {
|
|
// Even if branches is null (can happen when lazy routes haven't loaded yet),
|
|
// we still need to patch span.end() so that when lazy routes load and the span ends,
|
|
// we can update the transaction name correctly.
|
|
patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if a span name should be updated during wildcard route resolution.
|
|
*
|
|
* Update conditions (in priority order):
|
|
* 1. No current name + allowNoCurrentName: true → always update (pageload spans)
|
|
* 2. Current name has wildcard + new is route without wildcard → upgrade (e.g., "/users/*" → "/users/:id")
|
|
* 3. Current source is not 'route' + new source is 'route' → upgrade (e.g., URL → parameterized route)
|
|
*
|
|
* @param currentName - The current span name (may be undefined)
|
|
* @param currentSource - The current span source ('route', 'url', or undefined)
|
|
* @param newName - The proposed new span name
|
|
* @param newSource - The proposed new span source
|
|
* @param allowNoCurrentName - If true, allow updates when there's no current name (for pageload spans)
|
|
* @returns true if the span name should be updated
|
|
*/
|
|
function shouldUpdateWildcardSpanName(
|
|
currentName,
|
|
currentSource,
|
|
newName,
|
|
newSource,
|
|
allowNoCurrentName = false,
|
|
) {
|
|
if (!newName) {
|
|
return false;
|
|
}
|
|
|
|
if (!currentName && allowNoCurrentName) {
|
|
return true;
|
|
}
|
|
|
|
const hasWildcard = currentName && utils.transactionNameHasWildcard(currentName);
|
|
|
|
if (hasWildcard && newSource === 'route' && !utils.transactionNameHasWildcard(newName)) {
|
|
return true;
|
|
}
|
|
|
|
if (currentSource !== 'route' && newSource === 'route') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function tryUpdateSpanNameBeforeEnd(
|
|
span,
|
|
spanJson,
|
|
currentName,
|
|
location,
|
|
routes,
|
|
basename,
|
|
spanType,
|
|
allRoutes,
|
|
) {
|
|
try {
|
|
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
|
|
if (currentSource === 'route' && currentName && !utils.transactionNameHasWildcard(currentName)) {
|
|
return;
|
|
}
|
|
|
|
const currentAllRoutes = Array.from(allRoutes);
|
|
const routesToUse = currentAllRoutes.length > 0 ? currentAllRoutes : routes;
|
|
const branches = _matchRoutes(routesToUse, location, basename) ;
|
|
|
|
if (!branches) {
|
|
return;
|
|
}
|
|
|
|
const [name, source] = utils.resolveRouteNameAndSource(
|
|
location,
|
|
routesToUse,
|
|
routesToUse,
|
|
branches,
|
|
basename,
|
|
_lazyRouteManifest,
|
|
_enableAsyncRouteHandlers,
|
|
);
|
|
|
|
const isImprovement = shouldUpdateWildcardSpanName(currentName, currentSource, name, source, true);
|
|
const spanNotEnded = spanType === 'pageload' || !spanJson.timestamp;
|
|
|
|
if (isImprovement && spanNotEnded) {
|
|
span.updateName(name);
|
|
span.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
}
|
|
} catch (error) {
|
|
debugBuild.DEBUG_BUILD && core.debug.warn(`Error updating span details before ending: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Patches the span.end() method to update the transaction name one last time before the span is sent.
|
|
* This handles cases where the span is cancelled early (e.g., document.hidden) before lazy routes have finished loading.
|
|
*/
|
|
function patchSpanEnd(
|
|
span,
|
|
location,
|
|
routes,
|
|
basename,
|
|
spanType,
|
|
) {
|
|
const patchedPropertyName = `__sentry_${spanType}_end_patched__` ;
|
|
const hasEndBeenPatched = (span )?.[patchedPropertyName];
|
|
|
|
if (hasEndBeenPatched || !span.end) {
|
|
return;
|
|
}
|
|
|
|
// Uses global allRoutes to access lazy-loaded routes added after this function was called.
|
|
|
|
const originalEnd = span.end.bind(span);
|
|
let endCalled = false;
|
|
|
|
span.end = function patchedEnd(...args) {
|
|
if (endCalled) {
|
|
return;
|
|
}
|
|
endCalled = true;
|
|
|
|
// Capture timestamp immediately to avoid delay from async operations
|
|
// If no timestamp was provided, capture the current time now
|
|
const endTimestamp = args.length > 0 ? args[0] : Date.now() / 1000;
|
|
|
|
const spanJson = core.spanToJSON(span);
|
|
const currentName = spanJson.description;
|
|
const currentSource = spanJson.data?.[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
|
|
|
|
// Helper to clean up activeNavigationSpans after span ends
|
|
const cleanupNavigationSpan = () => {
|
|
const client = core.getClient();
|
|
if (client && spanType === 'navigation') {
|
|
const trackedNav = activeNavigationSpans.get(client);
|
|
if (trackedNav && trackedNav.span === span) {
|
|
activeNavigationSpans.delete(client);
|
|
}
|
|
}
|
|
};
|
|
|
|
const pendingPromises = pendingLazyRouteLoads.get(span);
|
|
const mayHaveLazyRoutes = (span ).__sentry_may_have_lazy_routes__;
|
|
|
|
// Wait for lazy routes if:
|
|
// 1. (There are pending promises OR the span was marked as potentially having lazy routes) AND
|
|
// 2. Current name exists AND
|
|
// 3. Either the name has a wildcard OR the source is not 'route' (URL-based names)
|
|
const hasPendingOrMayHaveLazyRoutes = (pendingPromises && pendingPromises.size > 0) || mayHaveLazyRoutes;
|
|
const shouldWaitForLazyRoutes =
|
|
hasPendingOrMayHaveLazyRoutes &&
|
|
currentName &&
|
|
(utils.transactionNameHasWildcard(currentName) || currentSource !== 'route');
|
|
|
|
if (shouldWaitForLazyRoutes) {
|
|
if (_lazyRouteTimeout === 0) {
|
|
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutes);
|
|
cleanupNavigationSpan();
|
|
originalEnd(endTimestamp);
|
|
return;
|
|
}
|
|
|
|
// If we have pending promises, wait for them. Otherwise, just wait for the timeout.
|
|
// This handles the case where we know lazy routes might load but patchRoutesOnNavigation
|
|
// hasn't been called yet.
|
|
const timeoutPromise = new Promise(r => setTimeout(r, _lazyRouteTimeout));
|
|
let waitPromise;
|
|
|
|
if (pendingPromises && pendingPromises.size > 0) {
|
|
const allSettled = Promise.allSettled(pendingPromises).then(() => {});
|
|
waitPromise = _lazyRouteTimeout === Infinity ? allSettled : Promise.race([allSettled, timeoutPromise]);
|
|
} else {
|
|
// No pending promises yet, but we know lazy routes might load
|
|
// Wait for the timeout to give React Router time to call patchRoutesOnNavigation
|
|
waitPromise = timeoutPromise;
|
|
}
|
|
|
|
waitPromise
|
|
.then(() => {
|
|
const updatedSpanJson = core.spanToJSON(span);
|
|
tryUpdateSpanNameBeforeEnd(
|
|
span,
|
|
updatedSpanJson,
|
|
updatedSpanJson.description,
|
|
location,
|
|
routes,
|
|
basename,
|
|
spanType,
|
|
allRoutes,
|
|
);
|
|
cleanupNavigationSpan();
|
|
originalEnd(endTimestamp);
|
|
})
|
|
.catch(() => {
|
|
cleanupNavigationSpan();
|
|
originalEnd(endTimestamp);
|
|
});
|
|
return;
|
|
}
|
|
|
|
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutes);
|
|
cleanupNavigationSpan();
|
|
originalEnd(endTimestamp);
|
|
};
|
|
|
|
core.addNonEnumerableProperty(span , patchedPropertyName, true);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function createV6CompatibleWithSentryReactRouterRouting(
|
|
Routes,
|
|
version,
|
|
) {
|
|
if (!_useEffect || !_useLocation || !_useNavigationType || !_createRoutesFromChildren || !_matchRoutes) {
|
|
debugBuild.DEBUG_BUILD &&
|
|
core.debug.warn(`reactRouterV6Instrumentation was unable to wrap Routes because of one or more missing parameters.
|
|
useEffect: ${_useEffect}. useLocation: ${_useLocation}. useNavigationType: ${_useNavigationType}.
|
|
createRoutesFromChildren: ${_createRoutesFromChildren}. matchRoutes: ${_matchRoutes}.`);
|
|
|
|
return Routes;
|
|
}
|
|
|
|
const SentryRoutes = (props) => {
|
|
const isMountRenderPass = React.useRef(true);
|
|
|
|
const location = _useLocation();
|
|
const navigationType = _useNavigationType();
|
|
|
|
_useEffect(
|
|
() => {
|
|
const routes = _createRoutesFromChildren(props.children) ;
|
|
|
|
if (isMountRenderPass.current) {
|
|
addRoutesToAllRoutes(routes);
|
|
|
|
updatePageloadTransaction({
|
|
activeRootSpan: utils.getActiveRootSpan(),
|
|
location,
|
|
routes,
|
|
allRoutes: Array.from(allRoutes),
|
|
});
|
|
isMountRenderPass.current = false;
|
|
} else {
|
|
// Note: Component-based routes don't support lazy route tracking via lazyRouteTimeout
|
|
// because React.lazy() loads happen at the component level, not the router level.
|
|
// Use createBrowserRouter with patchRoutesOnNavigation for lazy route tracking.
|
|
handleNavigation({ location, routes, navigationType, version, allRoutes: Array.from(allRoutes) });
|
|
}
|
|
},
|
|
// Re-run only on location/navigation changes, not children changes
|
|
[location, navigationType],
|
|
);
|
|
|
|
// @ts-expect-error Setting more specific React Component typing for `R` generic above
|
|
// will break advanced type inference done by react router params
|
|
return React.createElement(Routes, { ...props,} );
|
|
};
|
|
|
|
hoistNonReactStatics.hoistNonReactStatics(SentryRoutes, Routes);
|
|
|
|
// @ts-expect-error Setting more specific React Component typing for `R` generic above
|
|
// will break advanced type inference done by react router params
|
|
return SentryRoutes;
|
|
}
|
|
|
|
exports.addResolvedRoutesToParent = addResolvedRoutesToParent;
|
|
exports.addRoutesToAllRoutes = addRoutesToAllRoutes;
|
|
exports.allRoutes = allRoutes;
|
|
exports.computeLocationKey = computeLocationKey;
|
|
exports.createReactRouterV6CompatibleTracingIntegration = createReactRouterV6CompatibleTracingIntegration;
|
|
exports.createV6CompatibleWithSentryReactRouterRouting = createV6CompatibleWithSentryReactRouterRouting;
|
|
exports.createV6CompatibleWrapCreateBrowserRouter = createV6CompatibleWrapCreateBrowserRouter;
|
|
exports.createV6CompatibleWrapCreateMemoryRouter = createV6CompatibleWrapCreateMemoryRouter;
|
|
exports.createV6CompatibleWrapUseRoutes = createV6CompatibleWrapUseRoutes;
|
|
exports.handleNavigation = handleNavigation;
|
|
exports.processResolvedRoutes = processResolvedRoutes;
|
|
exports.shouldSkipNavigation = shouldSkipNavigation;
|
|
exports.updateNavigationSpan = updateNavigationSpan;
|
|
//# sourceMappingURL=instrumentation.js.map
|