Skip to content

feat(nextjs): Add browserTracingIntegration #10397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 2, 2024
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