Skip to content

Commit 931a0f0

Browse files
committed
ref(nextjs): Clean up browser tracing integration
1 parent b1a6a43 commit 931a0f0

File tree

6 files changed

+373
-356
lines changed

6 files changed

+373
-356
lines changed
Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import {
2-
browserTracingIntegration as originalBrowserTracingIntegration,
3-
startBrowserTracingNavigationSpan,
4-
startBrowserTracingPageLoadSpan,
5-
} from '@sentry/react';
6-
import type { Integration, StartSpanOptions } from '@sentry/types';
7-
import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
1+
import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react';
2+
import type { Integration } from '@sentry/types';
3+
import { nextRouterInstrumentNavigation, nextRouterInstrumentPageLoad } from './routing/nextRoutingInstrumentation';
84

95
/**
106
* A custom browser tracing integration for Next.js.
@@ -18,36 +14,24 @@ export function browserTracingIntegration(
1814
instrumentPageLoad: false,
1915
});
2016

17+
const { instrumentPageLoad = true, instrumentNavigation = true } = options;
18+
2119
return {
2220
...browserTracingIntegrationInstance,
2321
afterAllSetup(client) {
24-
const startPageloadCallback = (startSpanOptions: StartSpanOptions): void => {
25-
startBrowserTracingPageLoadSpan(client, startSpanOptions);
26-
};
27-
28-
const startNavigationCallback = (startSpanOptions: StartSpanOptions): void => {
29-
startBrowserTracingNavigationSpan(client, startSpanOptions);
30-
};
31-
3222
// We need to run the navigation span instrumentation before the `afterAllSetup` hook on the normal browser
3323
// tracing integration because we need to ensure the order of execution is as follows:
3424
// Instrumentation to start span on RSC fetch request runs -> Instrumentation to put tracing headers from active span on fetch runs
3525
// If it were the other way around, the RSC fetch request would not receive the tracing headers from the navigation transaction.
36-
nextRouterInstrumentation(
37-
false,
38-
options.instrumentNavigation === undefined ? true : options.instrumentNavigation,
39-
startPageloadCallback,
40-
startNavigationCallback,
41-
);
26+
if (instrumentNavigation) {
27+
nextRouterInstrumentNavigation(client);
28+
}
4229

4330
browserTracingIntegrationInstance.afterAllSetup(client);
4431

45-
nextRouterInstrumentation(
46-
options.instrumentPageLoad === undefined ? true : options.instrumentPageLoad,
47-
false,
48-
startPageloadCallback,
49-
startNavigationCallback,
50-
);
32+
if (instrumentPageLoad) {
33+
nextRouterInstrumentPageLoad(client);
34+
}
5135
},
5236
};
5337
}

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

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,55 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
44
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
55
} from '@sentry/core';
6-
import { WINDOW } from '@sentry/react';
7-
import type { StartSpanOptions } from '@sentry/types';
6+
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
7+
import type { Client } from '@sentry/types';
88
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils';
99

10-
type StartSpanCb = (context: StartSpanOptions) => void;
11-
12-
/**
13-
* Instruments the Next.js Client App Router.
14-
*/
15-
export function appRouterInstrumentation(
16-
shouldInstrumentPageload: boolean,
17-
shouldInstrumentNavigation: boolean,
18-
startPageloadSpanCallback: StartSpanCb,
19-
startNavigationSpanCallback: StartSpanCb,
20-
): void {
21-
// We keep track of the previous location name so we can set the `from` field on navigation transactions.
22-
// This is either a route or a pathname.
23-
let currPathname = WINDOW.location.pathname;
24-
25-
if (shouldInstrumentPageload) {
26-
startPageloadSpanCallback({
27-
name: currPathname,
28-
// pageload should always start at timeOrigin (and needs to be in s, not ms)
29-
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
30-
attributes: {
31-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
32-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation',
33-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
34-
},
35-
});
36-
}
10+
/** Instruments the Next.js app router for pageloads. */
11+
export function appRouterInstrumentPageLoad(client: Client): void {
12+
startBrowserTracingPageLoadSpan(client, {
13+
name: WINDOW.location.pathname,
14+
// pageload should always start at timeOrigin (and needs to be in s, not ms)
15+
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
16+
attributes: {
17+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
18+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation',
19+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
20+
},
21+
});
22+
}
3723

38-
if (shouldInstrumentNavigation) {
39-
addFetchInstrumentationHandler(handlerData => {
40-
// The instrumentation handler is invoked twice - once for starting a request and once when the req finishes
41-
// We can use the existence of the end-timestamp to filter out "finishing"-events.
42-
if (handlerData.endTimestamp !== undefined) {
43-
return;
44-
}
24+
/** Instruments the Next.js app router for navigation. */
25+
export function appRouterInstrumentNavigation(client: Client): void {
26+
addFetchInstrumentationHandler(handlerData => {
27+
// The instrumentation handler is invoked twice - once for starting a request and once when the req finishes
28+
// We can use the existence of the end-timestamp to filter out "finishing"-events.
29+
if (handlerData.endTimestamp !== undefined) {
30+
return;
31+
}
4532

46-
// Only GET requests can be navigating RSC requests
47-
if (handlerData.fetchData.method !== 'GET') {
48-
return;
49-
}
33+
// Only GET requests can be navigating RSC requests
34+
if (handlerData.fetchData.method !== 'GET') {
35+
return;
36+
}
5037

51-
const parsedNavigatingRscFetchArgs = parseNavigatingRscFetchArgs(handlerData.args);
38+
const parsedNavigatingRscFetchArgs = parseNavigatingRscFetchArgs(handlerData.args);
5239

53-
if (parsedNavigatingRscFetchArgs === null) {
54-
return;
55-
}
40+
if (parsedNavigatingRscFetchArgs === null) {
41+
return;
42+
}
5643

57-
const newPathname = parsedNavigatingRscFetchArgs.targetPathname;
58-
currPathname = newPathname;
44+
const newPathname = parsedNavigatingRscFetchArgs.targetPathname;
5945

60-
startNavigationSpanCallback({
61-
name: newPathname,
62-
attributes: {
63-
from: currPathname,
64-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
65-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
66-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
67-
},
68-
});
46+
startBrowserTracingNavigationSpan(client, {
47+
name: newPathname,
48+
attributes: {
49+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
50+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
51+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
52+
},
6953
});
70-
}
54+
});
7155
}
7256

7357
function parseNavigatingRscFetchArgs(fetchArgs: unknown[]): null | {
Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
import { WINDOW } from '@sentry/react';
2-
import type { StartSpanOptions } from '@sentry/types';
2+
import type { Client } from '@sentry/types';
33

4-
import { appRouterInstrumentation } from './appRouterRoutingInstrumentation';
5-
import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation';
4+
import { appRouterInstrumentNavigation, appRouterInstrumentPageLoad } from './appRouterRoutingInstrumentation';
5+
import { pagesRouterInstrumentNavigation, pagesRouterInstrumentPageLoad } from './pagesRouterRoutingInstrumentation';
66

7-
type StartSpanCb = (context: StartSpanOptions) => void;
7+
/**
8+
* Instruments the Next.js Client Router for page loads.
9+
*/
10+
export function nextRouterInstrumentPageLoad(client: Client): void {
11+
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
12+
if (isAppRouter) {
13+
appRouterInstrumentPageLoad(client);
14+
} else {
15+
pagesRouterInstrumentPageLoad(client);
16+
}
17+
}
818

919
/**
10-
* Instruments the Next.js Client Router.
20+
* Instruments the Next.js Client Router for navigation.
1121
*/
12-
export function nextRouterInstrumentation(
13-
shouldInstrumentPageload: boolean,
14-
shouldInstrumentNavigation: boolean,
15-
startPageloadSpanCallback: StartSpanCb,
16-
startNavigationSpanCallback: StartSpanCb,
17-
): void {
22+
export function nextRouterInstrumentNavigation(client: Client): void {
1823
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
1924
if (isAppRouter) {
20-
appRouterInstrumentation(
21-
shouldInstrumentPageload,
22-
shouldInstrumentNavigation,
23-
startPageloadSpanCallback,
24-
startNavigationSpanCallback,
25-
);
25+
appRouterInstrumentNavigation(client);
2626
} else {
27-
pagesRouterInstrumentation(
28-
shouldInstrumentPageload,
29-
shouldInstrumentNavigation,
30-
startPageloadSpanCallback,
31-
startNavigationSpanCallback,
32-
);
27+
pagesRouterInstrumentNavigation(client);
3328
}
3429
}

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

Lines changed: 65 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_OP,
44
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
55
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
6-
getClient,
6+
continueTrace,
7+
getCurrentScope,
8+
withScope,
79
} from '@sentry/core';
8-
import { WINDOW } from '@sentry/react';
9-
import type { StartSpanOptions, TransactionSource } from '@sentry/types';
10-
import {
11-
browserPerformanceTimeOrigin,
12-
logger,
13-
propagationContextFromHeaders,
14-
stripUrlQueryAndFragment,
15-
} from '@sentry/utils';
10+
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
11+
import type { Client, TransactionSource } from '@sentry/types';
12+
import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils';
1613
import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils';
1714
import RouterImport from 'next/router';
1815

@@ -30,8 +27,6 @@ const globalObject = WINDOW as typeof WINDOW & {
3027
};
3128
};
3229

33-
type StartSpanCb = (context: StartSpanOptions) => void;
34-
3530
/**
3631
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
3732
*/
@@ -104,73 +99,77 @@ function extractNextDataTagInformation(): NextDataTagInfo {
10499
}
105100

106101
/**
107-
* Instruments the Next.js pages router. Only supported for
108-
* client side routing. Works for Next >= 10.
102+
* Instruments the Next.js pages router for pageloads.
103+
* Only supported for client side routing. Works for Next >= 10.
109104
*
110105
* Leverages the SingletonRouter from the `next/router` to
111106
* generate pageload/navigation transactions and parameterize
112107
* transaction names.
113108
*/
114-
export function pagesRouterInstrumentation(
115-
shouldInstrumentPageload: boolean,
116-
shouldInstrumentNavigation: boolean,
117-
startPageloadSpanCallback: StartSpanCb,
118-
startNavigationSpanCallback: StartSpanCb,
119-
): void {
109+
export function pagesRouterInstrumentPageLoad(client: Client): void {
120110
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
121-
const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
122-
let prevLocationName = route || globalObject.location.pathname;
123-
124-
if (shouldInstrumentPageload) {
125-
const client = getClient();
126-
startPageloadSpanCallback({
127-
name: prevLocationName,
128-
// pageload should always start at timeOrigin (and needs to be in s, not ms)
129-
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
130-
traceId,
131-
parentSpanId,
132-
parentSampled: sampled,
133-
...(params && client && client.getOptions().sendDefaultPii && { data: params }),
134-
attributes: {
135-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
136-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation',
137-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
138-
},
139-
metadata: {
140-
dynamicSamplingContext: dsc,
141-
},
142-
});
143-
}
111+
const name = route || globalObject.location.pathname;
112+
113+
// Continue trace updates the _current_ scope, but we want to break out of it again...
114+
// This is a bit hacky, because we want to get the span to use both the correct scope _and_ the correct propagation context
115+
// but wards, we want to reset it to avoid this also applying to other spans
116+
const scope = getCurrentScope();
117+
const propagationContextBefore = scope.getPropagationContext();
118+
119+
continueTrace({ sentryTrace, baggage }, () => {
120+
// Ensure we are on the original current scope again, so the span is set as active on it
121+
return withScope(scope, () => {
122+
startBrowserTracingPageLoadSpan(client, {
123+
name,
124+
// pageload should always start at timeOrigin (and needs to be in s, not ms)
125+
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
144126

145-
if (shouldInstrumentNavigation) {
146-
Router.events.on('routeChangeStart', (navigationTarget: string) => {
147-
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
148-
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);
149-
150-
let newLocation: string;
151-
let spanSource: TransactionSource;
152-
153-
if (matchedRoute) {
154-
newLocation = matchedRoute;
155-
spanSource = 'route';
156-
} else {
157-
newLocation = strippedNavigationTarget;
158-
spanSource = 'url';
159-
}
160-
161-
startNavigationSpanCallback({
162-
name: newLocation,
163127
attributes: {
164-
from: prevLocationName,
165-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
166-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
167-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
128+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
129+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation',
130+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
131+
...(params && client.getOptions().sendDefaultPii && { ...params }),
168132
},
169133
});
134+
});
135+
});
136+
137+
scope.setPropagationContext(propagationContextBefore);
138+
}
170139

171-
prevLocationName = newLocation;
140+
/**
141+
* Instruments the Next.js pages router for navigation.
142+
* Only supported for client side routing. Works for Next >= 10.
143+
*
144+
* Leverages the SingletonRouter from the `next/router` to
145+
* generate pageload/navigation transactions and parameterize
146+
* transaction names.
147+
*/
148+
export function pagesRouterInstrumentNavigation(client: Client): void {
149+
Router.events.on('routeChangeStart', (navigationTarget: string) => {
150+
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
151+
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);
152+
153+
let newLocation: string;
154+
let spanSource: TransactionSource;
155+
156+
if (matchedRoute) {
157+
newLocation = matchedRoute;
158+
spanSource = 'route';
159+
} else {
160+
newLocation = strippedNavigationTarget;
161+
spanSource = 'url';
162+
}
163+
164+
startBrowserTracingNavigationSpan(client, {
165+
name: newLocation,
166+
attributes: {
167+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
168+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
169+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
170+
},
172171
});
173-
}
172+
});
174173
}
175174

176175
function getNextRouteFromPathname(pathname: string): string | undefined {

0 commit comments

Comments
 (0)