Skip to content

Commit f546d0f

Browse files
lobsterkatieAbhiPrasadiker-barriocanalHazAT
authored
feat(nextjs): Frontend + withSentry Performance Monitoring (#3580)
Co-authored-by: Abhijeet Prasad <[email protected]> Co-authored-by: iker barriocanal <[email protected]> Co-authored-by: Daniel Griesser <[email protected]>
1 parent 752856c commit f546d0f

29 files changed

+992
-95
lines changed

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@sentry/integrations": "6.4.1",
2222
"@sentry/node": "6.4.1",
2323
"@sentry/react": "6.4.1",
24+
"@sentry/tracing": "6.4.1",
2425
"@sentry/utils": "6.4.1",
2526
"@sentry/webpack-plugin": "1.15.0",
2627
"tslib": "^1.9.3"

packages/nextjs/src/index.client.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
11
import { configureScope, init as reactInit } from '@sentry/react';
2+
import { defaultRequestInstrumentationOptions, Integrations } from '@sentry/tracing';
23

4+
import { nextRouterInstrumentation } from './performance/client';
35
import { MetadataBuilder } from './utils/metadataBuilder';
46
import { NextjsOptions } from './utils/nextjsOptions';
7+
import { addIntegration, UserIntegrations } from './utils/userIntegrations';
58

69
export * from '@sentry/react';
10+
export { nextRouterInstrumentation } from './performance/client';
11+
12+
const { BrowserTracing } = Integrations;
713

814
/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
915
export function init(options: NextjsOptions): void {
1016
const metadataBuilder = new MetadataBuilder(options, ['nextjs', 'react']);
1117
metadataBuilder.addSdkMetadata();
1218
options.environment = options.environment || process.env.NODE_ENV;
13-
reactInit(options);
19+
20+
// Only add BrowserTracing if a tracesSampleRate or tracesSampler is set
21+
const integrations =
22+
options.tracesSampleRate === undefined && options.tracesSampler === undefined
23+
? options.integrations
24+
: createClientIntegrations(options.integrations);
25+
26+
reactInit({
27+
...options,
28+
integrations,
29+
});
1430
configureScope(scope => {
1531
scope.setTag('runtime', 'browser');
1632
});
1733
}
34+
35+
const defaultBrowserTracingIntegration = new BrowserTracing({
36+
tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
37+
routingInstrumentation: nextRouterInstrumentation,
38+
});
39+
40+
function createClientIntegrations(integrations?: UserIntegrations): UserIntegrations {
41+
if (integrations) {
42+
return addIntegration(defaultBrowserTracingIntegration, integrations, {
43+
BrowserTracing: { keyPath: 'options.routingInstrumentation', value: nextRouterInstrumentation },
44+
});
45+
} else {
46+
return [defaultBrowserTracingIntegration];
47+
}
48+
}

packages/nextjs/src/index.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function init(options: NextjsOptions): void {
1717
const metadataBuilder = new MetadataBuilder(options, ['nextjs', 'node']);
1818
metadataBuilder.addSdkMetadata();
1919
options.environment = options.environment || process.env.NODE_ENV;
20+
// TODO capture project root and store in an env var for RewriteFrames?
2021
addServerIntegrations(options);
2122
// Right now we only capture frontend sessions for Next.js
2223
options.autoSessionTracking = false;
@@ -47,5 +48,5 @@ function addServerIntegrations(options: NextjsOptions): void {
4748
export { withSentryConfig } from './utils/config';
4849
export { withSentry } from './utils/handlers';
4950

50-
// TODO capture project root (which this returns) for RewriteFrames?
51+
// wrap various server methods to enable error monitoring and tracing
5152
instrumentServer();
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Primitive, Transaction, TransactionContext } from '@sentry/types';
2+
import { fill, getGlobalObject, stripUrlQueryAndFragment } from '@sentry/utils';
3+
import { default as Router } from 'next/router';
4+
5+
const global = getGlobalObject<Window>();
6+
7+
type StartTransactionCb = (context: TransactionContext) => Transaction | undefined;
8+
9+
const DEFAULT_TAGS = Object.freeze({
10+
'routing.instrumentation': 'next-router',
11+
});
12+
13+
let activeTransaction: Transaction | undefined = undefined;
14+
let prevTransactionName: string | undefined = undefined;
15+
let startTransaction: StartTransactionCb | undefined = undefined;
16+
17+
/**
18+
* Creates routing instrumention for Next Router. Only supported for
19+
* client side routing. Works for Next >= 10.
20+
*
21+
* Leverages the SingletonRouter from the `next/router` to
22+
* generate pageload/navigation transactions and parameterize
23+
* transaction names.
24+
*/
25+
export function nextRouterInstrumentation(
26+
startTransactionCb: StartTransactionCb,
27+
startTransactionOnPageLoad: boolean = true,
28+
startTransactionOnLocationChange: boolean = true,
29+
): void {
30+
startTransaction = startTransactionCb;
31+
Router.ready(() => {
32+
// We can only start the pageload transaction when we have access to the parameterized
33+
// route name. Setting the transaction name after the transaction is started could lead
34+
// to possible race conditions with the router, so this approach was taken.
35+
if (startTransactionOnPageLoad) {
36+
prevTransactionName = Router.route !== null ? stripUrlQueryAndFragment(Router.route) : global.location.pathname;
37+
activeTransaction = startTransactionCb({
38+
name: prevTransactionName,
39+
op: 'pageload',
40+
tags: DEFAULT_TAGS,
41+
});
42+
}
43+
44+
// Spans that aren't attached to any transaction are lost; so if transactions aren't
45+
// created (besides potentially the onpageload transaction), no need to wrap the router.
46+
if (!startTransactionOnLocationChange) return;
47+
48+
// `withRouter` uses `useRouter` underneath:
49+
// https://github.com/vercel/next.js/blob/de42719619ae69fbd88e445100f15701f6e1e100/packages/next/client/with-router.tsx#L21
50+
// Router events also use the router:
51+
// https://github.com/vercel/next.js/blob/de42719619ae69fbd88e445100f15701f6e1e100/packages/next/client/router.ts#L92
52+
// `Router.changeState` handles the router state changes, so it may be enough to only wrap it
53+
// (instead of wrapping all of the Router's functions).
54+
const routerPrototype = Object.getPrototypeOf(Router.router);
55+
fill(routerPrototype, 'changeState', changeStateWrapper);
56+
});
57+
}
58+
59+
type RouterChangeState = (
60+
method: string,
61+
url: string,
62+
as: string,
63+
options: Record<string, any>,
64+
...args: any[]
65+
) => void;
66+
type WrappedRouterChangeState = RouterChangeState;
67+
68+
/**
69+
* Wraps Router.changeState()
70+
* https://github.com/vercel/next.js/blob/da97a18dafc7799e63aa7985adc95f213c2bf5f3/packages/next/next-server/lib/router/router.ts#L1204
71+
* Start a navigation transaction every time the router changes state.
72+
*/
73+
function changeStateWrapper(originalChangeStateWrapper: RouterChangeState): WrappedRouterChangeState {
74+
const wrapper = function(
75+
this: any,
76+
method: string,
77+
// The parameterized url, ex. posts/[id]/[comment]
78+
url: string,
79+
// The actual url, ex. posts/85/my-comment
80+
as: string,
81+
options: Record<string, any>,
82+
// At the moment there are no additional arguments (meaning the rest parameter is empty).
83+
// This is meant to protect from future additions to Next.js API, especially since this is an
84+
// internal API.
85+
...args: any[]
86+
): Promise<boolean> {
87+
if (startTransaction !== undefined) {
88+
if (activeTransaction) {
89+
activeTransaction.finish();
90+
}
91+
const tags: Record<string, Primitive> = {
92+
...DEFAULT_TAGS,
93+
method,
94+
...options,
95+
};
96+
if (prevTransactionName) {
97+
tags.from = prevTransactionName;
98+
}
99+
prevTransactionName = stripUrlQueryAndFragment(url);
100+
activeTransaction = startTransaction({
101+
name: prevTransactionName,
102+
op: 'navigation',
103+
tags,
104+
});
105+
}
106+
return originalChangeStateWrapper.call(this, method, url, as, options, ...args);
107+
};
108+
return wrapper;
109+
}

0 commit comments

Comments
 (0)