UNPKG

@sentry/nextjs

Version:
199 lines (172 loc) 7.76 kB
import { stripUrlQueryAndFragment, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, parseBaggageHeader, browserPerformanceTimeOrigin, logger } from '@sentry/core'; import { startBrowserTracingNavigationSpan, WINDOW, startBrowserTracingPageLoadSpan } from '@sentry/react'; import RouterImport from 'next/router'; import { DEBUG_BUILD } from '../../common/debug-build.js'; // next/router v10 is CJS // // For ESM/CJS interoperability 'reasons', depending on how this file is loaded, Router might be on the default export const Router = RouterImport.events ? RouterImport : (RouterImport ).default; const globalObject = WINDOW ; /** * Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app. */ /** * 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?.innerHTML) { try { nextData = JSON.parse(nextDataTag.innerHTML); } catch (e) { DEBUG_BUILD && 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?.pageProps) { nextDataTagInfo.sentryTrace = props.pageProps._sentryTraceData; nextDataTagInfo.baggage = props.pageProps._sentryBaggage; } return nextDataTagInfo; } /** * Instruments the Next.js pages router for pageloads. * 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 pagesRouterInstrumentPageLoad(client) { const { route, params, sentryTrace, baggage } = extractNextDataTagInformation(); const parsedBaggage = parseBaggageHeader(baggage); let name = route || globalObject.location.pathname; // /_error is the fallback page for all errors. If there is a transaction name for /_error, use that instead if (parsedBaggage?.['sentry-transaction'] && name === '/_error') { name = parsedBaggage['sentry-transaction']; // Strip any HTTP method from the span name name = name.replace(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i, ''); } const origin = browserPerformanceTimeOrigin(); startBrowserTracingPageLoadSpan( client, { name, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTime: origin ? origin / 1000 : undefined, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url', ...(params && client.getOptions().sendDefaultPii && { ...params }), }, }, { sentryTrace, baggage }, ); } /** * Instruments the Next.js pages router for navigation. * 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 pagesRouterInstrumentNavigation(client) { Router.events.on('routeChangeStart', (navigationTarget) => { const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget); const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget); let newLocation; let spanSource; if (matchedRoute) { newLocation = matchedRoute; spanSource = 'route'; } else { newLocation = strippedNavigationTarget; spanSource = 'url'; } startBrowserTracingNavigationSpan(client, { name: newLocation, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, }, }); }); } 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 ); } export { pagesRouterInstrumentNavigation, pagesRouterInstrumentPageLoad }; //# sourceMappingURL=pagesRouterRoutingInstrumentation.js.map