123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- 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
|