Skip to content

Commit cfb1b60

Browse files
authored
feat(nextjs): Add browserTracingIntegration (#10397)
1 parent cc0fcb8 commit cfb1b60

10 files changed

+297
-105
lines changed

dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os';
12
import type { PlaywrightTestConfig } from '@playwright/test';
23
import { devices } from '@playwright/test';
34

@@ -31,6 +32,8 @@ const config: PlaywrightTestConfig = {
3132
},
3233
/* Run tests in files in parallel */
3334
fullyParallel: true,
35+
/* 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. */
36+
workers: os.cpus().length,
3437
/* Fail the build on CI if you accidentally left test.only in the source code. */
3538
forbidOnly: !!process.env.CI,
3639
/* `next dev` is incredibly buggy with the app dir */

dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os';
12
import type { PlaywrightTestConfig } from '@playwright/test';
23
import { devices } from '@playwright/test';
34

@@ -29,6 +30,8 @@ const config: PlaywrightTestConfig = {
2930
*/
3031
timeout: 10000,
3132
},
33+
/* 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. */
34+
workers: os.cpus().length,
3235
/* Run tests in files in parallel */
3336
fullyParallel: true,
3437
/* Fail the build on CI if you accidentally left test.only in the source code. */

packages/nextjs/src/client/browserTracingIntegration.ts

+73-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { BrowserTracing as OriginalBrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/react';
1+
import {
2+
BrowserTracing as OriginalBrowserTracing,
3+
browserTracingIntegration as originalBrowserTracingIntegration,
4+
defaultRequestInstrumentationOptions,
5+
startBrowserTracingNavigationSpan,
6+
startBrowserTracingPageLoadSpan,
7+
} from '@sentry/react';
8+
import type { Integration, StartSpanOptions } from '@sentry/types';
29
import { nextRouterInstrumentation } from '../index.client';
310

411
/**
512
* A custom BrowserTracing integration for Next.js.
13+
*
14+
* @deprecated Use `browserTracingIntegration` instead.
615
*/
716
export class BrowserTracing extends OriginalBrowserTracing {
817
public constructor(options?: ConstructorParameters<typeof OriginalBrowserTracing>[0]) {
@@ -19,8 +28,71 @@ export class BrowserTracing extends OriginalBrowserTracing {
1928
]
2029
: // eslint-disable-next-line deprecation/deprecation
2130
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
31+
// eslint-disable-next-line deprecation/deprecation
2232
routingInstrumentation: nextRouterInstrumentation,
2333
...options,
2434
});
2535
}
2636
}
37+
38+
/**
39+
* A custom BrowserTracing integration for Next.js.
40+
*/
41+
export function browserTracingIntegration(
42+
options?: Parameters<typeof originalBrowserTracingIntegration>[0],
43+
): Integration {
44+
const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
45+
// eslint-disable-next-line deprecation/deprecation
46+
tracingOrigins:
47+
process.env.NODE_ENV === 'development'
48+
? [
49+
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
50+
// has cors and it doesn't like extra headers when it's accessed from a different URL.
51+
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
52+
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
53+
/^\/(?!\/)/,
54+
]
55+
: // eslint-disable-next-line deprecation/deprecation
56+
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
57+
...options,
58+
instrumentNavigation: false,
59+
instrumentPageLoad: false,
60+
});
61+
62+
return {
63+
...browserTracingIntegrationInstance,
64+
afterAllSetup(client) {
65+
const startPageloadCallback = (startSpanOptions: StartSpanOptions): void => {
66+
startBrowserTracingPageLoadSpan(client, startSpanOptions);
67+
};
68+
69+
const startNavigationCallback = (startSpanOptions: StartSpanOptions): void => {
70+
startBrowserTracingNavigationSpan(client, startSpanOptions);
71+
};
72+
73+
// We need to run the navigation span instrumentation before the `afterAllSetup` hook on the normal browser
74+
// tracing integration because we need to ensure the order of execution is as follows:
75+
// Instrumentation to start span on RSC fetch request runs -> Instrumentation to put tracing headers from active span on fetch runs
76+
// If it were the other way around, the RSC fetch request would not receive the tracing headers from the navigation transaction.
77+
// eslint-disable-next-line deprecation/deprecation
78+
nextRouterInstrumentation(
79+
() => undefined,
80+
false,
81+
options?.instrumentNavigation,
82+
startPageloadCallback,
83+
startNavigationCallback,
84+
);
85+
86+
browserTracingIntegrationInstance.afterAllSetup(client);
87+
88+
// eslint-disable-next-line deprecation/deprecation
89+
nextRouterInstrumentation(
90+
() => undefined,
91+
options?.instrumentPageLoad,
92+
false,
93+
startPageloadCallback,
94+
startNavigationCallback,
95+
);
96+
},
97+
};
98+
}

packages/nextjs/src/client/index.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
2-
import type { BrowserOptions, browserTracingIntegration } from '@sentry/react';
2+
import type { BrowserOptions } from '@sentry/react';
33
import {
44
Integrations as OriginalIntegrations,
55
getCurrentScope,
@@ -10,11 +10,13 @@ import type { EventProcessor, Integration } from '@sentry/types';
1010

1111
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
1212
import { getVercelEnv } from '../common/getVercelEnv';
13+
import { browserTracingIntegration } from './browserTracingIntegration';
1314
import { BrowserTracing } from './browserTracingIntegration';
1415
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
1516
import { applyTunnelRouteOption } from './tunnelRoute';
1617

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

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

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

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

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

108113
// If BrowserTracing was added, but it is not our forked version,
109114
// replace it with our forked version with the same options
115+
// eslint-disable-next-line deprecation/deprecation
110116
if (!(browserTracing instanceof BrowserTracing)) {
117+
// eslint-disable-next-line deprecation/deprecation
111118
const options: ConstructorParameters<typeof BrowserTracing>[0] = (browserTracing as BrowserTracing).options;
112119
// This option is overwritten by the custom integration
113120
delete options.routingInstrumentation;
114121
// eslint-disable-next-line deprecation/deprecation
115122
delete options.tracingOrigins;
123+
// eslint-disable-next-line deprecation/deprecation
116124
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
117125
}
118126

@@ -126,7 +134,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] {
126134
// will get treeshaken away
127135
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
128136
if (hasTracingEnabled(options)) {
129-
customDefaultIntegrations.push(new BrowserTracing());
137+
customDefaultIntegrations.push(browserTracingIntegration());
130138
}
131139
}
132140

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

151+
export { browserTracingIntegration } from './browserTracingIntegration';
152+
143153
export * from '../common';

packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
11
import { WINDOW } from '@sentry/react';
2-
import type { Primitive, Transaction, TransactionContext } from '@sentry/types';
2+
import type { Primitive, Span, StartSpanOptions, Transaction, TransactionContext } from '@sentry/types';
33
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils';
44

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

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

1112
/**
12-
* Instruments the Next.js Clientside App Router.
13+
* Instruments the Next.js Client App Router.
1314
*/
15+
// TODO(v8): Clean this function up by splitting into pageload and navigation instrumentation respectively. Also remove startTransactionCb in the process.
1416
export function appRouterInstrumentation(
1517
startTransactionCb: StartTransactionCb,
1618
startTransactionOnPageLoad: boolean = true,
1719
startTransactionOnLocationChange: boolean = true,
20+
startPageloadSpanCallback: StartSpanCb,
21+
startNavigationSpanCallback: StartSpanCb,
1822
): void {
1923
// We keep track of the active transaction so we can finish it when we start a navigation transaction.
20-
let activeTransaction: Transaction | undefined = undefined;
24+
let activeTransaction: Span | undefined = undefined;
2125

2226
// We keep track of the previous location name so we can set the `from` field on navigation transactions.
2327
// This is either a route or a pathname.
2428
let prevLocationName = WINDOW.location.pathname;
2529

2630
if (startTransactionOnPageLoad) {
27-
activeTransaction = startTransactionCb({
31+
const transactionContext = {
2832
name: prevLocationName,
2933
op: 'pageload',
3034
origin: 'auto.pageload.nextjs.app_router_instrumentation',
3135
tags: DEFAULT_TAGS,
3236
// pageload should always start at timeOrigin (and needs to be in s, not ms)
3337
startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
3438
metadata: { source: 'url' },
35-
});
39+
} as const;
40+
activeTransaction = startTransactionCb(transactionContext);
41+
startPageloadSpanCallback(transactionContext);
3642
}
3743

3844
if (startTransactionOnLocationChange) {
@@ -66,13 +72,16 @@ export function appRouterInstrumentation(
6672
activeTransaction.end();
6773
}
6874

69-
startTransactionCb({
75+
const transactionContext = {
7076
name: transactionName,
7177
op: 'navigation',
7278
origin: 'auto.navigation.nextjs.app_router_instrumentation',
7379
tags,
7480
metadata: { source: 'url' },
75-
});
81+
} as const;
82+
83+
startTransactionCb(transactionContext);
84+
startNavigationSpanCallback(transactionContext);
7685
});
7786
}
7887
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
import { WINDOW } from '@sentry/react';
2-
import type { Transaction, TransactionContext } from '@sentry/types';
2+
import type { StartSpanOptions, Transaction, TransactionContext } from '@sentry/types';
33

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

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

910
/**
10-
* Instruments the Next.js Clientside Router.
11+
* Instruments the Next.js Client Router.
12+
*
13+
* @deprecated Use `browserTracingIntegration()` as exported from `@sentry/nextjs` instead.
1114
*/
1215
export function nextRouterInstrumentation(
1316
startTransactionCb: StartTransactionCb,
1417
startTransactionOnPageLoad: boolean = true,
1518
startTransactionOnLocationChange: boolean = true,
19+
startPageloadSpanCallback?: StartSpanCb,
20+
startNavigationSpanCallback?: StartSpanCb,
1621
): void {
1722
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
1823
if (isAppRouter) {
19-
appRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange);
24+
appRouterInstrumentation(
25+
startTransactionCb,
26+
startTransactionOnPageLoad,
27+
startTransactionOnLocationChange,
28+
startPageloadSpanCallback || (() => undefined),
29+
startNavigationSpanCallback || (() => undefined),
30+
);
2031
} else {
21-
pagesRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange);
32+
pagesRouterInstrumentation(
33+
startTransactionCb,
34+
startTransactionOnPageLoad,
35+
startTransactionOnLocationChange,
36+
startPageloadSpanCallback || (() => undefined),
37+
startNavigationSpanCallback || (() => undefined),
38+
);
2239
}
2340
}

packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ParsedUrlQuery } from 'querystring';
22
import { getClient, getCurrentScope } from '@sentry/core';
33
import { WINDOW } from '@sentry/react';
4-
import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
4+
import type { Primitive, StartSpanOptions, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
55
import {
66
browserPerformanceTimeOrigin,
77
logger,
@@ -20,6 +20,7 @@ const globalObject = WINDOW as typeof WINDOW & {
2020
};
2121

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

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

131134
if (startTransactionOnPageLoad) {
132135
const source = route ? 'route' : 'url';
133-
activeTransaction = startTransactionCb({
136+
const transactionContext = {
134137
name: prevLocationName,
135138
op: 'pageload',
136139
origin: 'auto.pageload.nextjs.pages_router_instrumentation',
@@ -143,7 +146,9 @@ export function pagesRouterInstrumentation(
143146
source,
144147
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
145148
},
146-
});
149+
} as const;
150+
activeTransaction = startTransactionCb(transactionContext);
151+
startPageloadSpanCallback(transactionContext);
147152
}
148153

149154
if (startTransactionOnLocationChange) {
@@ -173,13 +178,15 @@ export function pagesRouterInstrumentation(
173178
activeTransaction.end();
174179
}
175180

176-
const navigationTransaction = startTransactionCb({
181+
const transactionContext = {
177182
name: transactionName,
178183
op: 'navigation',
179184
origin: 'auto.navigation.nextjs.pages_router_instrumentation',
180185
tags,
181186
metadata: { source: transactionSource },
182-
});
187+
} as const;
188+
const navigationTransaction = startTransactionCb(transactionContext);
189+
startNavigationSpanCallback(transactionContext);
183190

184191
if (navigationTransaction) {
185192
// In addition to the navigation transaction we're also starting a span to mark Next.js's `routeChangeStart`

0 commit comments

Comments
 (0)