diff --git a/package.json b/package.json index 72721c43..3581f2c2 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,8 @@ "@fortawesome/free-regular-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/react-fontawesome": "0.1.14", - "@sentry/browser": "6.3.6", - "@sentry/node": "6.3.6", + "@sentry/browser": "6.7.2", + "@sentry/node": "6.7.2", "@types/lodash.isequal": "4.5.5", "@unly/simple-logger": "1.0.0", "@unly/universal-language-detector": "2.0.3", diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 971658db..254c4cd7 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -300,6 +300,12 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele Sentry.captureException(props.err); }); } + } else { + // XXX Opinionated: Record an exception in Sentry for 404, if you don't want this then uncomment the below code + const err = new Error(`Page not found (404) for "${router?.asPath}"`); + + logger.warn(err); + Sentry.captureException(err); } const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend diff --git a/src/app/isNextApiRequest.ts b/src/app/isNextApiRequest.ts new file mode 100644 index 00000000..f523bd9f --- /dev/null +++ b/src/app/isNextApiRequest.ts @@ -0,0 +1,14 @@ +import { IncomingMessage } from 'http'; +import { NextApiRequest } from 'next'; + +/** + * TS type guard resolving whether "req" matches a "NextApiRequest" object. + * + * @param req + * + * @see https://www.typescripttutorial.net/typescript-tutorial/typescript-type-guards/ + * @see https://www.logicbig.com/tutorials/misc/typescript/type-guards.html + */ +export const isNextApiRequest = (req: NextApiRequest | IncomingMessage): req is NextApiRequest => { + return (req as NextApiRequest).body !== undefined; +}; diff --git a/src/modules/core/sentry/config.ts b/src/modules/core/sentry/config.ts index 54a1092d..b3dc37b6 100644 --- a/src/modules/core/sentry/config.ts +++ b/src/modules/core/sentry/config.ts @@ -20,4 +20,4 @@ export const ALERT_TYPES = { * @see https://github.com/vercel/next.js/blob/canary/examples/with-sentry/pages/_error.js#L45 * @see https://vercel.com/docs/platform/limits#streaming-responses */ -export const FLUSH_TIMEOUT = 5000; +export const FLUSH_TIMEOUT = 2000; diff --git a/src/modules/core/sentry/init.ts b/src/modules/core/sentry/init.ts index df3deb54..7f823ae8 100644 --- a/src/modules/core/sentry/init.ts +++ b/src/modules/core/sentry/init.ts @@ -2,12 +2,23 @@ import * as Sentry from '@sentry/node'; import { isBrowser } from '@unly/utils'; /** - * Initialize Sentry and export it. + * Initializes Sentry and exports it. * - * Helper to avoid duplicating the init() call in every /pages/api file. - * Also used in pages/_app for the client side, which automatically applies it for all frontend pages. + * Helper to avoid duplicating the Sentry initialization in: + * - The "/pages/api" files, for the server side. + * - The "pages/_app" file, for the client side, which in turns automatically applies it for all frontend pages. * - * Doesn't initialise Sentry if SENTRY_DSN isn't defined + * Also configures the default scope, subsequent calls to "configureScope" will enrich the scope. + * Must only contain tags/contexts/extras that are universal (not server or browser specific). + * + * The Sentry scope will be enriched by: + * - BrowserPageBootstrap, for browser-specific metadata. + * - ServerPageBootstrap, for server-specific metadata. + * - API endpoints, for per-API additional metadata. + * - React components, for per-component additional metadata. + * + * Doesn't initialize Sentry if SENTRY_DSN isn't defined. + * Re-exports the Sentry object to make it simpler to consume by developers (DX). * * @see https://www.npmjs.com/package/@sentry/node */ @@ -19,13 +30,7 @@ if (process.env.SENTRY_DSN) { release: process.env.NEXT_PUBLIC_APP_VERSION_RELEASE, }); - if (!process.env.SENTRY_DSN && process.env.NODE_ENV !== 'test') { - // eslint-disable-next-line no-console - console.error('Sentry DSN not defined'); - } - - // Scope configured by default, subsequent calls to "configureScope" will add additional data - Sentry.configureScope((scope) => { // See https://www.npmjs.com/package/@sentry/node + Sentry.configureScope((scope) => { scope.setTag('customerRef', process.env.NEXT_PUBLIC_CUSTOMER_REF); scope.setTag('appStage', process.env.NEXT_PUBLIC_APP_STAGE); scope.setTag('appName', process.env.NEXT_PUBLIC_APP_NAME); @@ -40,4 +45,11 @@ if (process.env.SENTRY_DSN) { scope.setTag('memory', process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || null); // Optional - Available on production environment only scope.setTag('runtimeEngine', isBrowser() ? 'browser' : 'server'); }); +} else { + if (process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console + console.error(`Sentry DSN not defined, events (exceptions, messages, etc.) won't be sent to Sentry.`); + } } + +export default Sentry; diff --git a/src/modules/core/sentry/server.ts b/src/modules/core/sentry/server.ts index 1818b295..faed18d2 100644 --- a/src/modules/core/sentry/server.ts +++ b/src/modules/core/sentry/server.ts @@ -1,21 +1,27 @@ +import { isNextApiRequest } from '@/app/isNextApiRequest'; import { convertRequestBodyToJSObject } from '@/modules/core/api/convertRequestBodyToJSObject'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; -import * as Sentry from '@sentry/node'; +import Sentry from '@/modules/core/sentry/init'; +import { IncomingMessage } from 'http'; // Automatically inits Sentry during import import map from 'lodash.map'; import { NextApiRequest } from 'next'; /** - * Configure the Sentry scope by extracting useful tags and context from the given request. + * Configures the Sentry scope by extracting useful tags and context from the given request. + * + * XXX Because it imports Sentry from "@/modules/core/sentry/init", it automatically initializes Sentry as well * * @param req * @param tags * @param contexts * @see https://www.npmjs.com/package/@sentry/node */ -export const configureReq = (req: NextApiRequest, tags?: { [key: string]: string }, contexts?: { [key: string]: any }): void => { +export const configureReq = (req: NextApiRequest | IncomingMessage, tags?: { [key: string]: string }, contexts?: { [key: string]: any }): void => { let parsedBody: GenericObject = {}; try { - parsedBody = convertRequestBodyToJSObject(req); + if (isNextApiRequest(req)) { + parsedBody = convertRequestBodyToJSObject(req); + } } catch (e) { // eslint-disable-next-line no-console // console.error(e); @@ -25,12 +31,15 @@ export const configureReq = (req: NextApiRequest, tags?: { [key: string]: string scope.setTag('host', req?.headers?.host); scope.setTag('url', req?.url); scope.setTag('method', req?.method); - scope.setExtra('query', req?.query); - scope.setExtra('body', req?.body); - scope.setExtra('cookies', req?.cookies); scope.setContext('headers', req?.headers); scope.setContext('parsedBody', parsedBody); + if (isNextApiRequest(req)) { + scope.setExtra('query', req?.query); + scope.setExtra('body', req?.body); + scope.setExtra('cookies', req?.cookies); + } + map(tags, (value: string, tag: string) => { scope.setTag(tag, value); }); diff --git a/src/pages/[locale]/demo/built-in-utilities/errors-handling.tsx b/src/pages/[locale]/demo/built-in-utilities/errors-handling.tsx index f3b4296e..cbc85171 100644 --- a/src/pages/[locale]/demo/built-in-utilities/errors-handling.tsx +++ b/src/pages/[locale]/demo/built-in-utilities/errors-handling.tsx @@ -77,7 +77,7 @@ const ErrorsHandlingPage: NextPage = (props): JSX.Element => {

404 - Using CSR

- This page doesn't exist and should display a 404 page. + This page doesn't exist and should display a 404 page. The error will be reported to Sentry.

@@ -98,7 +98,7 @@ const ErrorsHandlingPage: NextPage = (props): JSX.Element => {

404 - Using full page reload

- This page doesn't exist and should display a 404 page. + This page doesn't exist and should display a 404 page. The error will be reported to Sentry.

@@ -123,6 +123,7 @@ const ErrorsHandlingPage: NextPage = (props): JSX.Element => { This page throws an error right from the Page component and should display a 500 page error without anything else (no footer/header). + The error will be reported to Sentry. = (props): JSX.Element => {


-

Interactive error (simulating User interaction)

+

Interactive errors (simulating User interaction)

Go to interactive error page
diff --git a/src/pages/[locale]/demo/built-in-utilities/interactive-error.tsx b/src/pages/[locale]/demo/built-in-utilities/interactive-error.tsx index a2eb677d..53dfd434 100644 --- a/src/pages/[locale]/demo/built-in-utilities/interactive-error.tsx +++ b/src/pages/[locale]/demo/built-in-utilities/interactive-error.tsx @@ -76,7 +76,7 @@ const InteractiveErrorPage: NextPage = (props): JSX.Element => {