Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/nextjs/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import type { Integration } from '@sentry/core';
import { createUrlRouteProvider, setRouteProvider } from '@sentry/core/browser';
import { browserTracingIntegration as originalBrowserTracingIntegration, isBotUserAgent } from '@sentry/react';
import { maybeParameterizeRoute, stripBasePath, stripTrailingSlash, withBasePath } from './routing/parameterization';
import { getNextRouteFromPathname } from './routing/pagesRouterRoutingInstrumentation';
import { nextRouterInstrumentNavigation, nextRouterInstrumentPageLoad } from './routing/nextRoutingInstrumentation';

/**
* Resolves a URL against whichever router manifest the app ships.
*
* The two want the pathname differently: App Router routes are generated with `basePath` baked in,
* while Next strips it internally for the Pages Router.
*/
function resolveNextRoute(url: URL): string | undefined {
const pathname = stripTrailingSlash(url.pathname);

return maybeParameterizeRoute(withBasePath(pathname)) ?? getNextRouteFromPathname(stripBasePath(pathname));
}

/**
* A custom browser tracing integration for Next.js.
*/
Expand All @@ -28,6 +43,14 @@ export function browserTracingIntegration(

return {
...browserTracingIntegrationInstance,
setup(client) {
// Registered here rather than in `afterAllSetup` so it is in place before the pageload span is
// named. The build-time route manifest is already on the global object at this point, so nothing
// has to wait for the router itself.
setRouteProvider(createUrlRouteProvider(resolveNextRoute), client);

browserTracingIntegrationInstance.setup?.(client);
},
afterAllSetup(client) {
if (isBotUserAgent()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,9 @@ import {
WINDOW,
getAbsoluteUrl,
} from '@sentry/react';
import { maybeParameterizeRoute } from './parameterization';
import { maybeParameterizeRoute, stripTrailingSlash } from './parameterization';
import { URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes';

/**
* Strips trailing slash from a pathname, unless it's the root path.
* This normalizes paths like '/about/' to '/about' to handle Next.js `trailingSlash: true` config.
*/
function stripTrailingSlash(pathname: string): string {
return pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
}

function setNavigationSpanUrlAttributes(span: Span, urlPath: string, urlOrPath: string): void {
span.setAttributes({
[URL_PATH]: urlPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,12 @@ export function pagesRouterInstrumentNavigation(client: Client): void {
});
}

function getNextRouteFromPathname(pathname: string): string | undefined {
/**
* Matches a pathname against the Pages Router build manifest, e.g. `/users/1` -> `/users/[id]`.
*
* Expects a pathname without `basePath`, which is what Next reports internally.
*/
export function getNextRouteFromPathname(pathname: string): string | undefined {
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
Expand Down
40 changes: 40 additions & 0 deletions packages/nextjs/src/client/routing/parameterization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ let cachedManifestString: string | undefined = undefined;
const compiledRegexCache: Map<string, RegExp> = new Map();
const routeResultCache: Map<string, string | undefined> = new Map();

const globalWithInjectedBasePath = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryBasePath: string | undefined;
};

/**
* Strips trailing slash from a pathname, unless it's the root path.
* This normalizes paths like '/about/' to '/about' to handle Next.js `trailingSlash: true` config.
*/
export function stripTrailingSlash(pathname: string): string {
return pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
}

function getBasePath(): string | undefined {
return process.env._sentryBasePath ?? globalWithInjectedBasePath._sentryBasePath;
}

/**
* Prefixes a pathname with the configured `basePath` when it is missing.
*
* The App Router manifest is generated with `basePath` baked into every route, so a pathname that
* lacks it matches nothing.
*/
export function withBasePath(pathname: string): string {
const basePath = getBasePath();

return basePath && !pathname.startsWith(basePath) ? `${basePath}${pathname}` : pathname;
}

/**
* Removes the configured `basePath` from a pathname.
*
* The opposite of {@link withBasePath}, because Next strips `basePath` internally for the Pages
* Router: `__BUILD_MANIFEST.sortedPages` holds routes without it.
*/
export function stripBasePath(pathname: string): string {
const basePath = getBasePath();

return basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) || '/' : pathname;
}

/**
* Calculate the specificity score for a route path.
* Lower scores indicate more specific routes.
Expand Down
Loading