Skip to content

Commit 5c462f1

Browse files
authored
feat(nextjs): Instrument server-side getInitialProps of _app, _document and _error (#5604)
1 parent 40bb2de commit 5c462f1

10 files changed

+195
-29
lines changed

packages/nextjs/src/config/loaders/dataFetchersLoader.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -151,22 +151,33 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>,
151151
if (hasDefaultExport(ast)) {
152152
outputFileContent += `
153153
import { default as _sentry_default } from "${this.resourcePath}?sentry-proxy-loader";
154-
import { withSentryGetInitialProps } from "@sentry/nextjs";`;
154+
import {
155+
withSentryServerSideGetInitialProps,
156+
withSentryServerSideAppGetInitialProps,
157+
withSentryServerSideDocumentGetInitialProps,
158+
withSentryServerSideErrorGetInitialProps,
159+
} from "@sentry/nextjs";`;
155160

156161
if (parameterizedRouteName === '/_app') {
157-
// getInitialProps signature is a bit different in _app.js so we need a different wrapper
158-
// Currently a no-op
159-
} else if (parameterizedRouteName === '/_error') {
160-
// getInitialProps behaviour is a bit different in _error.js so we probably want different wrapper
161-
// Currently a no-op
162+
outputFileContent += `
163+
if (typeof _sentry_default.getInitialProps === 'function') {
164+
_sentry_default.getInitialProps = withSentryServerSideAppGetInitialProps(_sentry_default.getInitialProps);
165+
}`;
162166
} else if (parameterizedRouteName === '/_document') {
163-
// getInitialProps signature is a bit different in _document.js so we need a different wrapper
164-
// Currently a no-op
167+
outputFileContent += `
168+
if (typeof _sentry_default.getInitialProps === 'function') {
169+
_sentry_default.getInitialProps = withSentryServerSideDocumentGetInitialProps(_sentry_default.getInitialProps);
170+
}`;
171+
} else if (parameterizedRouteName === '/_error') {
172+
outputFileContent += `
173+
if (typeof _sentry_default.getInitialProps === 'function') {
174+
_sentry_default.getInitialProps = withSentryServerSideErrorGetInitialProps(_sentry_default.getInitialProps);
175+
}`;
165176
} else {
166177
// We enter this branch for any "normal" Next.js page
167178
outputFileContent += `
168179
if (typeof _sentry_default.getInitialProps === 'function') {
169-
_sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps, '${parameterizedRouteName}');
180+
_sentry_default.getInitialProps = withSentryServerSideGetInitialProps(_sentry_default.getInitialProps);
170181
}`;
171182
}
172183

packages/nextjs/src/config/templates/proxyLoaderTemplate.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ const origGetInitialProps = pageComponent.getInitialProps;
2626
const origGetStaticProps = userPageModule.getStaticProps;
2727
const origGetServerSideProps = userPageModule.getServerSideProps;
2828

29+
const getInitialPropsWrappers: Record<string, any> = {
30+
'/_app': Sentry.withSentryServerSideAppGetInitialProps,
31+
'/_document': Sentry.withSentryServerSideDocumentGetInitialProps,
32+
'/_error': Sentry.withSentryServerSideErrorGetInitialProps,
33+
};
34+
35+
const getInitialPropsWrapper = getInitialPropsWrappers['__ROUTE__'] || Sentry.withSentryServerSideGetInitialProps;
36+
2937
if (typeof origGetInitialProps === 'function') {
30-
pageComponent.getInitialProps = Sentry.withSentryServerSideGetInitialProps(
31-
origGetInitialProps,
32-
'__ROUTE__',
33-
) as NextPageComponent['getInitialProps'];
38+
pageComponent.getInitialProps = getInitialPropsWrapper(origGetInitialProps) as NextPageComponent['getInitialProps'];
3439
}
3540

3641
export const getStaticProps =
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export { withSentryGetStaticProps } from './withSentryGetStaticProps';
2-
export { withSentryGetServerSideProps } from './withSentryGetServerSideProps';
32
export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps';
3+
export { withSentryServerSideAppGetInitialProps } from './withSentryServerSideAppGetInitialProps';
4+
export { withSentryServerSideDocumentGetInitialProps } from './withSentryServerSideDocumentGetInitialProps';
5+
export { withSentryServerSideErrorGetInitialProps } from './withSentryServerSideErrorGetInitialProps';
6+
export { withSentryGetServerSideProps } from './withSentryGetServerSideProps';

packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ export function withSentryGetServerSideProps(
2929

3030
if (hasTracingEnabled()) {
3131
return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, {
32-
parameterizedRoute,
33-
functionName: 'getServerSideProps',
32+
dataFetcherRouteName: parameterizedRoute,
33+
requestedRouteName: parameterizedRoute,
34+
dataFetchingMethodName: 'getServerSideProps',
3435
});
3536
} else {
3637
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import App from 'next/app';
3+
4+
import { isBuild } from '../../utils/isBuild';
5+
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
7+
type AppGetInitialProps = typeof App['getInitialProps'];
8+
9+
/**
10+
* Create a wrapped version of the user's exported `getInitialProps` function in
11+
* a custom app ("_app.js").
12+
*
13+
* @param origAppGetInitialProps The user's `getInitialProps` function
14+
* @param parameterizedRoute The page's parameterized route
15+
* @returns A wrapped version of the function
16+
*/
17+
export function withSentryServerSideAppGetInitialProps(origAppGetInitialProps: AppGetInitialProps): AppGetInitialProps {
18+
return async function (
19+
...appGetInitialPropsArguments: Parameters<AppGetInitialProps>
20+
): ReturnType<AppGetInitialProps> {
21+
if (isBuild()) {
22+
return origAppGetInitialProps(...appGetInitialPropsArguments);
23+
}
24+
25+
const [context] = appGetInitialPropsArguments;
26+
const { req, res } = context.ctx;
27+
28+
const errorWrappedAppGetInitialProps = withErrorInstrumentation(origAppGetInitialProps);
29+
30+
if (hasTracingEnabled()) {
31+
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
32+
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
33+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
34+
return callTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, appGetInitialPropsArguments, req!, res!, {
35+
dataFetcherRouteName: '/_app',
36+
requestedRouteName: context.ctx.pathname,
37+
dataFetchingMethodName: 'getInitialProps',
38+
});
39+
} else {
40+
return errorWrappedAppGetInitialProps(...appGetInitialPropsArguments);
41+
}
42+
};
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import Document from 'next/document';
3+
4+
import { isBuild } from '../../utils/isBuild';
5+
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
7+
type DocumentGetInitialProps = typeof Document.getInitialProps;
8+
9+
/**
10+
* Create a wrapped version of the user's exported `getInitialProps` function in
11+
* a custom document ("_document.js").
12+
*
13+
* @param origDocumentGetInitialProps The user's `getInitialProps` function
14+
* @param parameterizedRoute The page's parameterized route
15+
* @returns A wrapped version of the function
16+
*/
17+
export function withSentryServerSideDocumentGetInitialProps(
18+
origDocumentGetInitialProps: DocumentGetInitialProps,
19+
): DocumentGetInitialProps {
20+
return async function (
21+
...documentGetInitialPropsArguments: Parameters<DocumentGetInitialProps>
22+
): ReturnType<DocumentGetInitialProps> {
23+
if (isBuild()) {
24+
return origDocumentGetInitialProps(...documentGetInitialPropsArguments);
25+
}
26+
27+
const [context] = documentGetInitialPropsArguments;
28+
const { req, res } = context;
29+
30+
const errorWrappedGetInitialProps = withErrorInstrumentation(origDocumentGetInitialProps);
31+
32+
if (hasTracingEnabled()) {
33+
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
34+
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
35+
return callTracedServerSideDataFetcher(
36+
errorWrappedGetInitialProps,
37+
documentGetInitialPropsArguments,
38+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
39+
req!,
40+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41+
res!,
42+
{
43+
dataFetcherRouteName: '/_document',
44+
requestedRouteName: context.pathname,
45+
dataFetchingMethodName: 'getInitialProps',
46+
},
47+
);
48+
} else {
49+
return errorWrappedGetInitialProps(...documentGetInitialPropsArguments);
50+
}
51+
};
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { NextPageContext } from 'next';
3+
import { ErrorProps } from 'next/error';
4+
5+
import { isBuild } from '../../utils/isBuild';
6+
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
7+
8+
type ErrorGetInitialProps = (context: NextPageContext) => Promise<ErrorProps>;
9+
10+
/**
11+
* Create a wrapped version of the user's exported `getInitialProps` function in
12+
* a custom error page ("_error.js").
13+
*
14+
* @param origErrorGetInitialProps The user's `getInitialProps` function
15+
* @param parameterizedRoute The page's parameterized route
16+
* @returns A wrapped version of the function
17+
*/
18+
export function withSentryServerSideErrorGetInitialProps(
19+
origErrorGetInitialProps: ErrorGetInitialProps,
20+
): ErrorGetInitialProps {
21+
return async function (
22+
...errorGetInitialPropsArguments: Parameters<ErrorGetInitialProps>
23+
): ReturnType<ErrorGetInitialProps> {
24+
if (isBuild()) {
25+
return origErrorGetInitialProps(...errorGetInitialPropsArguments);
26+
}
27+
28+
const [context] = errorGetInitialPropsArguments;
29+
const { req, res } = context;
30+
31+
const errorWrappedGetInitialProps = withErrorInstrumentation(origErrorGetInitialProps);
32+
33+
if (hasTracingEnabled()) {
34+
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
35+
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
36+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
37+
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, errorGetInitialPropsArguments, req!, res!, {
38+
dataFetcherRouteName: '/_error',
39+
requestedRouteName: context.pathname,
40+
dataFetchingMethodName: 'getInitialProps',
41+
});
42+
} else {
43+
return errorWrappedGetInitialProps(...errorGetInitialPropsArguments);
44+
}
45+
};
46+
}

packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ type GetInitialProps = Required<NextPage>['getInitialProps'];
1313
* @param parameterizedRoute The page's parameterized route
1414
* @returns A wrapped version of the function
1515
*/
16-
export function withSentryServerSideGetInitialProps(
17-
origGetInitialProps: GetInitialProps,
18-
parameterizedRoute: string,
19-
): GetInitialProps {
16+
export function withSentryServerSideGetInitialProps(origGetInitialProps: GetInitialProps): GetInitialProps {
2017
return async function (
2118
...getInitialPropsArguments: Parameters<GetInitialProps>
2219
): Promise<ReturnType<GetInitialProps>> {
@@ -34,8 +31,9 @@ export function withSentryServerSideGetInitialProps(
3431
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
3532
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3633
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
37-
parameterizedRoute,
38-
functionName: 'getInitialProps',
34+
dataFetcherRouteName: context.pathname,
35+
requestedRouteName: context.pathname,
36+
dataFetchingMethodName: 'getInitialProps',
3937
});
4038
} else {
4139
return errorWrappedGetInitialProps(...getInitialPropsArguments);

packages/nextjs/src/config/wrappers/wrapperUtils.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,12 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
7373
req: IncomingMessage,
7474
res: ServerResponse,
7575
options: {
76-
parameterizedRoute: string;
77-
functionName: string;
76+
/** Parameterized route of the request - will be used for naming the transaction. */
77+
requestedRouteName: string;
78+
/** Name of the route the data fetcher was defined in - will be used for describing the data fetcher's span. */
79+
dataFetcherRouteName: string;
80+
/** Name of the data fetching method - will be used for describing the data fetcher's span. */
81+
dataFetchingMethodName: string;
7882
},
7983
): Promise<ReturnType<F>> {
8084
return domain.create().bind(async () => {
@@ -84,8 +88,8 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
8488
// TODO: Extract trace data from `req` object (trace and baggage headers) and attach it to transaction
8589

8690
const newTransaction = startTransaction({
87-
op: 'nextjs.data',
88-
name: options.parameterizedRoute,
91+
op: 'nextjs.data.server',
92+
name: options.requestedRouteName,
8993
metadata: {
9094
source: 'route',
9195
},
@@ -97,8 +101,8 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
97101
}
98102

99103
const dataFetcherSpan = requestTransaction.startChild({
100-
op: 'nextjs.data',
101-
description: `${options.functionName} (${options.parameterizedRoute})`,
104+
op: 'nextjs.data.server',
105+
description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
102106
});
103107

104108
const currentScope = getCurrentHub().getScope();
@@ -158,7 +162,7 @@ export async function callDataFetcherTraced<F extends (...args: any[]) => Promis
158162
// Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another
159163
// route's transaction
160164
const span = transaction.startChild({
161-
op: 'nextjs.data',
165+
op: 'nextjs.data.server',
162166
description: `${dataFetchingMethodName} (${parameterizedRoute})`,
163167
});
164168

packages/nextjs/src/index.server.ts

+3
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ export {
129129
withSentryGetServerSideProps,
130130
withSentryGetStaticProps,
131131
withSentryServerSideGetInitialProps,
132+
withSentryServerSideAppGetInitialProps,
133+
withSentryServerSideDocumentGetInitialProps,
134+
withSentryServerSideErrorGetInitialProps,
132135
} from './config/wrappers';
133136
export { withSentry } from './utils/withSentry';
134137

0 commit comments

Comments
 (0)