Skip to content

Commit e855e76

Browse files
authored
feat(nextjs): Add exception handler for _error.js (#5259)
In order to have Sentry capture certain kinds of errors in nextjs, users need to add a custom `_error.js` file to their projects. We do this automatically when users go through the wizard setup, but [the file we've been adding](https://github.com/getsentry/sentry-wizard/blob/v1.2.17/scripts/NextJs/configs/_error.js) is just a clone of the one in vercel's `with-sentry` nextjs example app, and is simultaneously quite verbose and pretty bare-bones in terms of what it does. (This is not a knock on the folks who wrote it, who don't have the context we do, but the fact remains that it could stand to be improved.) This does so, by creating a utility function, `captureUnderscoreErrorException`, for users to use in place of the manual edge-case handling and `captureException` calls in the original. In addition to cleaning things up, this allows us to modify behavior, fix bugs, and add features without users having to update their code. (Existing users will have to update to use the function, of course, but after that they should never have to touch it again. And for new users, it'll be a set-it-and-forget-it.) With this change, the `_error.js` we add with the wizard becomes just ```js import * as Sentry from '@sentry/nextjs'; import NextErrorComponent from 'next/error'; const CustomErrorComponent = props => { Sentry.captureUnderscoreErrorException(props); return <NextErrorComponent statusCode={props.statusCode} />; }; CustomErrorComponent.getInitialProps = async contextData => { await Sentry.captureUnderscoreErrorException(contextData); return NextErrorComponent.getInitialProps(contextData); }; export default CustomErrorComponent; ``` (The real copy has helpful comments, but they've been removed here for the sake of brevity.) And speaking of adding features... why not start now? This new function improves on the existing code by: - filtering out all 400-type errors, not just 404s, - annotating errors with a `mechanism` value, - adding request data to the event when available, and - capturing a message when falsy errors are thrown. (See the PR for screenshots of the difference adding this data makes.) The file injected by the wizard is updated in getsentry/sentry-wizard#170, and the file used in the `with-sentry` example app is updated in vercel/next.js#37866.
1 parent 2870f1d commit e855e76

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

packages/nextjs/src/index.client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { addIntegration, UserIntegrations } from './utils/userIntegrations';
99

1010
export * from '@sentry/react';
1111
export { nextRouterInstrumentation } from './performance/client';
12+
export { captureUnderscoreErrorException } from './utils/_error';
1213

1314
export { Integrations };
1415

packages/nextjs/src/index.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NextjsOptions } from './utils/nextjsOptions';
1212
import { addIntegration } from './utils/userIntegrations';
1313

1414
export * from '@sentry/node';
15+
export { captureUnderscoreErrorException } from './utils/_error';
1516

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

77-
filterTransactions.id = 'NextServer404Filter';
78+
filterTransactions.id = 'NextServer404TransactionFilter';
7879

7980
configureScope(scope => {
8081
scope.setTag('runtime', 'node');

packages/nextjs/src/utils/_error.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { captureException, withScope } from '@sentry/core';
2+
import { getCurrentHub } from '@sentry/hub';
3+
import { addExceptionMechanism, addRequestDataToEvent, objectify } from '@sentry/utils';
4+
import { NextPageContext } from 'next';
5+
6+
type ContextOrProps = {
7+
[key: string]: unknown;
8+
req?: NextPageContext['req'];
9+
res?: NextPageContext['res'];
10+
err?: NextPageContext['err'] | string;
11+
statusCode?: number;
12+
};
13+
14+
/** Platform-agnostic version of `flush` */
15+
function flush(timeout?: number): PromiseLike<boolean> {
16+
const client = getCurrentHub().getClient();
17+
return client ? client.flush(timeout) : Promise.resolve(false);
18+
}
19+
20+
/**
21+
* Capture the exception passed by nextjs to the `_error` page, adding context data as appropriate.
22+
*
23+
* @param contextOrProps The data passed to either `getInitialProps` or `render` by nextjs
24+
*/
25+
export async function captureUnderscoreErrorException(contextOrProps: ContextOrProps): Promise<void> {
26+
const { req, res, err } = contextOrProps;
27+
28+
// 404s (and other 400-y friends) can trigger `_error`, but we don't want to send them to Sentry
29+
const statusCode = (res && res.statusCode) || contextOrProps.statusCode;
30+
if (statusCode && statusCode < 500) {
31+
return Promise.resolve();
32+
}
33+
34+
// Nextjs only passes the pathname in the context data given to `getInitialProps`, not the main render function, but
35+
// unlike `req` and `res`, for which that also applies, it passes it on both server and client.
36+
//
37+
// TODO: This check is only necessary because of the workaround for https://github.com/vercel/next.js/issues/8592
38+
// explained below. Once that's fixed, we'll have to keep the `inGetInitialProps` check, because lots of people will
39+
// still call this function in their custom error component's `render` function, but we can get rid of the check for
40+
// `err` and just always bail if we're not in `getInitialProps`.
41+
const inGetInitialProps = contextOrProps.pathname !== undefined;
42+
if (!inGetInitialProps && !err) {
43+
return Promise.resolve();
44+
}
45+
46+
withScope(scope => {
47+
scope.addEventProcessor(event => {
48+
addExceptionMechanism(event, {
49+
type: 'instrument',
50+
handled: true,
51+
data: {
52+
// TODO: Get rid of second half of ternary once https://github.com/vercel/next.js/issues/8592 is fixed.
53+
function: inGetInitialProps ? '_error.getInitialProps' : '_error.customErrorComponent',
54+
},
55+
});
56+
return event;
57+
});
58+
59+
if (req) {
60+
scope.addEventProcessor(event => addRequestDataToEvent(event, req));
61+
}
62+
63+
// If third-party libraries (or users themselves) throw something falsy, we want to capture it as a message (which
64+
// is what passing a string to `captureException` will wind up doing)
65+
const finalError = err || `_error.js called with falsy error (${err})`;
66+
67+
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
68+
// store a seen flag on it. (Because of https://github.com/vercel/next.js/issues/8592, it can happen that the custom
69+
// error component's `getInitialProps` won't have run, so we have people call this function in their error
70+
// component's main render function in addition to in its `getInitialProps`, just in case. By forcing it to be an
71+
// object, we can flag it as seen, so that if we hit this a second time, we can no-op.)
72+
captureException(objectify(finalError));
73+
});
74+
75+
// In case this is being run as part of a serverless function (as is the case with the server half of nextjs apps
76+
// deployed to vercel), make sure the error gets sent to Sentry before the lambda exits.
77+
await flush(2000);
78+
}

0 commit comments

Comments
 (0)