Skip to content

Commit 045ccc5

Browse files
committed
extract request-ending utils into separate module
1 parent 8000012 commit 045ccc5

File tree

4 files changed

+86
-84
lines changed

4 files changed

+86
-84
lines changed

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Transaction } from '@sentry/types';
1+
import type { Transaction, WrappedFunction } from '@sentry/types';
2+
import type { ServerResponse } from 'http';
23
import type { NextApiRequest, NextApiResponse } from 'next';
34

45
// The `NextApiHandler` and `WrappedNextApiHandler` types are the same as the official `NextApiHandler` type, except:
@@ -41,5 +42,9 @@ export type AugmentedNextApiResponse = NextApiResponse & {
4142
__sentryTransaction?: Transaction;
4243
};
4344

45+
export type AugmentedResponse = (NextApiResponse | ServerResponse) & {
46+
__sentryTransaction?: Transaction;
47+
};
48+
4449
export type ResponseEndMethod = AugmentedNextApiResponse['end'];
45-
export type WrappedResponseEndMethod = AugmentedNextApiResponse['end'];
50+
export type WrappedResponseEndMethod = AugmentedNextApiResponse['end'] & WrappedFunction;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { flush } from '@sentry/node';
2+
import { Transaction } from '@sentry/types';
3+
import { fill, logger } from '@sentry/utils';
4+
5+
import { AugmentedResponse, ResponseEndMethod, WrappedResponseEndMethod } from '../types';
6+
7+
/**
8+
* Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
9+
*
10+
* Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
11+
* things in the right order, in this case it's safe, because the native `.end()` actually *is* (effectively) async, and
12+
* its run actually *is* (literally) awaited, just manually so (which reflects the fact that the core of the
13+
* request/response code in Node by far predates the introduction of `async`/`await`). When `.end()` is done, it emits
14+
* the `prefinish` event, and only once that fires does request processing continue. See
15+
* https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
16+
*
17+
* Also note: `res.end()` isn't called until *after* all response data and headers have been sent, so blocking inside of
18+
* `end` doesn't delay data getting to the end user. See
19+
* https://nodejs.org/api/http.html#responseenddata-encoding-callback.
20+
*
21+
* @param transaction The transaction tracing request handling
22+
* @param res: The request's corresponding response
23+
*/
24+
export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: AugmentedResponse): void {
25+
res.__sentryTransaction = transaction;
26+
27+
const wrapEndMethod = (origEnd: ResponseEndMethod): WrappedResponseEndMethod => {
28+
return async function sentryWrappedEnd(this: AugmentedResponse, ...args: unknown[]) {
29+
await finishTransaction(this);
30+
await flushQueue();
31+
32+
return origEnd.call(this, ...args);
33+
};
34+
};
35+
36+
// Prevent double-wrapping
37+
if (!(res.end as WrappedResponseEndMethod).__sentry_original__) {
38+
fill(res, 'end', wrapEndMethod);
39+
}
40+
}
41+
42+
/** Finish the given response's transaction and set HTTP status data */
43+
export async function finishTransaction(res: AugmentedResponse): Promise<void> {
44+
const { __sentryTransaction: transaction } = res;
45+
46+
if (transaction) {
47+
transaction.setHttpStatus(res.statusCode);
48+
49+
// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
50+
// transaction closes, and make sure to wait until that's done before flushing events
51+
const transactionFinished: Promise<void> = new Promise(resolve => {
52+
setImmediate(() => {
53+
transaction.finish();
54+
resolve();
55+
});
56+
});
57+
await transactionFinished;
58+
}
59+
}
60+
61+
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */
62+
export async function flushQueue(): Promise<void> {
63+
try {
64+
__DEBUG_BUILD__ && logger.log('Flushing events...');
65+
await flush(2000);
66+
__DEBUG_BUILD__ && logger.log('Done flushing events');
67+
} catch (e) {
68+
__DEBUG_BUILD__ && logger.log('Error while flushing events:\n', e);
69+
}
70+
}

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

+6-72
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node';
1+
import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
22
import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
33
import {
44
addExceptionMechanism,
@@ -11,14 +11,8 @@ import {
1111
import * as domain from 'domain';
1212

1313
import { formatAsCode, nextLogger } from '../../utils/nextLogger';
14-
import type {
15-
AugmentedNextApiRequest,
16-
AugmentedNextApiResponse,
17-
NextApiHandler,
18-
ResponseEndMethod,
19-
WrappedNextApiHandler,
20-
WrappedResponseEndMethod,
21-
} from './types';
14+
import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler, WrappedNextApiHandler } from './types';
15+
import { autoEndTransactionOnResponseEnd, finishTransaction, flushQueue } from './utils/responseEnd';
2216

2317
/**
2418
* Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only
@@ -72,11 +66,6 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
7266
}
7367
req.__withSentry_applied__ = true;
7468

75-
// first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
76-
// fires (if we don't do this, the lambda will close too early and events will be either delayed or lost)
77-
// eslint-disable-next-line @typescript-eslint/unbound-method
78-
res.end = wrapEndMethod(res.end);
79-
8069
// use a domain in order to prevent scope bleed between requests
8170
const local = domain.create();
8271
local.add(req);
@@ -137,9 +126,7 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
137126
);
138127
currentScope.setSpan(transaction);
139128

140-
// save a link to the transaction on the response, so that even if there's an error (landing us outside of
141-
// the domain), we can still finish it (albeit possibly missing some scope data)
142-
res.__sentryTransaction = transaction;
129+
autoEndTransactionOnResponseEnd(transaction, res);
143130
}
144131
}
145132

@@ -189,7 +176,8 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
189176
// out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the
190177
// moment they detect an error, so it's important to get this done before rethrowing the error. Apps not
191178
// deployed serverlessly will run into this cleanup function again in `res.end(), but it'll just no-op.)
192-
await finishSentryProcessing(res);
179+
await finishTransaction(res);
180+
await flushQueue();
193181

194182
// We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
195183
// would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
@@ -203,57 +191,3 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
203191
return boundHandler();
204192
};
205193
}
206-
207-
/**
208-
* Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
209-
*
210-
* Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
211-
* things in the right order, in this case it's safe, because the native `.end()` actually *is* async, and its run
212-
* actually *is* awaited, just manually so (which reflects the fact that the core of the request/response code in Node
213-
* by far predates the introduction of `async`/`await`). When `.end()` is done, it emits the `prefinish` event, and
214-
* only once that fires does request processing continue. See
215-
* https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
216-
*
217-
* @param origEnd The original `res.end()` method
218-
* @returns The wrapped version
219-
*/
220-
function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
221-
return async function newEnd(this: AugmentedNextApiResponse, ...args: unknown[]) {
222-
await finishSentryProcessing(this);
223-
224-
return origEnd.call(this, ...args);
225-
};
226-
}
227-
228-
/**
229-
* Close the open transaction (if any) and flush events to Sentry.
230-
*
231-
* @param res The outgoing response for this request, on which the transaction is stored
232-
*/
233-
async function finishSentryProcessing(res: AugmentedNextApiResponse): Promise<void> {
234-
const { __sentryTransaction: transaction } = res;
235-
236-
if (transaction) {
237-
transaction.setHttpStatus(res.statusCode);
238-
239-
// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
240-
// transaction closes, and make sure to wait until that's done before flushing events
241-
const transactionFinished: Promise<void> = new Promise(resolve => {
242-
setImmediate(() => {
243-
transaction.finish();
244-
resolve();
245-
});
246-
});
247-
await transactionFinished;
248-
}
249-
250-
// Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
251-
// ends. If there was an error, rethrow it so that the normal exception-handling mechanisms can apply.
252-
try {
253-
__DEBUG_BUILD__ && logger.log('Flushing events...');
254-
await flush(2000);
255-
__DEBUG_BUILD__ && logger.log('Done flushing events');
256-
} catch (e) {
257-
__DEBUG_BUILD__ && logger.log('Error while flushing events:\n', e);
258-
}
259-
}

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

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
22
import { getActiveTransaction } from '@sentry/tracing';
33
import { Transaction } from '@sentry/types';
4-
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData, fill } from '@sentry/utils';
4+
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
55
import * as domain from 'domain';
66
import { IncomingMessage, ServerResponse } from 'http';
77

8+
import { autoEndTransactionOnResponseEnd } from './utils/responseEnd';
9+
810
declare module 'http' {
911
interface IncomingMessage {
1012
_sentryTransaction?: Transaction;
@@ -26,15 +28,6 @@ function setTransactionOnRequest(transaction: Transaction, req: IncomingMessage)
2628
req._sentryTransaction = transaction;
2729
}
2830

29-
function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void {
30-
fill(res, 'end', (originalEnd: ServerResponse['end']) => {
31-
return function (this: unknown, ...endArguments: Parameters<ServerResponse['end']>) {
32-
transaction.finish();
33-
return originalEnd.call(this, ...endArguments);
34-
};
35-
});
36-
}
37-
3831
/**
3932
* Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown.
4033
*

0 commit comments

Comments
 (0)