-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathwrapperUtils.ts
More file actions
243 lines (219 loc) · 9.16 KB
/
Copy pathwrapperUtils.ts
File metadata and controls
243 lines (219 loc) · 9.16 KB
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import {
captureException,
getActiveTransaction,
getCurrentHub,
runWithAsyncContext,
startTransaction,
} from '@sentry/core';
import type { Span, Transaction } from '@sentry/types';
import { addExceptionMechanism, isString, tracingContextFromHeaders } from '@sentry/utils';
import type { IncomingMessage, ServerResponse } from 'http';
import { platformSupportsStreaming } from './platformSupportsStreaming';
import { autoEndTransactionOnResponseEnd, flushQueue } from './responseEnd';
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;
}
/**
* 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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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.apply(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, scope => {
scope.addEventProcessor(event => {
addExceptionMechanism(event, {
handled: false,
});
return event;
});
return scope;
});
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 origDataFetcher 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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Promise<any> | any>(
origDataFetcher: 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;
},
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
return async function (this: unknown, ...args: Parameters<F>): Promise<ReturnType<F>> {
return runWithAsyncContext(async () => {
const hub = getCurrentHub();
const scope = hub.getScope();
const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan();
let dataFetcherSpan;
const sentryTrace =
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined;
const baggage = req.headers?.baggage;
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
sentryTrace,
baggage,
);
scope.setPropagationContext(propagationContext);
if (platformSupportsStreaming()) {
let spanToContinue: Span;
if (previousSpan === undefined) {
const newTransaction = startTransaction(
{
op: 'http.server',
name: options.requestedRouteName,
...traceparentData,
status: 'ok',
metadata: {
request: req,
source: 'route',
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
},
{ request: req },
);
if (platformSupportsStreaming()) {
// On platforms that don't support streaming, doing things after res.end() is unreliable.
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);
spanToContinue = newTransaction;
} else {
spanToContinue = previousSpan;
}
dataFetcherSpan = spanToContinue.startChild({
op: 'function.nextjs',
description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
status: 'ok',
});
} else {
dataFetcherSpan = startTransaction({
op: 'function.nextjs',
name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
...traceparentData,
status: 'ok',
metadata: {
request: req,
source: 'route',
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
});
}
scope.setSpan(dataFetcherSpan);
scope.setSDKProcessingMetadata({ request: req });
try {
return await origDataFetcher.apply(this, args);
} 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');
previousSpan?.setStatus('internal_error');
throw e;
} finally {
dataFetcherSpan.finish();
scope.setSpan(previousSpan);
if (!platformSupportsStreaming()) {
await flushQueue();
}
}
});
};
}
/**
* 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: 'function.nextjs',
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, scope => {
scope.addEventProcessor(event => {
addExceptionMechanism(event, {
handled: false,
});
return event;
});
return scope;
});
throw err;
}
}