Skip to content

Commit 9c903e3

Browse files
committed
add function to capture exception in _error page
1 parent 7573f19 commit 9c903e3

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)