Files
klz-cables.com/.pnpm-store/v10/files/57/db400e996bda7cceff709d891a9bc67aa82a225c514aec802ecf018c245bc27a2746459e1498f8b36aae440ef461ba303145c6bdd7b6e118dc1786d6805639
Marc Mintel 5397309103
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
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

1281 lines
44 KiB
Plaintext

import { browserTracingIntegration, WINDOW, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan } from '@sentry/browser';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, debug, addNonEnumerableProperty, spanToJSON, getCurrentScope, getClient } from '@sentry/core';
import * as React from 'react';
import { DEBUG_BUILD } from '../debug-build.js';
import { hoistNonReactStatics } from '../hoist-non-react-statics.js';
import { checkRouteForAsyncHandler } from './lazy-routes.js';
import { getActiveRootSpan, setNavigationContext, clearNavigationContext, resolveRouteNameAndSource, transactionNameHasWildcard, initializeRouterUtils } from './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 (WINDOW?.requestAnimationFrame) {
return WINDOW.requestAnimationFrame(callback);
}
return setTimeout(callback, 0) ;
}
/**
* Cancels a scheduled callback, handling both RAF (browser) and timeout (SSR) IDs.
*/
function cancelScheduledCallback(id) {
if (WINDOW?.cancelAnimationFrame) {
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 && transactionNameHasWildcard(trackedNav.routeName);
const proposedHasWildcard = 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) {
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 ?? getActiveRootSpan();
if (targetSpan) {
const spanJson = spanToJSON(targetSpan);
// Skip update if span has already ended (timestamp is set when span.end() is called)
if (spanJson.timestamp) {
DEBUG_BUILD && 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 WINDOW !== 'undefined') {
const globalLocation = 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 = spanToJSON(activeRootSpan);
const currentName = spanJson.description;
const hasBeenNamed = (activeRootSpan )?.__sentry_navigation_name_set__;
const currentNameHasWildcard = currentName && transactionNameHasWildcard(currentName);
const shouldUpdate = !hasBeenNamed || forceUpdate || currentNameHasWildcard;
if (shouldUpdate && !spanJson.timestamp) {
const currentBranches = matchRoutes(allRoutes, location);
const [name, source] = resolveRouteNameAndSource(
location,
allRoutes,
allRoutes,
(currentBranches ) || [],
_basename,
_lazyRouteManifest,
_enableAsyncRouteHandlers,
);
const currentSource = spanJson.data?.[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(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
// Only mark as finalized for non-wildcard route names (allows URL→route upgrades).
if (!transactionNameHasWildcard(name) && source === 'route') {
addNonEnumerableProperty(
activeRootSpan ,
'__sentry_navigation_name_set__',
true,
);
}
}
}
}
function setupRouterSubscription(
router,
routes,
version,
basename,
activeRootSpan,
) {
let isInitialPageloadComplete = false;
let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload';
let hasSeenPopAfterPageload = false;
let scheduledNavigationHandler = null;
let lastHandledPathname = null;
router.subscribe((state) => {
if (!isInitialPageloadComplete) {
const currentRootSpan = getActiveRootSpan();
const isCurrentlyInPageload = currentRootSpan && 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) {
DEBUG_BUILD &&
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) {
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 = 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
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) {
DEBUG_BUILD &&
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) {
checkRouteForAsyncHandler(route, processResolvedRoutes);
}
}
// Capture the active span BEFORE creating the router (same as browser router)
const memoryActiveRootSpanEarly = 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) {
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 = 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 = 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;
DEBUG_BUILD &&
debug.log(
'[React Router] lazyRouteTimeout set to Infinity, capping at finalTimeout:',
finalTimeout,
'ms to prevent indefinite hangs',
);
} else if (Number.isNaN(configuredMaxWait)) {
DEBUG_BUILD &&
debug.warn('[React Router] lazyRouteTimeout must be a number, falling back to default:', defaultMaxWait);
_lazyRouteTimeout = defaultMaxWait;
} else if (configuredMaxWait < 0) {
DEBUG_BUILD &&
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
initializeRouterUtils(matchRoutes, stripBasename || false);
},
afterAllSetup(client) {
integration.afterAllSetup(client);
const initPathName = WINDOW.location?.pathname;
if (instrumentPageLoad && initPathName) {
startBrowserTracingPageLoadSpan(client, {
name: initPathName,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[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) {
DEBUG_BUILD &&
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: 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 = 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 ? 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 = setNavigationContext(targetPath, activeRootSpan);
let result;
try {
result = await originalPatchRoutes(args);
} finally {
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 ? 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 = getClient();
if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) {
return;
}
const activeRootSpan = getActiveRootSpan();
if (activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload' && navigationType === 'POP') {
return;
}
if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) {
const [name, source] = 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 ? !!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;
DEBUG_BUILD &&
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(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source );
addNonEnumerableProperty(
trackedNav.span ,
'__sentry_navigation_name_set__',
true,
);
trackedNav.routeName = name;
DEBUG_BUILD && debug.log(`[Tracing] Updated navigation span name from "${oldName}" to "${name}"`);
}
} else {
DEBUG_BUILD && 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 = startBrowserTracingNavigationSpan(client, {
name: placeholderEntry.routeName, // Use placeholder's routeName in case it was updated
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[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] = resolveRouteNameAndSource(
location,
allRoutes || routes,
allRoutes || routes,
branches,
basename,
_lazyRouteManifest,
_enableAsyncRouteHandlers,
);
getCurrentScope().setTransactionName(name || '/');
if (activeRootSpan) {
activeRootSpan.updateName(name);
activeRootSpan.setAttribute(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 && transactionNameHasWildcard(currentName);
if (hasWildcard && newSource === 'route' && !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?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
if (currentSource === 'route' && currentName && !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] = 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(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
}
} catch (error) {
DEBUG_BUILD && 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 = spanToJSON(span);
const currentName = spanJson.description;
const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// Helper to clean up activeNavigationSpans after span ends
const cleanupNavigationSpan = () => {
const client = 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 &&
(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 = 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);
};
addNonEnumerableProperty(span , patchedPropertyName, true);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createV6CompatibleWithSentryReactRouterRouting(
Routes,
version,
) {
if (!_useEffect || !_useLocation || !_useNavigationType || !_createRoutesFromChildren || !_matchRoutes) {
DEBUG_BUILD &&
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: 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(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;
}
export { addResolvedRoutesToParent, addRoutesToAllRoutes, allRoutes, computeLocationKey, createReactRouterV6CompatibleTracingIntegration, createV6CompatibleWithSentryReactRouterRouting, createV6CompatibleWrapCreateBrowserRouter, createV6CompatibleWrapCreateMemoryRouter, createV6CompatibleWrapUseRoutes, handleNavigation, processResolvedRoutes, shouldSkipNavigation, updateNavigationSpan };
//# sourceMappingURL=instrumentation.js.map