Skip to content

Commit 5ea63ac

Browse files
committed
Fix Sentry config on the server side (#377)
* Re-export Sentry from init (DX) and call init from error endpoint to see if it fixes the issue with flush() timeout * Improve sentry init documentation + rollback to timeout 2s + auto-init Sentry when using configureReq * Implement better error handling on _error page (enrich Sentry metadata from req object) * Fix 404 error handling and reporting to Sentry * Improve documentation about error handling * Upgrade Sentry to latest (minor)
1 parent 9a4e137 commit 5ea63ac

File tree

10 files changed

+131
-90
lines changed

10 files changed

+131
-90
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@
117117
"@fortawesome/free-regular-svg-icons": "5.15.3",
118118
"@fortawesome/free-solid-svg-icons": "5.15.3",
119119
"@fortawesome/react-fontawesome": "0.1.14",
120-
"@sentry/browser": "6.3.6",
121-
"@sentry/node": "6.3.6",
120+
"@sentry/browser": "6.7.2",
121+
"@sentry/node": "6.7.2",
122122
"@types/lodash.isequal": "4.5.5",
123123
"@unly/simple-logger": "1.0.0",
124124
"@unly/universal-language-detector": "2.0.3",

src/app/components/MultiversalAppBootstrap.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
301301
Sentry.captureException(props.err);
302302
});
303303
}
304+
} else {
305+
// XXX Opinionated: Record an exception in Sentry for 404, if you don't want this then uncomment the below code
306+
const err = new Error(`Page not found (404) for "${router?.asPath}"`);
307+
308+
logger.warn(err);
309+
Sentry.captureException(err);
304310
}
305311

306312
const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend

src/app/isNextApiRequest.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { IncomingMessage } from 'http';
2+
import { NextApiRequest } from 'next';
3+
4+
/**
5+
* TS type guard resolving whether "req" matches a "NextApiRequest" object.
6+
*
7+
* @param req
8+
*
9+
* @see https://www.typescripttutorial.net/typescript-tutorial/typescript-type-guards/
10+
* @see https://www.logicbig.com/tutorials/misc/typescript/type-guards.html
11+
*/
12+
export const isNextApiRequest = (req: NextApiRequest | IncomingMessage): req is NextApiRequest => {
13+
return (req as NextApiRequest).body !== undefined;
14+
};

src/modules/core/sentry/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ export const ALERT_TYPES = {
2020
* @see https://github.com/vercel/next.js/blob/canary/examples/with-sentry/pages/_error.js#L45
2121
* @see https://vercel.com/docs/platform/limits#streaming-responses
2222
*/
23-
export const FLUSH_TIMEOUT = 5000;
23+
export const FLUSH_TIMEOUT = 2000;

src/modules/core/sentry/init.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@ import * as Sentry from '@sentry/node';
22
import { isBrowser } from '@unly/utils';
33

44
/**
5-
* Initialize Sentry and export it.
5+
* Initializes Sentry and exports it.
66
*
7-
* Helper to avoid duplicating the init() call in every /pages/api file.
8-
* Also used in pages/_app for the client side, which automatically applies it for all frontend pages.
7+
* Helper to avoid duplicating the Sentry initialization in:
8+
* - The "/pages/api" files, for the server side.
9+
* - The "pages/_app" file, for the client side, which in turns automatically applies it for all frontend pages.
910
*
10-
* Doesn't initialise Sentry if SENTRY_DSN isn't defined
11+
* Also configures the default scope, subsequent calls to "configureScope" will enrich the scope.
12+
* Must only contain tags/contexts/extras that are universal (not server or browser specific).
13+
*
14+
* The Sentry scope will be enriched by:
15+
* - BrowserPageBootstrap, for browser-specific metadata.
16+
* - ServerPageBootstrap, for server-specific metadata.
17+
* - API endpoints, for per-API additional metadata.
18+
* - React components, for per-component additional metadata.
19+
*
20+
* Doesn't initialize Sentry if SENTRY_DSN isn't defined.
21+
* Re-exports the Sentry object to make it simpler to consume by developers (DX).
1122
*
1223
* @see https://www.npmjs.com/package/@sentry/node
1324
*/
@@ -19,13 +30,7 @@ if (process.env.SENTRY_DSN) {
1930
release: process.env.NEXT_PUBLIC_APP_VERSION_RELEASE,
2031
});
2132

22-
if (!process.env.SENTRY_DSN && process.env.NODE_ENV !== 'test') {
23-
// eslint-disable-next-line no-console
24-
console.error('Sentry DSN not defined');
25-
}
26-
27-
// Scope configured by default, subsequent calls to "configureScope" will add additional data
28-
Sentry.configureScope((scope) => { // See https://www.npmjs.com/package/@sentry/node
33+
Sentry.configureScope((scope) => {
2934
scope.setTag('customerRef', process.env.NEXT_PUBLIC_CUSTOMER_REF);
3035
scope.setTag('appStage', process.env.NEXT_PUBLIC_APP_STAGE);
3136
scope.setTag('appName', process.env.NEXT_PUBLIC_APP_NAME);
@@ -40,4 +45,11 @@ if (process.env.SENTRY_DSN) {
4045
scope.setTag('memory', process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || null); // Optional - Available on production environment only
4146
scope.setTag('runtimeEngine', isBrowser() ? 'browser' : 'server');
4247
});
48+
} else {
49+
if (process.env.NODE_ENV !== 'test') {
50+
// eslint-disable-next-line no-console
51+
console.error(`Sentry DSN not defined, events (exceptions, messages, etc.) won't be sent to Sentry.`);
52+
}
4353
}
54+
55+
export default Sentry;

src/modules/core/sentry/server.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1+
import { isNextApiRequest } from '@/app/isNextApiRequest';
12
import { convertRequestBodyToJSObject } from '@/modules/core/api/convertRequestBodyToJSObject';
23
import { GenericObject } from '@/modules/core/data/types/GenericObject';
3-
import * as Sentry from '@sentry/node';
4+
import Sentry from '@/modules/core/sentry/init';
5+
import { IncomingMessage } from 'http'; // Automatically inits Sentry during import
46
import map from 'lodash.map';
57
import { NextApiRequest } from 'next';
68

79
/**
8-
* Configure the Sentry scope by extracting useful tags and context from the given request.
10+
* Configures the Sentry scope by extracting useful tags and context from the given request.
11+
*
12+
* XXX Because it imports Sentry from "@/modules/core/sentry/init", it automatically initializes Sentry as well
913
*
1014
* @param req
1115
* @param tags
1216
* @param contexts
1317
* @see https://www.npmjs.com/package/@sentry/node
1418
*/
15-
export const configureReq = (req: NextApiRequest, tags?: { [key: string]: string }, contexts?: { [key: string]: any }): void => {
19+
export const configureReq = (req: NextApiRequest | IncomingMessage, tags?: { [key: string]: string }, contexts?: { [key: string]: any }): void => {
1620
let parsedBody: GenericObject = {};
1721
try {
18-
parsedBody = convertRequestBodyToJSObject(req);
22+
if (isNextApiRequest(req)) {
23+
parsedBody = convertRequestBodyToJSObject(req);
24+
}
1925
} catch (e) {
2026
// eslint-disable-next-line no-console
2127
// console.error(e);
@@ -25,12 +31,15 @@ export const configureReq = (req: NextApiRequest, tags?: { [key: string]: string
2531
scope.setTag('host', req?.headers?.host);
2632
scope.setTag('url', req?.url);
2733
scope.setTag('method', req?.method);
28-
scope.setExtra('query', req?.query);
29-
scope.setExtra('body', req?.body);
30-
scope.setExtra('cookies', req?.cookies);
3134
scope.setContext('headers', req?.headers);
3235
scope.setContext('parsedBody', parsedBody);
3336

37+
if (isNextApiRequest(req)) {
38+
scope.setExtra('query', req?.query);
39+
scope.setExtra('body', req?.body);
40+
scope.setExtra('cookies', req?.cookies);
41+
}
42+
3443
map(tags, (value: string, tag: string) => {
3544
scope.setTag(tag, value);
3645
});

src/pages/[locale]/demo/built-in-utilities/errors-handling.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
7777
<h2>404 - Using CSR</h2>
7878

7979
<Alert color={'info'}>
80-
This page doesn't exist and should display a 404 page.
80+
This page doesn't exist and should display a 404 page. The error will be reported to Sentry.
8181
</Alert>
8282

8383
<p>
@@ -98,7 +98,7 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
9898
<h2>404 - Using full page reload</h2>
9999

100100
<Alert color={'info'}>
101-
This page doesn't exist and should display a 404 page.
101+
This page doesn't exist and should display a 404 page. The error will be reported to Sentry.
102102
</Alert>
103103

104104
<p>
@@ -123,6 +123,7 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
123123

124124
<Alert color={'info'}>
125125
This page throws an error right from the Page component and should display a 500 page error without anything else (no footer/header).
126+
The error will be reported to Sentry.
126127
</Alert>
127128

128129
<Code
@@ -162,7 +163,7 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
162163

163164
<hr />
164165

165-
<h2>Interactive error (simulating User interaction)</h2>
166+
<h2>Interactive errors (simulating User interaction)</h2>
166167

167168
<Btn mode={'primary-outline'}>
168169
<I18nLink href={'/demo/built-in-utilities/interactive-error'}>Go to interactive error page</I18nLink><br />

src/pages/[locale]/demo/built-in-utilities/interactive-error.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const InteractiveErrorPage: NextPage<Props> = (props): JSX.Element => {
7676
<Button
7777
onClick={(): void => {
7878
setIsClicked(true);
79-
throw new Error('Page 500 error example');
79+
throw new Error('Page 500 error example (handled)');
8080
}}
8181
>
8282
Will it crash the whole app?
@@ -99,7 +99,7 @@ const InteractiveErrorPage: NextPage<Props> = (props): JSX.Element => {
9999
<Button
100100
onClick={(): void => {
101101
setIsClicked(true);
102-
throw new Error('Page 500 error example');
102+
throw new Error('Page 500 error example (handled)');
103103
}}
104104
>
105105
Will it crash the whole app?

src/pages/_error.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,19 @@ const ErrorPage = (props: ErrorPageProps): JSX.Element => {
8686
};
8787

8888
/**
89-
* TODO Doc - Is this only called when the error happens server-side?
90-
* What's the point of getInitialProps when using SSG or hybrid apps?
89+
* Might be called from the server and the client side.
90+
*
91+
* Won't be called for 404 errors (those are caught in MultiversalPageBootstrap).
92+
*
93+
* XXX Question: What's the point of getInitialProps when using SSG or hybrid apps? Is it being used? In what cases?
9194
*
9295
* @param props
9396
*
9497
* @see https://github.com/vercel/next.js/blob/canary/examples/with-sentry/pages/_error.js
9598
*/
9699
ErrorPage.getInitialProps = async (props: NextPageContext): Promise<ErrorProps> => {
97100
const {
101+
req,
98102
res,
99103
err,
100104
asPath,
@@ -115,13 +119,9 @@ ErrorPage.getInitialProps = async (props: NextPageContext): Promise<ErrorProps>
115119
if (res) {
116120
// Running on the server, the response object is available.
117121
//
118-
// Next.js will pass an err on the server if a page's `getInitialProps`
119-
// threw or returned a Promise that rejected
120-
121-
// XXX Opinionated: Record an exception in Sentry for 404, if you don't want this then uncomment the below code
122-
// if (res.statusCode === 404) {
123-
// return { statusCode: 404, isReadyToRender: true };
124-
// }
122+
// Next.js will pass an err on the server if a page's `getInitialProps` threw or returned a Promise that rejected
123+
const configureReq = (await import('@/modules/core/sentry/server')).configureReq;
124+
configureReq(req);
125125

126126
if (err) {
127127
Sentry.captureException(err);
@@ -134,7 +134,6 @@ ErrorPage.getInitialProps = async (props: NextPageContext): Promise<ErrorProps>
134134
// Running on the client (browser).
135135
//
136136
// Next.js will provide an err if:
137-
//
138137
// - a page's `getInitialProps` threw or returned a Promise that rejected
139138
// - an exception was thrown somewhere in the React lifecycle (render,
140139
// componentDidMount, etc) that was caught by Next.js's React Error

yarn.lock

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,82 +2206,82 @@
22062206
dependencies:
22072207
any-observable "^0.3.0"
22082208

2209-
"@sentry/browser@6.3.6":
2210-
version "6.3.6"
2211-
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.3.6.tgz#bba07033efded6c844de88dcc47f99548a29afed"
2212-
integrity sha512-l4323jxuBOArki6Wf+EHes39IEyJ2Zj/CIUaTY7GWh7CntpfHQAfFmZWQw3Ozq+ka1u8lVp25RPhb4Wng3azNA==
2213-
dependencies:
2214-
"@sentry/core" "6.3.6"
2215-
"@sentry/types" "6.3.6"
2216-
"@sentry/utils" "6.3.6"
2209+
"@sentry/browser@6.7.2":
2210+
version "6.7.2"
2211+
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.7.2.tgz#cfbe060de5a9694617f175a6bde469e5e266792e"
2212+
integrity sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==
2213+
dependencies:
2214+
"@sentry/core" "6.7.2"
2215+
"@sentry/types" "6.7.2"
2216+
"@sentry/utils" "6.7.2"
22172217
tslib "^1.9.3"
22182218

2219-
"@sentry/core@6.3.6":
2220-
version "6.3.6"
2221-
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.3.6.tgz#e2ec6ae7e456e61f28000bab2d8ce85f58c59c66"
2222-
integrity sha512-w6BRizAqh7BaiM9oeKzO6aACXwRijUPacYaVLX/OfhqCSueF9uDxpMRT7+4D/eCeDVqgJYhBJ4Vsu2NSstkk4A==
2219+
"@sentry/core@6.7.2":
2220+
version "6.7.2"
2221+
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.7.2.tgz#1d294fac6e62744bce3b9dfbcd90b14e93620480"
2222+
integrity sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==
22232223
dependencies:
2224-
"@sentry/hub" "6.3.6"
2225-
"@sentry/minimal" "6.3.6"
2226-
"@sentry/types" "6.3.6"
2227-
"@sentry/utils" "6.3.6"
2224+
"@sentry/hub" "6.7.2"
2225+
"@sentry/minimal" "6.7.2"
2226+
"@sentry/types" "6.7.2"
2227+
"@sentry/utils" "6.7.2"
22282228
tslib "^1.9.3"
22292229

2230-
"@sentry/hub@6.3.6":
2231-
version "6.3.6"
2232-
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.3.6.tgz#e7bc6960e30d8731e23c6e77f31af0bfb1d5af3c"
2233-
integrity sha512-foBZ3ilMnm9Gf9OolrAxYHK8jrA6IF72faDdJ3Al+1H27qcpnBaMdrdEp2/jzwu/dgmwuLmbBaMjEPXaGH/0JQ==
2230+
"@sentry/hub@6.7.2":
2231+
version "6.7.2"
2232+
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.7.2.tgz#31b250e74aa303877620dfa500aa89e4411e2dec"
2233+
integrity sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==
22342234
dependencies:
2235-
"@sentry/types" "6.3.6"
2236-
"@sentry/utils" "6.3.6"
2235+
"@sentry/types" "6.7.2"
2236+
"@sentry/utils" "6.7.2"
22372237
tslib "^1.9.3"
22382238

2239-
"@sentry/minimal@6.3.6":
2240-
version "6.3.6"
2241-
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.3.6.tgz#aebcebd2ee9007b0ec505b9fcefd10f10fc5d43d"
2242-
integrity sha512-uM2/dH0a6zfvI5f+vg+/mST+uTBdN6Jgpm585ipH84ckCYQwIIDRg6daqsen4S1sy/xgg1P1YyC3zdEC4G6b1Q==
2239+
"@sentry/minimal@6.7.2":
2240+
version "6.7.2"
2241+
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.7.2.tgz#9e6c0c587daea64a9042041694a4ad5d559d16cd"
2242+
integrity sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==
22432243
dependencies:
2244-
"@sentry/hub" "6.3.6"
2245-
"@sentry/types" "6.3.6"
2244+
"@sentry/hub" "6.7.2"
2245+
"@sentry/types" "6.7.2"
22462246
tslib "^1.9.3"
22472247

2248-
"@sentry/node@6.3.6":
2249-
version "6.3.6"
2250-
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.3.6.tgz#e584500a10a9162d47fc8b55c9bf2ac3fec8d9f9"
2251-
integrity sha512-QVWakREgVUV/rocm4uMq+RkC0/g9d/z2BYic+2b0ZZMZD2aXF5RulrUQlAO2MzoXcO+bqpkXQsqdhMccqB4Zeg==
2248+
"@sentry/node@6.7.2":
2249+
version "6.7.2"
2250+
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.7.2.tgz#ef2b865af2c37d83966db7fbd031179aa8c82cc0"
2251+
integrity sha512-vfNTmxBbHthAKPDBo0gVk/aNHdgUfXLzmaK7FgWO7cISiI2soCfvKEIP61XqIFZru06teqcRuDsYlR4wSeyWpg==
22522252
dependencies:
2253-
"@sentry/core" "6.3.6"
2254-
"@sentry/hub" "6.3.6"
2255-
"@sentry/tracing" "6.3.6"
2256-
"@sentry/types" "6.3.6"
2257-
"@sentry/utils" "6.3.6"
2253+
"@sentry/core" "6.7.2"
2254+
"@sentry/hub" "6.7.2"
2255+
"@sentry/tracing" "6.7.2"
2256+
"@sentry/types" "6.7.2"
2257+
"@sentry/utils" "6.7.2"
22582258
cookie "^0.4.1"
22592259
https-proxy-agent "^5.0.0"
22602260
lru_map "^0.3.3"
22612261
tslib "^1.9.3"
22622262

2263-
"@sentry/tracing@6.3.6":
2264-
version "6.3.6"
2265-
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.3.6.tgz#dc2aced01cdc401f97d6027113f6313503ee9c91"
2266-
integrity sha512-dfyYY2eESJGt5Qbigmfmb2U9ntqbwPhLNAOcjKaVg9WQRV5q2RkHCVctPoYk7TEAvfNeNRXCD8SnuFOZhttt8g==
2263+
"@sentry/tracing@6.7.2":
2264+
version "6.7.2"
2265+
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.7.2.tgz#78a6934837143ae5e200b49bd256bc8a917477bc"
2266+
integrity sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==
22672267
dependencies:
2268-
"@sentry/hub" "6.3.6"
2269-
"@sentry/minimal" "6.3.6"
2270-
"@sentry/types" "6.3.6"
2271-
"@sentry/utils" "6.3.6"
2268+
"@sentry/hub" "6.7.2"
2269+
"@sentry/minimal" "6.7.2"
2270+
"@sentry/types" "6.7.2"
2271+
"@sentry/utils" "6.7.2"
22722272
tslib "^1.9.3"
22732273

2274-
"@sentry/types@6.3.6":
2275-
version "6.3.6"
2276-
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.6.tgz#aa3687051af1dc04ebc4eaf7f9562872da67aa5c"
2277-
integrity sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==
2274+
"@sentry/types@6.7.2":
2275+
version "6.7.2"
2276+
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.7.2.tgz#8108272c98ad7784ddf9ddda0b7bdc6880ed6e50"
2277+
integrity sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg==
22782278

2279-
"@sentry/utils@6.3.6":
2280-
version "6.3.6"
2281-
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.3.6.tgz#6f619a525f2a94fa6b160500f63f4bd5bd171055"
2282-
integrity sha512-HnYlDBf8Dq8MEv7AulH7B6R1D/2LAooVclGdjg48tSrr9g+31kmtj+SAj2WWVHP9+bp29BWaC7i5nkfKrOibWw==
2279+
"@sentry/utils@6.7.2":
2280+
version "6.7.2"
2281+
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.7.2.tgz#c7f957ebe16de3e701a0c5477ac2dba04e7b4b68"
2282+
integrity sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==
22832283
dependencies:
2284-
"@sentry/types" "6.3.6"
2284+
"@sentry/types" "6.7.2"
22852285
tslib "^1.9.3"
22862286

22872287
"@sindresorhus/is@^0.14.0":

0 commit comments

Comments
 (0)