Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); const react = require('@sentry/react'); const utils = require('@sentry/utils'); const Router = require('next/router'); const debugBuild = require('../../common/debug-build.js'); const globalObject = react.WINDOW ; /** * Every Next.js page (static and dynamic ones) comes with a script tag with the id "__NEXT_DATA__". This script tag * contains a JSON object with data that was either generated at build time for static pages (`getStaticProps`), or at * runtime with data fetchers like `getServerSideProps.`. * * We can use this information to: * - Always get the parameterized route we're in when loading a page. * - Send trace information (trace-id, baggage) from the server to the client. * * This function extracts this information. */ function extractNextDataTagInformation() { let nextData; // Let's be on the safe side and actually check first if there is really a __NEXT_DATA__ script tag on the page. // Theoretically this should always be the case though. const nextDataTag = globalObject.document.getElementById('__NEXT_DATA__'); if (nextDataTag && nextDataTag.innerHTML) { try { nextData = JSON.parse(nextDataTag.innerHTML); } catch (e) { debugBuild.DEBUG_BUILD && utils.logger.warn('Could not extract __NEXT_DATA__'); } } if (!nextData) { return {}; } const nextDataTagInfo = {}; const { page, query, props } = nextData; // `nextData.page` always contains the parameterized route - except for when an error occurs in a data fetching // function, then it is "/_error", but that isn't a problem since users know which route threw by looking at the // parent transaction // TODO: Actually this is a problem (even though it is not that big), because the DSC and the transaction payload will contain // a different transaction name. Maybe we can fix this. Idea: Also send transaction name via pageProps when available. nextDataTagInfo.route = page; nextDataTagInfo.params = query; if (props && props.pageProps) { nextDataTagInfo.sentryTrace = props.pageProps._sentryTraceData; nextDataTagInfo.baggage = props.pageProps._sentryBaggage; } return nextDataTagInfo; } const DEFAULT_TAGS = { 'routing.instrumentation': 'next-pages-router', } ; // We keep track of the active transaction so we can finish it when we start a navigation transaction. let activeTransaction = undefined; // We keep track of the previous location name so we can set the `from` field on navigation transactions. // This is either a route or a pathname. let prevLocationName = undefined; const client = core.getClient(); /** * Instruments the Next.js pages router. Only supported for * client side routing. Works for Next >= 10. * * Leverages the SingletonRouter from the `next/router` to * generate pageload/navigation transactions and parameterize * transaction names. */ function pagesRouterInstrumentation( startTransactionCb, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true, startPageloadSpanCallback, startNavigationSpanCallback, ) { const { route, params, sentryTrace, baggage } = extractNextDataTagInformation(); // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = utils.tracingContextFromHeaders( sentryTrace, baggage, ); core.getCurrentScope().setPropagationContext(propagationContext); prevLocationName = route || globalObject.location.pathname; if (startTransactionOnPageLoad) { const source = route ? 'route' : 'url'; const transactionContext = { name: prevLocationName, op: 'pageload', origin: 'auto.pageload.nextjs.pages_router_instrumentation', tags: DEFAULT_TAGS, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTimestamp: utils.browserPerformanceTimeOrigin ? utils.browserPerformanceTimeOrigin / 1000 : undefined, ...(params && client && client.getOptions().sendDefaultPii && { data: params }), ...traceparentData, metadata: { source, dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, } ; activeTransaction = startTransactionCb(transactionContext); startPageloadSpanCallback(transactionContext); } if (startTransactionOnLocationChange) { Router.default.events.on('routeChangeStart', (navigationTarget) => { const strippedNavigationTarget = utils.stripUrlQueryAndFragment(navigationTarget); const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget); let transactionName; let transactionSource; if (matchedRoute) { transactionName = matchedRoute; transactionSource = 'route'; } else { transactionName = strippedNavigationTarget; transactionSource = 'url'; } const tags = { ...DEFAULT_TAGS, from: prevLocationName, }; prevLocationName = transactionName; if (activeTransaction) { activeTransaction.end(); } const transactionContext = { name: transactionName, op: 'navigation', origin: 'auto.navigation.nextjs.pages_router_instrumentation', tags, metadata: { source: transactionSource }, } ; const navigationTransaction = startTransactionCb(transactionContext); startNavigationSpanCallback(transactionContext); if (navigationTransaction) { // In addition to the navigation transaction we're also starting a span to mark Next.js's `routeChangeStart` // and `routeChangeComplete` events. // We don't want to finish the navigation transaction on `routeChangeComplete`, since users might want to attach // spans to that transaction even after `routeChangeComplete` is fired (eg. HTTP requests in some useEffect // hooks). Instead, we'll simply let the navigation transaction finish itself (it's an `IdleTransaction`). // eslint-disable-next-line deprecation/deprecation const nextRouteChangeSpan = navigationTransaction.startChild({ op: 'ui.nextjs.route-change', origin: 'auto.ui.nextjs.pages_router_instrumentation', description: 'Next.js Route Change', }); const finishRouteChangeSpan = () => { nextRouteChangeSpan.end(); Router.default.events.off('routeChangeComplete', finishRouteChangeSpan); }; Router.default.events.on('routeChangeComplete', finishRouteChangeSpan); } }); } } function getNextRouteFromPathname(pathname) { const pageRoutes = (globalObject.__BUILD_MANIFEST || {}).sortedPages; // Page route should in 99.999% of the cases be defined by now but just to be sure we make a check here if (!pageRoutes) { return; } return pageRoutes.find(route => { const routeRegExp = convertNextRouteToRegExp(route); return pathname.match(routeRegExp); }); } /** * Converts a Next.js style route to a regular expression that matches on pathnames (no query params or URL fragments). * * In general this involves replacing any instances of square brackets in a route with a wildcard: * e.g. "/users/[id]/info" becomes /\/users\/([^/]+?)\/info/ * * Some additional edgecases need to be considered: * - All routes have an optional slash at the end, meaning users can navigate to "/users/[id]/info" or * "/users/[id]/info/" - both will be resolved to "/users/[id]/info". * - Non-optional "catchall"s at the end of a route must be considered when matching (e.g. "/users/[...params]"). * - Optional "catchall"s at the end of a route must be considered when matching (e.g. "/users/[[...params]]"). * * @param route A Next.js style route as it is found in `global.__BUILD_MANIFEST.sortedPages` */ function convertNextRouteToRegExp(route) { // We can assume a route is at least "/". const routeParts = route.split('/'); let optionalCatchallWildcardRegex = ''; if (routeParts[routeParts.length - 1].match(/^\[\[\.\.\..+\]\]$/)) { // If last route part has pattern "[[...xyz]]" we pop the latest route part to get rid of the required trailing // slash that would come before it if we didn't pop it. routeParts.pop(); optionalCatchallWildcardRegex = '(?:/(.+?))?'; } const rejoinedRouteParts = routeParts .map( routePart => routePart .replace(/^\[\.\.\..+\]$/, '(.+?)') // Replace catch all wildcard with regex wildcard .replace(/^\[.*\]$/, '([^/]+?)'), // Replace route wildcards with lazy regex wildcards ) .join('/'); // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- routeParts are from the build manifest, so no raw user input return new RegExp( `^${rejoinedRouteParts}${optionalCatchallWildcardRegex}(?:/)?$`, // optional slash at the end ); } exports.pagesRouterInstrumentation = pagesRouterInstrumentation; //# sourceMappingURL=pagesRouterRoutingInstrumentation.js.map