Skip to content

feat(nextjs): Add exception handler for _error.js #5259

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

Merged
merged 1 commit into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions packages/nextjs/src/index.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { addIntegration, UserIntegrations } from './utils/userIntegrations';

export * from '@sentry/react';
export { nextRouterInstrumentation } from './performance/client';
export { captureUnderscoreErrorException } from './utils/_error';

export { Integrations };

Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NextjsOptions } from './utils/nextjsOptions';
import { addIntegration } from './utils/userIntegrations';

export * from '@sentry/node';
export { captureUnderscoreErrorException } from './utils/_error';

// Here we want to make sure to only include what doesn't have browser specifics
// because or SSR of next.js we can only use this.
Expand Down Expand Up @@ -74,7 +75,7 @@ export function init(options: NextjsOptions): void {
return event.type === 'transaction' && event.transaction === '/404' ? null : event;
};

filterTransactions.id = 'NextServer404Filter';
filterTransactions.id = 'NextServer404TransactionFilter';

configureScope(scope => {
scope.setTag('runtime', 'node');
Expand Down
78 changes: 78 additions & 0 deletions packages/nextjs/src/utils/_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { captureException, withScope } from '@sentry/core';
import { getCurrentHub } from '@sentry/hub';
import { addExceptionMechanism, addRequestDataToEvent, objectify } from '@sentry/utils';
import { NextPageContext } from 'next';

type ContextOrProps = {
[key: string]: unknown;
req?: NextPageContext['req'];
res?: NextPageContext['res'];
err?: NextPageContext['err'] | string;
statusCode?: number;
};

/** Platform-agnostic version of `flush` */
function flush(timeout?: number): PromiseLike<boolean> {
const client = getCurrentHub().getClient();
return client ? client.flush(timeout) : Promise.resolve(false);
}

/**
* Capture the exception passed by nextjs to the `_error` page, adding context data as appropriate.
*
* @param contextOrProps The data passed to either `getInitialProps` or `render` by nextjs
*/
export async function captureUnderscoreErrorException(contextOrProps: ContextOrProps): Promise<void> {
const { req, res, err } = contextOrProps;

// 404s (and other 400-y friends) can trigger `_error`, but we don't want to send them to Sentry
const statusCode = (res && res.statusCode) || contextOrProps.statusCode;
if (statusCode && statusCode < 500) {
return Promise.resolve();
}

// Nextjs only passes the pathname in the context data given to `getInitialProps`, not the main render function, but
// unlike `req` and `res`, for which that also applies, it passes it on both server and client.
//
// TODO: This check is only necessary because of the workaround for https://github.com/vercel/next.js/issues/8592
// explained below. Once that's fixed, we'll have to keep the `inGetInitialProps` check, because lots of people will
// still call this function in their custom error component's `render` function, but we can get rid of the check for
// `err` and just always bail if we're not in `getInitialProps`.
const inGetInitialProps = contextOrProps.pathname !== undefined;
if (!inGetInitialProps && !err) {
return Promise.resolve();
}

withScope(scope => {
scope.addEventProcessor(event => {
addExceptionMechanism(event, {
type: 'instrument',
handled: true,
data: {
// TODO: Get rid of second half of ternary once https://github.com/vercel/next.js/issues/8592 is fixed.
function: inGetInitialProps ? '_error.getInitialProps' : '_error.customErrorComponent',
},
});
return event;
});

if (req) {
scope.addEventProcessor(event => addRequestDataToEvent(event, req));
}

// If third-party libraries (or users themselves) throw something falsy, we want to capture it as a message (which
// is what passing a string to `captureException` will wind up doing)
const finalError = err || `_error.js called with falsy error (${err})`;

// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
// store a seen flag on it. (Because of https://github.com/vercel/next.js/issues/8592, it can happen that the custom
// error component's `getInitialProps` won't have run, so we have people call this function in their error
// component's main render function in addition to in its `getInitialProps`, just in case. By forcing it to be an
// object, we can flag it as seen, so that if we hit this a second time, we can no-op.)
captureException(objectify(finalError));
});

// In case this is being run as part of a serverless function (as is the case with the server half of nextjs apps
// deployed to vercel), make sure the error gets sent to Sentry before the lambda exits.
await flush(2000);
}