Skip to content

Commit 40bb2de

Browse files
authored
feat(nextjs): Create transactions in getInitialProps and getServerSideProps (#5593)
1 parent f7241c4 commit 40bb2de

9 files changed

+205
-39
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const origGetStaticProps = userPageModule.getStaticProps;
2727
const origGetServerSideProps = userPageModule.getServerSideProps;
2828

2929
if (typeof origGetInitialProps === 'function') {
30-
pageComponent.getInitialProps = Sentry.withSentryGetInitialProps(
30+
pageComponent.getInitialProps = Sentry.withSentryServerSideGetInitialProps(
3131
origGetInitialProps,
3232
'__ROUTE__',
3333
) as NextPageComponent['getInitialProps'];
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { withSentryGetStaticProps } from './withSentryGetStaticProps';
2-
export { withSentryGetInitialProps } from './withSentryGetInitialProps';
32
export { withSentryGetServerSideProps } from './withSentryGetServerSideProps';
3+
export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps';

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

-26
This file was deleted.

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

+20-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
12
import { GetServerSideProps } from 'next';
23

3-
import { callDataFetcherTraced } from './wrapperUtils';
4+
import { isBuild } from '../../utils/isBuild';
5+
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
46

57
/**
68
* Create a wrapped version of the user's exported `getServerSideProps` function
@@ -16,9 +18,22 @@ export function withSentryGetServerSideProps(
1618
return async function (
1719
...getServerSidePropsArguments: Parameters<GetServerSideProps>
1820
): ReturnType<GetServerSideProps> {
19-
return callDataFetcherTraced(origGetServerSideProps, getServerSidePropsArguments, {
20-
parameterizedRoute,
21-
dataFetchingMethodName: 'getServerSideProps',
22-
});
21+
if (isBuild()) {
22+
return origGetServerSideProps(...getServerSidePropsArguments);
23+
}
24+
25+
const [context] = getServerSidePropsArguments;
26+
const { req, res } = context;
27+
28+
const errorWrappedGetServerSideProps = withErrorInstrumentation(origGetServerSideProps);
29+
30+
if (hasTracingEnabled()) {
31+
return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, {
32+
parameterizedRoute,
33+
functionName: 'getServerSideProps',
34+
});
35+
} else {
36+
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
37+
}
2338
};
2439
}

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GetStaticProps } from 'next';
22

3-
import { callDataFetcherTraced } from './wrapperUtils';
3+
import { isBuild } from '../../utils/isBuild';
4+
import { callDataFetcherTraced, withErrorInstrumentation } from './wrapperUtils';
45

56
type Props = { [key: string]: unknown };
67

@@ -18,7 +19,13 @@ export function withSentryGetStaticProps(
1819
return async function (
1920
...getStaticPropsArguments: Parameters<GetStaticProps<Props>>
2021
): ReturnType<GetStaticProps<Props>> {
21-
return callDataFetcherTraced(origGetStaticProps, getStaticPropsArguments, {
22+
if (isBuild()) {
23+
return origGetStaticProps(...getStaticPropsArguments);
24+
}
25+
26+
const errorWrappedGetStaticProps = withErrorInstrumentation(origGetStaticProps);
27+
28+
return callDataFetcherTraced(errorWrappedGetStaticProps, getStaticPropsArguments, {
2229
parameterizedRoute,
2330
dataFetchingMethodName: 'getStaticProps',
2431
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { NextPage } from 'next';
3+
4+
import { isBuild } from '../../utils/isBuild';
5+
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
7+
type GetInitialProps = Required<NextPage>['getInitialProps'];
8+
9+
/**
10+
* Create a wrapped version of the user's exported `getInitialProps` function
11+
*
12+
* @param origGetInitialProps The user's `getInitialProps` function
13+
* @param parameterizedRoute The page's parameterized route
14+
* @returns A wrapped version of the function
15+
*/
16+
export function withSentryServerSideGetInitialProps(
17+
origGetInitialProps: GetInitialProps,
18+
parameterizedRoute: string,
19+
): GetInitialProps {
20+
return async function (
21+
...getInitialPropsArguments: Parameters<GetInitialProps>
22+
): Promise<ReturnType<GetInitialProps>> {
23+
if (isBuild()) {
24+
return origGetInitialProps(...getInitialPropsArguments);
25+
}
26+
27+
const [context] = getInitialPropsArguments;
28+
const { req, res } = context;
29+
30+
const errorWrappedGetInitialProps = withErrorInstrumentation(origGetInitialProps);
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+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
36+
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
37+
parameterizedRoute,
38+
functionName: 'getInitialProps',
39+
});
40+
} else {
41+
return errorWrappedGetInitialProps(...getInitialPropsArguments);
42+
}
43+
};
44+
}

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

+125-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,129 @@
1-
import { captureException } from '@sentry/core';
1+
import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
2+
import { addRequestDataToEvent } from '@sentry/node';
23
import { getActiveTransaction } from '@sentry/tracing';
4+
import { Transaction } from '@sentry/types';
5+
import { fill } from '@sentry/utils';
6+
import * as domain from 'domain';
7+
import { IncomingMessage, ServerResponse } from 'http';
8+
9+
declare module 'http' {
10+
interface IncomingMessage {
11+
_sentryTransaction?: Transaction;
12+
}
13+
}
14+
15+
function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
16+
return req._sentryTransaction;
17+
}
18+
19+
function setTransactionOnRequest(transaction: Transaction, req: IncomingMessage): void {
20+
req._sentryTransaction = transaction;
21+
}
22+
23+
function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void {
24+
fill(res, 'end', (originalEnd: ServerResponse['end']) => {
25+
return function (this: unknown, ...endArguments: Parameters<ServerResponse['end']>) {
26+
transaction.finish();
27+
return originalEnd.call(this, ...endArguments);
28+
};
29+
});
30+
}
31+
32+
/**
33+
* Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown.
34+
*/
35+
export function withErrorInstrumentation<F extends (...args: any[]) => any>(
36+
origFunction: F,
37+
): (...params: Parameters<F>) => ReturnType<F> {
38+
return function (this: unknown, ...origFunctionArguments: Parameters<F>): ReturnType<F> {
39+
try {
40+
const potentialPromiseResult = origFunction.call(this, ...origFunctionArguments);
41+
// First of all, we need to capture promise rejections so we have the following check, as well as the try-catch block.
42+
// Additionally, we do the following instead of `await`-ing so we do not change the method signature of the passed function from `() => unknown` to `() => Promise<unknown>.
43+
Promise.resolve(potentialPromiseResult).catch(err => {
44+
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
45+
captureException(err);
46+
});
47+
return potentialPromiseResult;
48+
} catch (e) {
49+
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
50+
captureException(e);
51+
throw e;
52+
}
53+
};
54+
}
55+
56+
/**
57+
* Calls a server-side data fetching function (that takes a `req` and `res` object in its context) with tracing
58+
* instrumentation. A transaction will be created for the incoming request (if it doesn't already exist) in addition to
59+
* a span for the wrapped data fetching function.
60+
*
61+
* All of the above happens in an isolated domain, meaning all thrown errors will be associated with the correct span.
62+
*
63+
* @param origFunction The data fetching method to call.
64+
* @param origFunctionArguments The arguments to call the data fetching method with.
65+
* @param req The data fetching function's request object.
66+
* @param res The data fetching function's response object.
67+
* @param options Options providing details for the created transaction and span.
68+
* @returns what the data fetching method call returned.
69+
*/
70+
export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Promise<any> | any>(
71+
origFunction: F,
72+
origFunctionArguments: Parameters<F>,
73+
req: IncomingMessage,
74+
res: ServerResponse,
75+
options: {
76+
parameterizedRoute: string;
77+
functionName: string;
78+
},
79+
): Promise<ReturnType<F>> {
80+
return domain.create().bind(async () => {
81+
let requestTransaction: Transaction | undefined = getTransactionFromRequest(req);
82+
83+
if (requestTransaction === undefined) {
84+
// TODO: Extract trace data from `req` object (trace and baggage headers) and attach it to transaction
85+
86+
const newTransaction = startTransaction({
87+
op: 'nextjs.data',
88+
name: options.parameterizedRoute,
89+
metadata: {
90+
source: 'route',
91+
},
92+
});
93+
94+
requestTransaction = newTransaction;
95+
autoEndTransactionOnResponseEnd(newTransaction, res);
96+
setTransactionOnRequest(newTransaction, req);
97+
}
98+
99+
const dataFetcherSpan = requestTransaction.startChild({
100+
op: 'nextjs.data',
101+
description: `${options.functionName} (${options.parameterizedRoute})`,
102+
});
103+
104+
const currentScope = getCurrentHub().getScope();
105+
if (currentScope) {
106+
currentScope.setSpan(dataFetcherSpan);
107+
currentScope.addEventProcessor(event =>
108+
addRequestDataToEvent(event, req, {
109+
include: {
110+
// When the `transaction` option is set to true, it tries to extract a transaction name from the request
111+
// object. We don't want this since we already have a high-quality transaction name with a parameterized
112+
// route. Setting `transaction` to `true` will clobber that transaction name.
113+
transaction: false,
114+
},
115+
}),
116+
);
117+
}
118+
119+
try {
120+
// TODO: Inject trace data into returned props
121+
return await origFunction(...origFunctionArguments);
122+
} finally {
123+
dataFetcherSpan.finish();
124+
}
125+
})();
126+
}
3127

4128
/**
5129
* Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope.

packages/nextjs/src/index.client.ts

-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ export * from '@sentry/react';
1111
export { nextRouterInstrumentation } from './performance/client';
1212
export { captureUnderscoreErrorException } from './utils/_error';
1313

14-
export { withSentryGetInitialProps } from './config/wrappers';
15-
1614
export { Integrations };
1715

1816
// Previously we expected users to import `BrowserTracing` like this:

packages/nextjs/src/index.server.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ function addServerIntegrations(options: NextjsOptions): void {
125125
export type { SentryWebpackPluginOptions } from './config/types';
126126
export { withSentryConfig } from './config';
127127
export { isBuild } from './utils/isBuild';
128-
export { withSentryGetServerSideProps, withSentryGetStaticProps, withSentryGetInitialProps } from './config/wrappers';
128+
export {
129+
withSentryGetServerSideProps,
130+
withSentryGetStaticProps,
131+
withSentryServerSideGetInitialProps,
132+
} from './config/wrappers';
129133
export { withSentry } from './utils/withSentry';
130134

131135
// Wrap various server methods to enable error monitoring and tracing. (Note: This only happens for non-Vercel

0 commit comments

Comments
 (0)