Skip to content

Commit 11df2e6

Browse files
committed
move newEnd into a factory function
1 parent 016aa5e commit 11df2e6

File tree

1 file changed

+60
-28
lines changed

1 file changed

+60
-28
lines changed

packages/nextjs/src/utils/handlers.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { captureException, flush, getCurrentHub, Handlers, startTransaction, withScope } from '@sentry/node';
2+
// import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
23
import { extractTraceparentData, getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
34
import { addExceptionMechanism, isString, logger, stripUrlQueryAndFragment } from '@sentry/utils';
5+
import { Scope } from '@sentry/hub';
46
import * as domain from 'domain';
57
import { NextApiHandler, NextApiResponse } from 'next';
68

@@ -11,43 +13,23 @@ const { parseRequest } = Handlers;
1113
// purely for clarity
1214
type WrappedNextApiHandler = NextApiHandler;
1315

16+
type ScopedResponse = NextApiResponse & { __sentryScope?: Scope };
17+
1418
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1519
export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
1620
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1721
return async (req, res) => {
18-
const origEnd = res.end;
19-
20-
async function newEnd(this: NextApiResponse, ...args: any[]) {
21-
const transaction = getActiveTransaction();
22-
console.log('Active transaction:', transaction?.name);
23-
24-
if (transaction) {
25-
transaction.setHttpStatus(res.statusCode);
26-
27-
transaction.finish();
28-
}
29-
try {
30-
logger.log('Flushing events...');
31-
await flush(2000);
32-
} catch (e) {
33-
logger.log(`Error while flushing events:\n${e}`);
34-
} finally {
35-
logger.log('Done flushing events');
36-
// res.end();
37-
}
38-
39-
return origEnd.call(this, ...args);
40-
}
22+
// first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
23+
// fires (if we don't do this, the lambda will close too early and events will be either delayed or lost)
24+
res.end = wrapEndMethod(res.end);
4125

42-
res.end = newEnd;
43-
44-
// wrap everything in a domain in order to prevent scope bleed between requests
26+
// use a domain in order to prevent scope bleed between requests
4527
const local = domain.create();
4628
local.add(req);
4729
local.add(res);
4830

4931
return local.bind(async () => {
50-
try {
32+
5133
const currentScope = getCurrentHub().getScope();
5234

5335
if (currentScope) {
@@ -84,11 +66,16 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
8466
{ request: req },
8567
);
8668
currentScope.setSpan(transaction);
69+
70+
// save a link to the transaction on the response, so that even if there's an error (landing us outside of
71+
// the domain), we can still finish the transaction and attach the correct data to it
72+
(res as ScopedResponse).__sentryScope = currentScope;
8773
}
8874
}
89-
75+
try {
9076
return await handler(req, res); // Call original handler
9177
} catch (e) {
78+
// TODO how do we know
9279
withScope(scope => {
9380
scope.addEventProcessor(event => {
9481
addExceptionMechanism(event, {
@@ -119,3 +106,48 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
119106
})();
120107
};
121108
};
109+
110+
type ResponseEndMethod = ScopedResponse['end'];
111+
type WrappedResponseEndMethod = ScopedResponse['end'];
112+
113+
function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
114+
return async function newEnd(this: ScopedResponse, ...args: any[]) {
115+
// if the handler errored, it will have popped us out of the domain, so push the domain's scope onto the stack
116+
// just in case (if we *are* still in the domain, this will replace the current scope with a clone of itself,
117+
// which is effectively a no-op as long as we remember to pop it off when we're done)
118+
const currentHub = getCurrentHub();
119+
currentHub.pushScope(this.__sentryScope);
120+
121+
const transaction = getActiveTransaction();
122+
console.log('Active transaction:', transaction?.name);
123+
124+
if (transaction) {
125+
transaction.setHttpStatus(this.statusCode);
126+
127+
// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
128+
// transaction closes, and make sure to wait until that's done before flushing events
129+
const transactionFinished: Promise<void> = new Promise(resolve => {
130+
setImmediate(() => {
131+
transaction.finish();
132+
resolve();
133+
});
134+
});
135+
await transactionFinished;
136+
}
137+
138+
// flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
139+
// ends
140+
try {
141+
logger.log('Flushing events...');
142+
await flush(2000);
143+
} catch (e) {
144+
logger.log(`Error while flushing events:\n${e}`);
145+
} finally {
146+
logger.log('Done flushing events');
147+
}
148+
149+
// now that our work is done, we can pop off the scope and allow the response to end
150+
currentHub.popScope();
151+
return origEnd.call(this, ...args);
152+
};
153+
}

0 commit comments

Comments
 (0)