Skip to content

Commit 82fb3fa

Browse files
author
Luca Forstner
committed
feat(nextjs): Create transactions in getInitialProps and getServerSideProps
1 parent 8c58e0d commit 82fb3fa

File tree

9 files changed

+187
-47
lines changed

9 files changed

+187
-47
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ 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 { withSentryServerSideGetInitialProps } from "@sentry/nextjs";`;
155155

156156
if (parameterizedRouteName === '/_app') {
157157
// getInitialProps signature is a bit different in _app.js so we need a different wrapper
@@ -166,7 +166,7 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>,
166166
// We enter this branch for any "normal" Next.js page
167167
outputFileContent += `
168168
if (typeof _sentry_default.getInitialProps === 'function') {
169-
_sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps, '${parameterizedRouteName}');
169+
_sentry_default.getInitialProps = withSentryServerSideGetInitialProps(_sentry_default.getInitialProps, '${parameterizedRouteName}');
170170
}`;
171171
}
172172

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { withSentryGetStaticProps } from './withSentryGetStaticProps';
2-
export { withSentryGetInitialProps } from './withSentryGetInitialProps';
2+
export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps';
33
export { withSentryGetServerSideProps } from './withSentryGetServerSideProps';

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

Lines changed: 0 additions & 26 deletions
This file was deleted.

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
12
import { GetServerSideProps } from 'next';
23

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

56
/**
67
* Create a wrapped version of the user's exported `getServerSideProps` function
@@ -16,9 +17,24 @@ export function withSentryGetServerSideProps(
1617
return async function (
1718
...getServerSidePropsArguments: Parameters<GetServerSideProps>
1819
): ReturnType<GetServerSideProps> {
19-
return callDataFetcherTraced(origGetServerSideProps, getServerSidePropsArguments, {
20-
parameterizedRoute,
21-
dataFetchingMethodName: 'getServerSideProps',
22-
});
20+
const [context] = getServerSidePropsArguments;
21+
const { req, res } = context;
22+
23+
const errorWrappedGetServerSideProps = withErrorInstrumentation(origGetServerSideProps);
24+
25+
if (hasTracingEnabled()) {
26+
return callServerSideDataFetcherWithTracingInstrumentation(
27+
errorWrappedGetServerSideProps,
28+
getServerSidePropsArguments,
29+
req,
30+
res,
31+
{
32+
parameterizedRoute,
33+
functionName: 'getServerSideProps',
34+
},
35+
);
36+
} else {
37+
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
38+
}
2339
};
2440
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { GetStaticProps } from 'next';
22

3-
import { callDataFetcherTraced } from './wrapperUtils';
3+
import { callDataFetcherTraced, withErrorInstrumentation } from './wrapperUtils';
44

55
type Props = { [key: string]: unknown };
66

7+
// TODO: This wrapper probably needs some kind of custom instrumentation because it is so different from the other data fetching methods.
78
/**
89
* Create a wrapped version of the user's exported `getStaticProps` function
910
*
@@ -18,7 +19,9 @@ export function withSentryGetStaticProps(
1819
return async function (
1920
...getStaticPropsArguments: Parameters<GetStaticProps<Props>>
2021
): ReturnType<GetStaticProps<Props>> {
21-
return callDataFetcherTraced(origGetStaticProps, getStaticPropsArguments, {
22+
const errorWrappedGetStaticProps = withErrorInstrumentation(origGetStaticProps);
23+
24+
return callDataFetcherTraced(errorWrappedGetStaticProps, getStaticPropsArguments, {
2225
parameterizedRoute,
2326
dataFetchingMethodName: 'getStaticProps',
2427
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { NextPage } from 'next';
3+
4+
import { callServerSideDataFetcherWithTracingInstrumentation, withErrorInstrumentation } from './wrapperUtils';
5+
6+
type GetInitialProps = Required<NextPage>['getInitialProps'];
7+
8+
/**
9+
* Create a wrapped version of the user's exported `getInitialProps` function
10+
*
11+
* @param origGetInitialProps The user's `getInitialProps` function
12+
* @param parameterizedRoute The page's parameterized route
13+
* @returns A wrapped version of the function
14+
*/
15+
export function withSentryServerSideGetInitialProps(
16+
origGetInitialProps: GetInitialProps,
17+
parameterizedRoute: string,
18+
): GetInitialProps {
19+
return async function (
20+
...getInitialPropsArguments: Parameters<GetInitialProps>
21+
): Promise<ReturnType<GetInitialProps>> {
22+
const [context] = getInitialPropsArguments;
23+
const { req, res } = context;
24+
25+
const errorWrappedGetInitialProps = withErrorInstrumentation(origGetInitialProps);
26+
27+
if (req && res && hasTracingEnabled()) {
28+
return callServerSideDataFetcherWithTracingInstrumentation(
29+
errorWrappedGetInitialProps,
30+
getInitialPropsArguments,
31+
req,
32+
res,
33+
{
34+
parameterizedRoute,
35+
functionName: 'getInitialProps',
36+
},
37+
);
38+
} else {
39+
return errorWrappedGetInitialProps(...getInitialPropsArguments);
40+
}
41+
};
42+
}

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

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

4110
/**
5111
* Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope.
6112
*
7113
* We only do the following until we move transaction creation into this function: When called, the wrapped function
8114
* will also update the name of the active transaction with a parameterized route provided via the `options` argument.
9115
*/
116+
// TODO: Delete this helper. It is only used by getStaticProps because it is so different from the other data fetching methods.
10117
export async function callDataFetcherTraced<F extends (...args: any[]) => Promise<any> | any>(
11118
origFunction: F,
12119
origFunctionArgs: Parameters<F>,
@@ -40,13 +147,7 @@ export async function callDataFetcherTraced<F extends (...args: any[]) => Promis
40147

41148
try {
42149
return await origFunction(...origFunctionArgs);
43-
} catch (err) {
44-
if (span) {
45-
span.finish();
46-
}
47-
48-
// TODO Copy more robust error handling over from `withSentry`
49-
captureException(err);
50-
throw err;
150+
} finally {
151+
span.finish();
51152
}
52153
}

packages/nextjs/src/index.client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export * from '@sentry/react';
1111
export { nextRouterInstrumentation } from './performance/client';
1212
export { captureUnderscoreErrorException } from './utils/_error';
1313

14-
export { withSentryGetInitialProps } from './config/wrappers';
14+
export { withSentryServerSideGetInitialProps } from './config/wrappers';
1515

1616
export { Integrations };
1717

packages/nextjs/src/index.server.ts

Lines changed: 5 additions & 1 deletion
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)