UNPKG

@sentry/nextjs

Version:
199 lines (196 loc) • 9.55 kB
import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, GLOBAL_OBJ, isObjectLike, filterCollectedUrl, timestampInSeconds } from '@sentry/core'; import { WINDOW, resolveCurrentRoute, startBrowserTracingNavigationSpan, resolveRoute, getAbsoluteUrl } from '@sentry/react'; import { stripTrailingSlash } from './parameterization.js'; import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, NAVIGATION_TYPE, SENTRY_ORIGIN, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; function getNavigationTargetUrl(href) { const isProtocolRelative = typeof href === "string" && href.startsWith("//"); return getAbsoluteUrl(isProtocolRelative ? `${WINDOW.location.protocol}${href}` : href); } function setNavigationSpanUrlAttributes(span, urlPath, urlOrPath) { span.setAttributes({ [URL_PATH]: urlPath, [URL_FULL]: filterCollectedUrl(getNavigationTargetUrl(urlOrPath)) }); } let pendingHistoryTraversal; let pendingHistoryTraversalTimeout; const PENDING_HISTORY_TRAVERSAL_TIMEOUT_MS = 1e3; function setPendingHistoryTraversal(navigationType) { clearTimeout(pendingHistoryTraversalTimeout); pendingHistoryTraversal = { navigationType, startTime: timestampInSeconds() }; pendingHistoryTraversalTimeout = setTimeout(() => { pendingHistoryTraversal = void 0; }, PENDING_HISTORY_TRAVERSAL_TIMEOUT_MS); } function takePendingHistoryTraversal() { clearTimeout(pendingHistoryTraversalTimeout); const traversal = pendingHistoryTraversal; pendingHistoryTraversal = void 0; return traversal; } let navigationRoutingMode = "router-patch"; const currentRouterPatchingNavigationSpanRef = { current: void 0 }; const GLOBAL_OBJ_WITH_NEXT_ROUTER = GLOBAL_OBJ; const globalWithInjectedBasePath = GLOBAL_OBJ; function appRouterInstrumentNavigation(client) { routerTransitionHandler = (href, navigationType) => { const basePath = process.env._sentryBasePath ?? globalWithInjectedBasePath._sentryBasePath; const normalizedHref = basePath && href.startsWith("/") && !href.startsWith(basePath) ? `${basePath}${href}` : href; const unparameterizedPathname = stripTrailingSlash(new URL(normalizedHref, WINDOW.location.href).pathname); const parameterizedPathname = resolveRoute(normalizedHref, client); const spanName = parameterizedPathname ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : unparameterizedPathname); if (navigationRoutingMode === "router-patch") { navigationRoutingMode = "transition-start-hook"; } const currentNavigationSpan = currentRouterPatchingNavigationSpanRef.current; if (currentNavigationSpan) { currentNavigationSpan.updateName(spanName); currentNavigationSpan.setAttributes({ // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. [NAVIGATION_TYPE]: `router.${navigationType}`, [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? "route" : "url", ...parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname } }); setNavigationSpanUrlAttributes(currentNavigationSpan, unparameterizedPathname, normalizedHref); currentRouterPatchingNavigationSpanRef.current = void 0; } else { startBrowserTracingNavigationSpan( client, { name: spanName, attributes: { [SENTRY_OP]: NAVIGATION, [SENTRY_ORIGIN]: "auto.navigation.nextjs.app_router_instrumentation", [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? "route" : "url", // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. [NAVIGATION_TYPE]: `router.${navigationType}`, [URL_TEMPLATE]: parameterizedPathname } }, { url: getNavigationTargetUrl(normalizedHref) } ); } }; WINDOW.addEventListener("popstate", () => { const pathname = stripTrailingSlash(WINDOW.location.pathname); const parameterizedPathname = resolveCurrentRoute(client); const spanName = parameterizedPathname ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname); const traversal = takePendingHistoryTraversal(); if (!traversal && currentRouterPatchingNavigationSpanRef.current?.isRecording()) { currentRouterPatchingNavigationSpanRef.current.updateName(spanName); currentRouterPatchingNavigationSpanRef.current.setAttribute( SENTRY_SEGMENT_NAME_SOURCE, parameterizedPathname ? "route" : "url" ); if (parameterizedPathname) { currentRouterPatchingNavigationSpanRef.current.setAttribute(URL_TEMPLATE, parameterizedPathname); } setNavigationSpanUrlAttributes(currentRouterPatchingNavigationSpanRef.current, pathname, WINDOW.location.href); } else { currentRouterPatchingNavigationSpanRef.current = startBrowserTracingNavigationSpan( client, { name: spanName, startTime: traversal?.startTime, attributes: { [SENTRY_ORIGIN]: "auto.navigation.nextjs.app_router_instrumentation", [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? "route" : "url", // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. [NAVIGATION_TYPE]: traversal?.navigationType ?? "browser.popstate", [URL_TEMPLATE]: parameterizedPathname } }, // The full location rather than just the pathname, so the span's `url.full` keeps the // (filtered) query string like the update path above does. { url: WINDOW.location.href } ); } }); let routerPatched = false; let triesToFindRouter = 0; const MAX_TRIES_TO_FIND_ROUTER = 500; const ROUTER_AVAILABILITY_CHECK_INTERVAL_MS = 20; const checkForRouterAvailabilityInterval = setInterval(() => { triesToFindRouter++; const router = GLOBAL_OBJ_WITH_NEXT_ROUTER?.next?.router; if (routerPatched || triesToFindRouter > MAX_TRIES_TO_FIND_ROUTER) { clearInterval(checkForRouterAvailabilityInterval); } else if (router) { clearInterval(checkForRouterAvailabilityInterval); routerPatched = true; patchRouter(client, router, currentRouterPatchingNavigationSpanRef); const globalValue = GLOBAL_OBJ_WITH_NEXT_ROUTER.next; if (globalValue) { GLOBAL_OBJ_WITH_NEXT_ROUTER.next = new Proxy(globalValue, { set(target, p, newValue) { if (p === "router" && isObjectLike(newValue)) { patchRouter(client, newValue, currentRouterPatchingNavigationSpanRef); } target[p] = newValue; return true; } }); } } }, ROUTER_AVAILABILITY_CHECK_INTERVAL_MS); } function transactionNameifyRouterArgument(target) { try { return new URL(target, "http://example.com/").pathname; } catch { return "/"; } } const patchedRouters = /* @__PURE__ */ new WeakSet(); function patchRouter(client, router, currentNavigationSpanRef) { if (patchedRouters.has(router)) { return; } patchedRouters.add(router); ["back", "forward", "push", "replace"].forEach((routerFunctionName) => { if (router?.[routerFunctionName]) { router[routerFunctionName] = new Proxy(router[routerFunctionName], { apply(target, thisArg, argArray) { if (navigationRoutingMode !== "router-patch") { return target.apply(thisArg, argArray); } if (routerFunctionName === "back" || routerFunctionName === "forward") { setPendingHistoryTraversal(`router.${routerFunctionName}`); return target.apply(thisArg, argArray); } const href = argArray[0]; const basePath = process.env._sentryBasePath ?? globalWithInjectedBasePath._sentryBasePath; const normalizedHref = basePath && typeof href === "string" && href.startsWith("/") && !href.startsWith(basePath) ? `${basePath}${href}` : href; const transactionName = stripTrailingSlash(transactionNameifyRouterArgument(normalizedHref)); const parameterizedPathname = resolveRoute(transactionName, client); currentNavigationSpanRef.current = startBrowserTracingNavigationSpan( client, { // With span streaming, span names have to be low cardinality, so we can't fall back to the URL. name: parameterizedPathname ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : transactionName), attributes: { [SENTRY_OP]: NAVIGATION, [SENTRY_ORIGIN]: "auto.navigation.nextjs.app_router_instrumentation", [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? "route" : "url", // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. [NAVIGATION_TYPE]: `router.${routerFunctionName}`, [URL_TEMPLATE]: parameterizedPathname } }, { url: getNavigationTargetUrl(normalizedHref) } ); return target.apply(thisArg, argArray); } }); } }); } let routerTransitionHandler = void 0; function captureRouterTransitionStart(href, navigationType) { if (routerTransitionHandler) { routerTransitionHandler(href, navigationType); } } export { appRouterInstrumentNavigation, captureRouterTransitionStart }; //# sourceMappingURL=appRouterRoutingInstrumentation.js.map