Skip to content
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os from 'os';
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

Expand Down Expand Up @@ -31,6 +32,8 @@ const config: PlaywrightTestConfig = {
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */
workers: os.cpus().length,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* `next dev` is incredibly buggy with the app dir */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os from 'os';
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

Expand Down Expand Up @@ -29,6 +30,8 @@ const config: PlaywrightTestConfig = {
*/
timeout: 10000,
},
/* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */
workers: os.cpus().length,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
Expand Down
74 changes: 73 additions & 1 deletion packages/nextjs/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { BrowserTracing as OriginalBrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/react';
import {
BrowserTracing as OriginalBrowserTracing,
browserTracingIntegration as originalBrowserTracingIntegration,
defaultRequestInstrumentationOptions,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from '@sentry/react';
import type { Integration, StartSpanOptions } from '@sentry/types';
import { nextRouterInstrumentation } from '../index.client';

/**
* A custom BrowserTracing integration for Next.js.
*
* @deprecated Use `browserTracingIntegration` instead.
*/
export class BrowserTracing extends OriginalBrowserTracing {
public constructor(options?: ConstructorParameters<typeof OriginalBrowserTracing>[0]) {
Expand All @@ -19,8 +28,71 @@ export class BrowserTracing extends OriginalBrowserTracing {
]
: // eslint-disable-next-line deprecation/deprecation
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
// eslint-disable-next-line deprecation/deprecation
routingInstrumentation: nextRouterInstrumentation,
...options,
});
}
}

/**
* A custom BrowserTracing integration for Next.js.
*/
export function browserTracingIntegration(
options?: Parameters<typeof originalBrowserTracingIntegration>[0],
): Integration {
const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
// eslint-disable-next-line deprecation/deprecation
tracingOrigins:
process.env.NODE_ENV === 'development'
? [
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
// has cors and it doesn't like extra headers when it's accessed from a different URL.
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
/^\/(?!\/)/,
]
: // eslint-disable-next-line deprecation/deprecation
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
...options,
instrumentNavigation: false,
instrumentPageLoad: false,
});

return {
...browserTracingIntegrationInstance,
afterAllSetup(client) {
const startPageloadCallback = (startSpanOptions: StartSpanOptions): void => {
startBrowserTracingPageLoadSpan(client, startSpanOptions);
};

const startNavigationCallback = (startSpanOptions: StartSpanOptions): void => {
startBrowserTracingNavigationSpan(client, startSpanOptions);
};

// We need to run the navigation span instrumentation before the `afterAllSetup` hook on the normal browser
// tracing integration because we need to ensure the order of execution is as follows:
// Instrumentation to start span on RSC fetch request runs -> Instrumentation to put tracing headers from active span on fetch runs
// If it were the other way around, the RSC fetch request would not receive the tracing headers from the navigation transaction.
// eslint-disable-next-line deprecation/deprecation
nextRouterInstrumentation(
() => undefined,
false,
options?.instrumentNavigation,
startPageloadCallback,
startNavigationCallback,
);

browserTracingIntegrationInstance.afterAllSetup(client);

// eslint-disable-next-line deprecation/deprecation
nextRouterInstrumentation(
() => undefined,
options?.instrumentPageLoad,
false,
startPageloadCallback,
startNavigationCallback,
);
},
};
}
16 changes: 13 additions & 3 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions, browserTracingIntegration } from '@sentry/react';
import type { BrowserOptions } from '@sentry/react';
import {
Integrations as OriginalIntegrations,
getCurrentScope,
Expand All @@ -10,11 +10,13 @@ import type { EventProcessor, Integration } from '@sentry/types';

import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
import { browserTracingIntegration } from './browserTracingIntegration';
import { BrowserTracing } from './browserTracingIntegration';
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
import { applyTunnelRouteOption } from './tunnelRoute';

export * from '@sentry/react';
// eslint-disable-next-line deprecation/deprecation
export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
export { captureUnderscoreErrorException } from '../common/_error';

Expand All @@ -35,6 +37,7 @@ export const Integrations = {
//
// import { BrowserTracing } from '@sentry/nextjs';
// const instance = new BrowserTracing();
// eslint-disable-next-line deprecation/deprecation
export { BrowserTracing, rewriteFramesIntegration };

// Treeshakable guard to remove all code related to tracing
Expand Down Expand Up @@ -68,7 +71,7 @@ export function init(options: BrowserOptions): void {
}

// TODO v8: Remove this again
// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/sveltekit` :(
// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/nextjs` :(
function fixBrowserTracingIntegration(options: BrowserOptions): void {
const { integrations } = options;
if (!integrations) {
Expand All @@ -89,6 +92,7 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void {
function isNewBrowserTracingIntegration(
integration: Integration,
): integration is Integration & { options?: Parameters<typeof browserTracingIntegration>[0] } {
// eslint-disable-next-line deprecation/deprecation
return !!integration.afterAllSetup && !!(integration as BrowserTracing).options;
}

Expand All @@ -102,17 +106,21 @@ function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Inte
// If `browserTracingIntegration()` was added, we need to force-convert it to our custom one
if (isNewBrowserTracingIntegration(browserTracing)) {
const { options } = browserTracing;
// eslint-disable-next-line deprecation/deprecation
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
}

// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
// eslint-disable-next-line deprecation/deprecation
if (!(browserTracing instanceof BrowserTracing)) {
// eslint-disable-next-line deprecation/deprecation
const options: ConstructorParameters<typeof BrowserTracing>[0] = (browserTracing as BrowserTracing).options;
// This option is overwritten by the custom integration
delete options.routingInstrumentation;
// eslint-disable-next-line deprecation/deprecation
delete options.tracingOrigins;
// eslint-disable-next-line deprecation/deprecation
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
}

Expand All @@ -126,7 +134,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] {
// will get treeshaken away
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
if (hasTracingEnabled(options)) {
customDefaultIntegrations.push(new BrowserTracing());
customDefaultIntegrations.push(browserTracingIntegration());
}
}

Expand All @@ -140,4 +148,6 @@ export function withSentryConfig<T>(exportedUserNextConfig: T): T {
return exportedUserNextConfig;
}

export { browserTracingIntegration } from './browserTracingIntegration';

export * from '../common';
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import { WINDOW } from '@sentry/react';
import type { Primitive, Transaction, TransactionContext } from '@sentry/types';
import type { Primitive, Span, StartSpanOptions, Transaction, TransactionContext } from '@sentry/types';
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils';

type StartTransactionCb = (context: TransactionContext) => Transaction | undefined;
type StartSpanCb = (context: StartSpanOptions) => void;

const DEFAULT_TAGS = {
'routing.instrumentation': 'next-app-router',
} as const;

/**
* Instruments the Next.js Clientside App Router.
* Instruments the Next.js Client App Router.
*/
// TODO(v8): Clean this function up by splitting into pageload and navigation instrumentation respectively. Also remove startTransactionCb in the process.
export function appRouterInstrumentation(
startTransactionCb: StartTransactionCb,
startTransactionOnPageLoad: boolean = true,
startTransactionOnLocationChange: boolean = true,
startPageloadSpanCallback: StartSpanCb,
startNavigationSpanCallback: StartSpanCb,
): void {
// We keep track of the active transaction so we can finish it when we start a navigation transaction.
let activeTransaction: Transaction | undefined = undefined;
let activeTransaction: Span | undefined = 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 = WINDOW.location.pathname;

if (startTransactionOnPageLoad) {
activeTransaction = startTransactionCb({
const transactionContext = {
name: prevLocationName,
op: 'pageload',
origin: 'auto.pageload.nextjs.app_router_instrumentation',
tags: DEFAULT_TAGS,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
metadata: { source: 'url' },
});
} as const;
activeTransaction = startTransactionCb(transactionContext);
startPageloadSpanCallback(transactionContext);
}

if (startTransactionOnLocationChange) {
Expand Down Expand Up @@ -66,13 +72,16 @@ export function appRouterInstrumentation(
activeTransaction.end();
}

startTransactionCb({
const transactionContext = {
name: transactionName,
op: 'navigation',
origin: 'auto.navigation.nextjs.app_router_instrumentation',
tags,
metadata: { source: 'url' },
});
} as const;

startTransactionCb(transactionContext);
startNavigationSpanCallback(transactionContext);
});
}
}
Expand Down
25 changes: 21 additions & 4 deletions packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
import { WINDOW } from '@sentry/react';
import type { Transaction, TransactionContext } from '@sentry/types';
import type { StartSpanOptions, Transaction, TransactionContext } from '@sentry/types';

import { appRouterInstrumentation } from './appRouterRoutingInstrumentation';
import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation';

type StartTransactionCb = (context: TransactionContext) => Transaction | undefined;
type StartSpanCb = (context: StartSpanOptions) => void;

/**
* Instruments the Next.js Clientside Router.
* Instruments the Next.js Client Router.
*
* @deprecated Use `browserTracingIntegration()` as exported from `@sentry/nextjs` instead.
*/
export function nextRouterInstrumentation(
startTransactionCb: StartTransactionCb,
startTransactionOnPageLoad: boolean = true,
startTransactionOnLocationChange: boolean = true,
startPageloadSpanCallback?: StartSpanCb,
startNavigationSpanCallback?: StartSpanCb,
): void {
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
if (isAppRouter) {
appRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange);
appRouterInstrumentation(
startTransactionCb,
startTransactionOnPageLoad,
startTransactionOnLocationChange,
startPageloadSpanCallback || (() => undefined),
startNavigationSpanCallback || (() => undefined),
);
} else {
pagesRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange);
pagesRouterInstrumentation(
startTransactionCb,
startTransactionOnPageLoad,
startTransactionOnLocationChange,
startPageloadSpanCallback || (() => undefined),
startNavigationSpanCallback || (() => undefined),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ParsedUrlQuery } from 'querystring';
import { getClient, getCurrentScope } from '@sentry/core';
import { WINDOW } from '@sentry/react';
import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
import type { Primitive, StartSpanOptions, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
import {
browserPerformanceTimeOrigin,
logger,
Expand All @@ -20,6 +20,7 @@ const globalObject = WINDOW as typeof WINDOW & {
};

type StartTransactionCb = (context: TransactionContext) => Transaction | undefined;
type StartSpanCb = (context: StartSpanOptions) => void;

/**
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
Expand Down Expand Up @@ -117,6 +118,8 @@ export function pagesRouterInstrumentation(
startTransactionCb: StartTransactionCb,
startTransactionOnPageLoad: boolean = true,
startTransactionOnLocationChange: boolean = true,
startPageloadSpanCallback: StartSpanCb,
startNavigationSpanCallback: StartSpanCb,
): void {
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
// eslint-disable-next-line deprecation/deprecation
Expand All @@ -130,7 +133,7 @@ export function pagesRouterInstrumentation(

if (startTransactionOnPageLoad) {
const source = route ? 'route' : 'url';
activeTransaction = startTransactionCb({
const transactionContext = {
name: prevLocationName,
op: 'pageload',
origin: 'auto.pageload.nextjs.pages_router_instrumentation',
Expand All @@ -143,7 +146,9 @@ export function pagesRouterInstrumentation(
source,
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
});
} as const;
activeTransaction = startTransactionCb(transactionContext);
startPageloadSpanCallback(transactionContext);
}

if (startTransactionOnLocationChange) {
Expand Down Expand Up @@ -173,13 +178,15 @@ export function pagesRouterInstrumentation(
activeTransaction.end();
}

const navigationTransaction = startTransactionCb({
const transactionContext = {
name: transactionName,
op: 'navigation',
origin: 'auto.navigation.nextjs.pages_router_instrumentation',
tags,
metadata: { source: transactionSource },
});
} as const;
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`
Expand Down
Loading