@sentry/nextjs
Version:
Official Sentry SDK for Next.js
199 lines (196 loc) • 9.55 kB
JavaScript
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