8000 ref(nextjs): Clean up browser tracing integration (#11022) · jonator/sentry-javascript@6088078 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6088078

Browse files
authored
ref(nextjs): Clean up browser tracing integration (getsentry#11022)
Updates the app & pages router instrumentation to avoid depreacted options & streamline it overall. This required a change to browser tracing, which is to allow to provide a custom `sentryTrace` / `baggage` string value to continue a trace from. Before this, we always picked up the trace from `meta` tag, which meant that if we tried to continue a trace manually in nextjs, then called the default browser tracing integration, it would just overwrite it. Now, you can optionally pass this like this: ```js startBrowserTracingPageLoadSpan( client, { name }, { sentryTrace, baggage } }); ``` And this will take presedence over looking at the meta tags! This also means we don't need any custom scope/trace handling in nextjs there anymore, which is also nice!
1 parent 3004c38 commit 6088078

13 files changed

+484
-399
lines changed

packages/core/src/baseclient.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,13 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
450450
): void;
451451

452452
/** @inheritdoc */
453-
public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
453+
public on(
454+
hook: 'start A3E2 PageLoadSpan',
455+
callback: (
456+
options: StartSpanOptions,
457+
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
458+
) => void,
459+
): void;
454460

455461
/** @inheritdoc */
456462
public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
@@ -500,7 +506,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
500506
public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void;
501507

502508
/** @inheritdoc */
503-
public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
509+
public emit(
510+
hook: 'startPageLoadSpan',
511+
options: StartSpanOptions,
512+
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
513+
): void;
504514

505515
/** @inheritdoc */
506516
public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void;
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+
}
42 10000 29

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: 48 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,10 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_OP,
44
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
55
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
6-
getClient,
76
} 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';
7+
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
8+
import type { Client, TransactionSource } from '@sentry/types';
9+
import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils';
1610
import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils';
1711
import RouterImport from 'next/router';
1812

@@ -30,8 +24,6 @@ const globalObject = WINDOW as typeof WINDOW & {
3024
};
3125
};
3226

33-
type StartSpanCb = (context: StartSpanOptions) => void;
34-
3527
/**
3628
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
3729
*/
@@ -104,73 +96,67 @@ function extractNextDataTagInformation(): NextDataTagInfo {
10496
}
10597

10698
/**
107-
* Instruments the Next.js pages router. Only supported for
108-
* client side routing. Works for Next >= 10.
99+
* Instruments the Next.js pages router for pageloads.
100+
* Only supported for client side routing. Works for Next >= 10.
109101
*
110102
* Leverages the SingletonRouter from the `next/router` to
111103
* generate pageload/navigation transactions and parameterize
112104
* transaction names.
113105
*/
114-
export function pagesRouterInstrumentation(
115-
shouldInstrumentPageload: boolean,
116-
shouldInstrumentNavigation: boolean,
117-
startPageloadSpanCallback: StartSpanCb,
118-
startNavigationSpanCallback: StartSpanCb,
119-
): void {
106+
export function pagesRouterInstrumentPageLoad(client: Client): void {
120107
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
121-
const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
122-
let prevLocationName = route || globalObject.location.pathname;
108+
const name = route || globalObject.location.pathname;
123109

124-
if (shouldInstrumentPageload) {
125-
const client = getClient();
126-
startPageloadSpanCallback({
127-
name: prevLocationName,
110+
startBrowserTracingPageLoadSpan(
111+
client,
112+
{
113+
name,
128114
// pageload should always start at timeOrigin (and needs to be in s, not ms)
129115
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
130-
traceId,
131-
parentSpanId,
132-
parentSampled: sampled,
133-
...(params && client && client.getOptions().sendDefaultPii && { data: params }),
134116
attributes: {
135117
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
136118
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation',
137119
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
120+
...(params && client.getOptions().sendDefaultPii && { ...params }),
138121
},
139-
metadata: {
140-
dynamicSamplingContext: dsc,
141-
},
142-
});
143-
}
122+
},
123+
{ sentryTrace, baggage },
124+
);
125+
}
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,
163-
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,
168-
},
169-
});
170-
171-
prevLocationName = newLocation;
127+
/**
128+
* Instruments the Next.js pages router for navigation.
129+
* Only supported for client side routing. Works for Next >= 10.
130+
*
131+
* Leverages the SingletonRouter from the `next/router` to
132+
* generate pageload/navigation transactions and parameterize
133+
* transaction names.
134+
*/
135+
export function pagesRouterInstrumentNavigation(client: Client): void {
136+
Router.events.on('routeChangeStart', (navigationTarget: string) => {
137+
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
138+
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);
139+
140+
let newLocation: string;
141+
let spanSource: TransactionSource;
142+
143+
if (matchedRoute) {
144+
newLocation = matchedRoute;
145+
spanSource = 'route';
146+
} else {
147+
newLocation = strippedNavigationTarget;
148+
spanSource = 'url';
149+
}
150+
151+
startBrowserTracingNavigationSpan(client, {
152+
name: newLocation,
153+
attributes: {
154+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
155+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
156+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
157+
},
172158
});
173-
}
159+
});
174160
}
175161

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

0 commit comments

Comments
 (0)
0