Skip to content

Commit

Permalink
Ensure withSentry reports API (serverless) errors from Vercel
Browse files Browse the repository at this point in the history
Potential fix for getsentry#3917 to ensure Sentry error reporting clean-up is
done -- transaction finished and error flushed -- when run from a
Vercel deployment, by explicitly calling the clean-up code in two
places: directly from the `withSentry` handler wrapper function, as
well as the existing monkey-patched `res.end()`.

In Vercel the `res.end()` function is not (or is not always) called,
which means the prior approach that relied on monkey-patching of
that function for clean-up did not work in Vercel.

Note 1: this is a naive fix: I'm not sure why res.end() isn't
called as expected or if there might be a better target for monkey-
patching.

Note 2: a new `__flushed` variable is used to avoid running the
clean-up code twice, should both the explicit and the monkey-patched
path be run. See the TODO asking whether this flag should be set at
the beginning, instead of the end, of the clean-up function
`finishTransactionAndFlush()`
  • Loading branch information
jmurty committed Oct 11, 2021
1 parent 5d6f996 commit 2956c47
Showing 1 changed file with 41 additions and 24 deletions.
65 changes: 41 additions & 24 deletions packages/nextjs/src/utils/withSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { parseRequest } = Handlers;
// purely for clarity
type WrappedNextApiHandler = NextApiHandler;

type AugmentedResponse = NextApiResponse & { __sentryTransaction?: Transaction };
type AugmentedResponse = NextApiResponse & { __sentryTransaction?: Transaction, __flushed?: boolean };

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
Expand Down Expand Up @@ -84,6 +84,10 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
return event;
});
captureException(e);

// Explicitly call function to finish transaction and flush in case the monkeypatched `res.end()` is
// never called; it isn't always called for Vercel deployment
await finishTransactionAndFlush(res);
}
throw e;
}
Expand All @@ -93,35 +97,48 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
};
};

async function finishTransactionAndFlush(res: AugmentedResponse) {
const transaction = res.__sentryTransaction;

if (transaction) {
transaction.setHttpStatus(res.statusCode);

// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
// transaction closes, and make sure to wait until that's done before flushing events
const transactionFinished: Promise<void> = new Promise((resolve) => {
setImmediate(() => {
transaction.finish();
resolve();
});
});
await transactionFinished;
}

// flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
// ends
try {
logger.log('Flushing events...');
await flush(2000);
logger.log('Done flushing events');
} catch (e) {
logger.log(`Error while flushing events:\n${e}`);
} finally {
// Flag response as already finished and flushed, to avoid double-flushing
// TODO Set at beginning of this function to better avoid double runs?
res.__flushed = true;
}
}

type ResponseEndMethod = AugmentedResponse['end'];
type WrappedResponseEndMethod = AugmentedResponse['end'];

function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
return async function newEnd(this: AugmentedResponse, ...args: unknown[]) {
const transaction = this.__sentryTransaction;

if (transaction) {
transaction.setHttpStatus(this.statusCode);

// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
// transaction closes, and make sure to wait until that's done before flushing events
const transactionFinished: Promise<void> = new Promise(resolve => {
setImmediate(() => {
transaction.finish();
resolve();
});
});
await transactionFinished;
}

// flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
// ends
try {
logger.log('Flushing events...');
await flush(2000);
logger.log('Done flushing events');
} catch (e) {
logger.log(`Error while flushing events:\n${e}`);
if (this.__flushed) {
logger.log('Skip finish transaction and flush, already done');
} else {
await finishTransactionAndFlush(this);
}

return origEnd.call(this, ...args);
Expand Down

0 comments on commit 2956c47

Please sign in to comment.