Skip to content

Commit 4205f31

Browse files
author
Luca Forstner
committed
Make wrapper name a little bit less annoying lol
1 parent fe67cdb commit 4205f31

File tree

3 files changed

+156
-5
lines changed

3 files changed

+156
-5
lines changed

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GetServerSideProps } from 'next';
22

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

55
/**
66
* Create a wrapped version of the user's exported `getServerSideProps` function
@@ -16,9 +16,18 @@ export function withSentryGetServerSideProps(
1616
return async function (
1717
...getServerSidePropsArguments: Parameters<GetServerSideProps>
1818
): ReturnType<GetServerSideProps> {
19-
return callDataFetcherTraced(origGetServerSideProps, getServerSidePropsArguments, {
20-
parameterizedRoute,
21-
dataFetchingMethodName: 'getServerSideProps',
22-
});
19+
const [context] = getServerSidePropsArguments;
20+
const { req, res } = context;
21+
22+
const errorWrappedGetServerSideProps = withErrorInstrumentation(origGetServerSideProps);
23+
24+
if (hasTracingEnabled()) {
25+
return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, {
26+
parameterizedRoute,
27+
functionName: 'getServerSideProps',
28+
});
29+
} else {
30+
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
31+
}
2332
};
2433
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { NextPage } from 'next';
3+
4+
import { callTracedServerSideDataFetcher, 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 callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req, res, {
29+
parameterizedRoute,
30+
functionName: 'getInitialProps',
31+
});
32+
} else {
33+
return errorWrappedGetInitialProps(...getInitialPropsArguments);
34+
}
35+
};
36+
}

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,111 @@
11
import { captureException } 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 callTracedServerSideDataFetcher<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.

0 commit comments

Comments
 (0)