Skip to content

Ensure withSentry reports API (serverless) errors from Vercel #4044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 41 additions & 25 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,47 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
};
};

async function finishTransactionAndFlush(res: AugmentedResponse): Promise<void> {
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