Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
199 lines
7.6 KiB
Plaintext
199 lines
7.6 KiB
Plaintext
import { stripUrlQueryAndFragment, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, parseBaggageHeader, browserPerformanceTimeOrigin, debug } 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 {
|
|
DEBUG_BUILD && debug.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
|