-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathwrapperUtils.ts
210 lines (187 loc) · 8.43 KB
/
wrapperUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
import { addRequestDataToEvent } from '@sentry/node';
import { getActiveTransaction } from '@sentry/tracing';
import { Transaction } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData, fill } from '@sentry/utils';
import * as domain from 'domain';
import { IncomingMessage, ServerResponse } from 'http';
declare module 'http' {
interface IncomingMessage {
_sentryTransaction?: Transaction;
}
}
/**
* Grabs a transaction off a Next.js datafetcher request object, if it was previously put there via
* `setTransactionOnRequest`.
*
* @param req The Next.js datafetcher request object
* @returns the Transaction on the request object if there is one, or `undefined` if the request object didn't have one.
*/
export function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
return req._sentryTransaction;
}
function setTransactionOnRequest(transaction: Transaction, req: IncomingMessage): void {
req._sentryTransaction = transaction;
}
function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void {
fill(res, 'end', (originalEnd: ServerResponse['end']) => {
return function (this: unknown, ...endArguments: Parameters<ServerResponse['end']>) {
transaction.finish();
return originalEnd.call(this, ...endArguments);
};
});
}
/**
* Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown.
*
* Note: This function turns the wrapped function into an asynchronous one.
*/
export function withErrorInstrumentation<F extends (...args: any[]) => any>(
origFunction: F,
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
return async function (this: unknown, ...origFunctionArguments: Parameters<F>): Promise<ReturnType<F>> {
try {
return await origFunction.call(this, ...origFunctionArguments);
} catch (e) {
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
captureException(e);
throw e;
}
};
}
/**
* Calls a server-side data fetching function (that takes a `req` and `res` object in its context) with tracing
* instrumentation. A transaction will be created for the incoming request (if it doesn't already exist) in addition to
* a span for the wrapped data fetching function.
*
* All of the above happens in an isolated domain, meaning all thrown errors will be associated with the correct span.
*
* @param origFunction The data fetching method to call.
* @param origFunctionArguments The arguments to call the data fetching method with.
* @param req The data fetching function's request object.
* @param res The data fetching function's response object.
* @param options Options providing details for the created transaction and span.
* @returns what the data fetching method call returned.
*/
export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Promise<any> | any>(
origFunction: F,
origFunctionArguments: Parameters<F>,
req: IncomingMessage,
res: ServerResponse,
options: {
/** Parameterized route of the request - will be used for naming the transaction. */
requestedRouteName: string;
/** Name of the route the data fetcher was defined in - will be used for describing the data fetcher's span. */
dataFetcherRouteName: string;
/** Name of the data fetching method - will be used for describing the data fetcher's span. */
dataFetchingMethodName: string;
},
): Promise<ReturnType<F>> {
return domain.create().bind(async () => {
let requestTransaction: Transaction | undefined = getTransactionFromRequest(req);
if (requestTransaction === undefined) {
const sentryTraceHeader = req.headers['sentry-trace'];
const rawBaggageString = req.headers && req.headers.baggage;
const traceparentData =
typeof sentryTraceHeader === 'string' ? extractTraceparentData(sentryTraceHeader) : undefined;
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(rawBaggageString);
const newTransaction = startTransaction({
op: 'nextjs.data.server',
name: options.requestedRouteName,
...traceparentData,
status: 'ok',
metadata: {
source: 'route',
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
});
requestTransaction = newTransaction;
autoEndTransactionOnResponseEnd(newTransaction, res);
// Link the transaction and the request together, so that when we would normally only have access to one, it's
// still possible to grab the other.
setTransactionOnRequest(newTransaction, req);
newTransaction.setMetadata({ request: req });
}
const dataFetcherSpan = requestTransaction.startChild({
op: 'nextjs.data.server',
description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
status: 'ok',
});
const currentScope = getCurrentHub().getScope();
if (currentScope) {
currentScope.setSpan(dataFetcherSpan);
currentScope.addEventProcessor(event =>
event.type !== 'transaction'
? addRequestDataToEvent(event, req, {
include: {
// When the `transaction` option is set to true, it tries to extract a transaction name from the request
// object. We don't want this since we already have a high-quality transaction name with a parameterized
// route. Setting `transaction` to `true` will clobber that transaction name.
transaction: false,
},
})
: event,
);
}
try {
return await origFunction(...origFunctionArguments);
} catch (e) {
// Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation`
// that set the transaction status, we need to manually set the status of the span & transaction
dataFetcherSpan.setStatus('internal_error');
const transaction = dataFetcherSpan.transaction;
if (transaction) {
transaction.setStatus('internal_error');
}
throw e;
} finally {
dataFetcherSpan.finish();
}
})();
}
/**
* Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope.
*
* We only do the following until we move transaction creation into this function: When called, the wrapped function
* will also update the name of the active transaction with a parameterized route provided via the `options` argument.
*/
export async function callDataFetcherTraced<F extends (...args: any[]) => Promise<any> | any>(
origFunction: F,
origFunctionArgs: Parameters<F>,
options: {
parameterizedRoute: string;
dataFetchingMethodName: string;
},
): Promise<ReturnType<F>> {
const { parameterizedRoute, dataFetchingMethodName } = options;
const transaction = getActiveTransaction();
if (!transaction) {
return origFunction(...origFunctionArgs);
}
// TODO: Make sure that the given route matches the name of the active transaction (to prevent background data
// fetching from switching the name to a completely other route) -- We'll probably switch to creating a transaction
// right here so making that check will probabably not even be necessary.
// Logic will be: If there is no active transaction, start one with correct name and source. If there is an active
// transaction, create a child span with correct name and source.
transaction.name = parameterizedRoute;
transaction.metadata.source = 'route';
// Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another
// route's transaction
const span = transaction.startChild({
op: 'nextjs.data.server',
description: `${dataFetchingMethodName} (${parameterizedRoute})`,
status: 'ok',
});
try {
return await origFunction(...origFunctionArgs);
} catch (err) {
// Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation`
// that set the transaction status, we need to manually set the status of the span & transaction
transaction.setStatus('internal_error');
span.setStatus('internal_error');
span.finish();
// TODO Copy more robust error handling over from `withSentry`
captureException(err);
throw err;
}
}