From 493e16fe11084774c18cfed110333ef43c9d4cf4 Mon Sep 17 00:00:00 2001 From: Vadorequest Date: Thu, 24 Jun 2021 13:29:57 +0200 Subject: [PATCH] Refactor Sentry + fix Sentry usage in API endpoints (#375) * Refactor Sentry, split code between files depending on usage (browser/server/universal) * Always flush Sentry in API endpoints * Always flush Sentry in _error handling --- src/app/components/BrowserPageBootstrap.tsx | 6 +- .../components/MultiversalAppBootstrap.tsx | 2 +- src/app/components/ServerPageBootstrap.tsx | 2 +- src/layouts/core/components/CoreLayout.tsx | 2 +- src/layouts/demo/components/DemoLayout.tsx | 2 +- .../components/QuickPreviewLayout.tsx | 2 +- .../core/githubActions/dispatchWorkflow.ts | 3 +- .../githubActions/dispatchWorkflowByPath.ts | 2 +- src/modules/core/sentry/browser.ts | 25 +++ src/modules/core/sentry/config.ts | 23 +++ src/modules/core/sentry/init.ts | 45 +++++ src/modules/core/sentry/sentry.ts | 162 ------------------ src/modules/core/sentry/server.ts | 42 +++++ src/modules/core/sentry/universal.ts | 37 ++++ src/pages/_app.tsx | 2 +- src/pages/_error.tsx | 9 + src/pages/api/error.ts | 7 +- src/pages/api/preview.ts | 10 +- src/pages/api/startVercelDeployment.ts | 7 +- src/pages/api/status.ts | 7 +- src/pages/api/webhooks/deploymentCompleted.ts | 14 +- 21 files changed, 227 insertions(+), 184 deletions(-) create mode 100644 src/modules/core/sentry/browser.ts create mode 100644 src/modules/core/sentry/config.ts create mode 100644 src/modules/core/sentry/init.ts delete mode 100644 src/modules/core/sentry/sentry.ts create mode 100644 src/modules/core/sentry/server.ts create mode 100644 src/modules/core/sentry/universal.ts diff --git a/src/app/components/BrowserPageBootstrap.tsx b/src/app/components/BrowserPageBootstrap.tsx index 1c74170f..ad3f416c 100644 --- a/src/app/components/BrowserPageBootstrap.tsx +++ b/src/app/components/BrowserPageBootstrap.tsx @@ -14,10 +14,8 @@ import { getClientNetworkConnectionType, getClientNetworkInformationSpeed, } from '@/modules/core/networkInformation/networkInformation'; -import { - configureSentryBrowserMetadata, - configureSentryUserMetadata, -} from '@/modules/core/sentry/sentry'; +import { configureSentryBrowserMetadata } from '@/modules/core/sentry/browser'; +import { configureSentryUserMetadata } from '@/modules/core/sentry/universal'; import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; import { CYPRESS_WINDOW_NS, diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 39a5c4bd..971658db 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -20,7 +20,7 @@ import { stopPreviewMode, } from '@/modules/core/previewMode/previewMode'; import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; -import { configureSentryI18n } from '@/modules/core/sentry/sentry'; +import { configureSentryI18n } from '@/modules/core/sentry/universal'; import deserializeSafe from '@/modules/core/serializeSafe/deserializeSafe'; import { detectCypress } from '@/modules/core/testing/cypress'; import { initCustomerTheme } from '@/modules/core/theming/theme'; diff --git a/src/app/components/ServerPageBootstrap.tsx b/src/app/components/ServerPageBootstrap.tsx index 032e9a23..a5a1d4d7 100644 --- a/src/app/components/ServerPageBootstrap.tsx +++ b/src/app/components/ServerPageBootstrap.tsx @@ -1,7 +1,7 @@ import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; import { OnlyServerPageProps } from '@/layouts/core/types/OnlyServerPageProps'; import { createLogger } from '@/modules/core/logging/logger'; -import { configureSentryUserMetadata } from '@/modules/core/sentry/sentry'; +import { configureSentryUserMetadata } from '@/modules/core/sentry/universal'; import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; import * as Sentry from '@sentry/node'; import React from 'react'; diff --git a/src/layouts/core/components/CoreLayout.tsx b/src/layouts/core/components/CoreLayout.tsx index aedf24b4..ac90cd23 100644 --- a/src/layouts/core/components/CoreLayout.tsx +++ b/src/layouts/core/components/CoreLayout.tsx @@ -3,7 +3,7 @@ import { GenericObject } from '@/modules/core/data/types/GenericObject'; import DefaultErrorLayout from '@/modules/core/errorHandling/DefaultErrorLayout'; import { createLogger } from '@/modules/core/logging/logger'; import PreviewModeBanner from '@/modules/core/previewMode/components/PreviewModeBanner'; -import Sentry from '@/modules/core/sentry/sentry'; +import Sentry from '@/modules/core/sentry/init'; import ErrorPage from '@/pages/_error'; import { Amplitude, diff --git a/src/layouts/demo/components/DemoLayout.tsx b/src/layouts/demo/components/DemoLayout.tsx index 3d7d02a6..6d5dc758 100644 --- a/src/layouts/demo/components/DemoLayout.tsx +++ b/src/layouts/demo/components/DemoLayout.tsx @@ -3,7 +3,7 @@ import { GenericObject } from '@/modules/core/data/types/GenericObject'; import DefaultErrorLayout from '@/modules/core/errorHandling/DefaultErrorLayout'; import { createLogger } from '@/modules/core/logging/logger'; import PreviewModeBanner from '@/modules/core/previewMode/components/PreviewModeBanner'; -import Sentry from '@/modules/core/sentry/sentry'; +import Sentry from '@/modules/core/sentry/init'; import ErrorPage from '@/pages/_error'; import { Amplitude, diff --git a/src/layouts/quickPreview/components/QuickPreviewLayout.tsx b/src/layouts/quickPreview/components/QuickPreviewLayout.tsx index a8f2f7f7..db65c883 100644 --- a/src/layouts/quickPreview/components/QuickPreviewLayout.tsx +++ b/src/layouts/quickPreview/components/QuickPreviewLayout.tsx @@ -2,7 +2,7 @@ import Head, { HeadProps } from '@/layouts/core/components/CoreHead'; import { SoftPageProps } from '@/layouts/core/types/SoftPageProps'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry from '@/modules/core/sentry/sentry'; +import Sentry from '@/modules/core/sentry/init'; import { Amplitude, LogOnMount, diff --git a/src/modules/core/githubActions/dispatchWorkflow.ts b/src/modules/core/githubActions/dispatchWorkflow.ts index edea80d4..ca7ea837 100644 --- a/src/modules/core/githubActions/dispatchWorkflow.ts +++ b/src/modules/core/githubActions/dispatchWorkflow.ts @@ -1,5 +1,6 @@ import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { ALERT_TYPES } from '../sentry/sentry'; +import { ALERT_TYPES } from '@/modules/core/sentry/config'; +import Sentry from '../sentry/init'; import { WorkflowsAPIResponse } from './types/WorkflowsAPIResponse'; const fileLabel = 'modules/core/githubActions/dispatchWorkflow'; diff --git a/src/modules/core/githubActions/dispatchWorkflowByPath.ts b/src/modules/core/githubActions/dispatchWorkflowByPath.ts index 52d0f2fa..2212bd3c 100644 --- a/src/modules/core/githubActions/dispatchWorkflowByPath.ts +++ b/src/modules/core/githubActions/dispatchWorkflowByPath.ts @@ -4,7 +4,7 @@ import { GITHUB_REPO_NAME, } from '@/app/constants'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry from '../sentry/sentry'; +import Sentry from '../sentry/init'; import dispatchWorkflow from './dispatchWorkflow'; import { WorkflowsAPIResponse } from './types/WorkflowsAPIResponse'; diff --git a/src/modules/core/sentry/browser.ts b/src/modules/core/sentry/browser.ts new file mode 100644 index 00000000..c550dcee --- /dev/null +++ b/src/modules/core/sentry/browser.ts @@ -0,0 +1,25 @@ +import { + ClientNetworkConnectionType, + ClientNetworkInformationSpeed, +} from '@/modules/core/networkInformation/networkInformation'; +import * as Sentry from '@sentry/node'; + +/** + * Configure Sentry tags related to the browser metadata. + * + * @param networkSpeed + * @param networkConnectionType + * @param isInIframe + * @param iframeReferrer + */ +export const configureSentryBrowserMetadata = (networkSpeed: ClientNetworkInformationSpeed, networkConnectionType: ClientNetworkConnectionType, isInIframe: boolean, iframeReferrer: string): void => { + if (process.env.SENTRY_DSN) { + Sentry.configureScope((scope) => { + scope.setTag('networkSpeed', networkSpeed); + scope.setTag('networkConnectionType', networkConnectionType); + scope.setTag('iframe', `${isInIframe}`); + scope.setExtra('iframe', isInIframe); + scope.setExtra('iframeReferrer', iframeReferrer); + }); + } +}; diff --git a/src/modules/core/sentry/config.ts b/src/modules/core/sentry/config.ts new file mode 100644 index 00000000..b3dc37b6 --- /dev/null +++ b/src/modules/core/sentry/config.ts @@ -0,0 +1,23 @@ +/** + * Alert types, meant to be assigned to "alertType" tag when reporting a message/exception/event to Sentry. + * + * Then, you can configure your own Sentry Alerts using the "alertType" tag and perform specific data processing. + * @example If the event's tags match "alertType equals 'vercel-deployment-invoked'", then send it to a dedicated Slack channel. + * + * @see https://sentry.io/organizations/unly/alerts/next-right-now/new/ + */ +export const ALERT_TYPES = { + VERCEL_DEPLOYMENT_INVOKED: 'vercel-deployment-invoked', + VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT: 'vercel-deployment-trigger-attempt', + VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_FAILED: 'vercel-deployment-trigger-attempt-failed', + VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_SUCCEEDED: 'vercel-deployment-trigger-attempt-succeeded', + VERCEL_DEPLOYMENT_COMPLETED: 'vercel-deployment-completed', +}; + +/** + * Maximum time in ms the Sentry client (browser or server) should wait. + * + * @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 = 2000; diff --git a/src/modules/core/sentry/init.ts b/src/modules/core/sentry/init.ts new file mode 100644 index 00000000..46ff83c9 --- /dev/null +++ b/src/modules/core/sentry/init.ts @@ -0,0 +1,45 @@ +import * as Sentry from '@sentry/node'; +import { isBrowser } from '@unly/utils'; + +/** + * Initialize Sentry and export 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. + * + * Doesn't initialise Sentry if SENTRY_DSN isn't defined + * + * @see https://www.npmjs.com/package/@sentry/node + */ +if (process.env.SENTRY_DSN) { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + enabled: process.env.NODE_ENV !== 'test', + environment: process.env.NEXT_PUBLIC_APP_STAGE, + 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 + 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); + scope.setTag('appBaseUrl', process.env.NEXT_PUBLIC_APP_BASE_URL); + scope.setTag('appVersion', process.env.NEXT_PUBLIC_APP_VERSION); + scope.setTag('appNameVersion', process.env.NEXT_PUBLIC_APP_NAME_VERSION); + scope.setTag('appBuildTime', process.env.NEXT_PUBLIC_APP_BUILD_TIME); + scope.setTag('buildTimeISO', (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME || null)).toISOString()); + scope.setTag('appBuildId', process.env.NEXT_PUBLIC_APP_BUILD_ID); + scope.setTag('nodejs', process.version); + scope.setTag('nodejsAWS', process.env.AWS_EXECUTION_ENV || null); // Optional - Available on production environment only + scope.setTag('memory', process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || null); // Optional - Available on production environment only + scope.setTag('runtimeEngine', isBrowser() ? 'browser' : 'server'); + }); +} + +export default Sentry; diff --git a/src/modules/core/sentry/sentry.ts b/src/modules/core/sentry/sentry.ts deleted file mode 100644 index 6ea0c7b1..00000000 --- a/src/modules/core/sentry/sentry.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - ClientNetworkConnectionType, - ClientNetworkInformationSpeed, -} from '@/modules/core/networkInformation/networkInformation'; -import * as Sentry from '@sentry/node'; -import { isBrowser } from '@unly/utils'; -import map from 'lodash.map'; -import { NextApiRequest } from 'next'; -import { convertRequestBodyToJSObject } from '../api/convertRequestBodyToJSObject'; -import { GenericObject } from '../data/types/GenericObject'; -import { UserSession } from '../userSession/useUserSession'; - -/** - * Initialize Sentry and export 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. - * - * Doesn't initialise Sentry if SENTRY_DSN isn't defined - * - * @see https://www.npmjs.com/package/@sentry/node - */ -if (process.env.SENTRY_DSN) { - Sentry.init({ - dsn: process.env.SENTRY_DSN, - enabled: process.env.NODE_ENV !== 'test', - environment: process.env.NEXT_PUBLIC_APP_STAGE, - 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 - 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); - scope.setTag('appBaseUrl', process.env.NEXT_PUBLIC_APP_BASE_URL); - scope.setTag('appVersion', process.env.NEXT_PUBLIC_APP_VERSION); - scope.setTag('appNameVersion', process.env.NEXT_PUBLIC_APP_NAME_VERSION); - scope.setTag('appBuildTime', process.env.NEXT_PUBLIC_APP_BUILD_TIME); - scope.setTag('buildTimeISO', (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME || null)).toISOString()); - scope.setTag('appBuildId', process.env.NEXT_PUBLIC_APP_BUILD_ID); - scope.setTag('nodejs', process.version); - scope.setTag('nodejsAWS', process.env.AWS_EXECUTION_ENV || null); // Optional - Available on production environment only - scope.setTag('memory', process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || null); // Optional - Available on production environment only - scope.setTag('runtimeEngine', isBrowser() ? 'browser' : 'server'); - }); -} - -/** - * Alert types, meant to be assigned to "alertType" tag when reporting a message/exception/event to Sentry. - * - * Then, you can configure your own Sentry Alerts using the "alertType" tag and perform specific data processing. - * @example If the event's tags match "alertType equals 'vercel-deployment-invoked'", then send it to a dedicated Slack channel. - * - * @see https://sentry.io/organizations/unly/alerts/next-right-now/new/ - */ -export const ALERT_TYPES = { - VERCEL_DEPLOYMENT_INVOKED: 'vercel-deployment-invoked', - VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT: 'vercel-deployment-trigger-attempt', - VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_FAILED: 'vercel-deployment-trigger-attempt-failed', - VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_SUCCEEDED: 'vercel-deployment-trigger-attempt-succeeded', - VERCEL_DEPLOYMENT_COMPLETED: 'vercel-deployment-completed', -}; - -/** - * Configure Sentry tags related to the current user. - * - * Allows to track all Sentry events related to a particular user. - * The tracking remains anonymous, there are no personal information being tracked, only internal ids. - * - * @param userSession - * @see https://www.npmjs.com/package/@sentry/node - */ -export const configureSentryUserMetadata = (userSession: UserSession): void => { - if (process.env.SENTRY_DSN) { - Sentry.configureScope((scope) => { - scope.setTag('userId', userSession?.id); - scope.setTag('userDeviceId', userSession?.deviceId); - scope.setContext('user', userSession); - }); - } -}; - -/** - * Configure Sentry tags related to the browser metadata. - * - * @param networkSpeed - * @param networkConnectionType - * @param isInIframe - * @param iframeReferrer - */ -export const configureSentryBrowserMetadata = (networkSpeed: ClientNetworkInformationSpeed, networkConnectionType: ClientNetworkConnectionType, isInIframe: boolean, iframeReferrer: string): void => { - if (process.env.SENTRY_DSN) { - Sentry.configureScope((scope) => { - scope.setTag('networkSpeed', networkSpeed); - scope.setTag('networkConnectionType', networkConnectionType); - scope.setTag('iframe', `${isInIframe}`); - scope.setExtra('iframe', isInIframe); - scope.setExtra('iframeReferrer', iframeReferrer); - }); - } -}; - -/** - * Configure Sentry tags for the currently used lang/locale. - * - * @param lang - * @param locale - * @see https://www.npmjs.com/package/@sentry/node - */ -export const configureSentryI18n = (lang: string, locale: string): void => { - if (process.env.SENTRY_DSN) { - Sentry.configureScope((scope) => { // See https://www.npmjs.com/package/@sentry/node - scope.setTag('lang', lang); - scope.setTag('locale', locale); - }); - } -}; - -/** - * Configure the Sentry scope by extracting useful tags and context from the given request. - * - * @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 => { - let parsedBody: GenericObject = {}; - try { - parsedBody = convertRequestBodyToJSObject(req); - } catch (e) { - // eslint-disable-next-line no-console - // console.error(e); - } // Do nothing, as "body" is not necessarily supposed to contain valid stringified JSON - - Sentry.configureScope((scope) => { - 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); - - map(tags, (value: string, tag: string) => { - scope.setTag(tag, value); - }); - - map(contexts, (value: { [key: string]: any; }, context: string) => { - scope.setContext(context, value); - }); - }); -}; - -export default Sentry; diff --git a/src/modules/core/sentry/server.ts b/src/modules/core/sentry/server.ts new file mode 100644 index 00000000..1818b295 --- /dev/null +++ b/src/modules/core/sentry/server.ts @@ -0,0 +1,42 @@ +import { convertRequestBodyToJSObject } from '@/modules/core/api/convertRequestBodyToJSObject'; +import { GenericObject } from '@/modules/core/data/types/GenericObject'; +import * as Sentry from '@sentry/node'; +import map from 'lodash.map'; +import { NextApiRequest } from 'next'; + +/** + * Configure the Sentry scope by extracting useful tags and context from the given request. + * + * @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 => { + let parsedBody: GenericObject = {}; + try { + parsedBody = convertRequestBodyToJSObject(req); + } catch (e) { + // eslint-disable-next-line no-console + // console.error(e); + } // Do nothing, as "body" is not necessarily supposed to contain valid stringified JSON + + Sentry.configureScope((scope) => { + 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); + + map(tags, (value: string, tag: string) => { + scope.setTag(tag, value); + }); + + map(contexts, (value: { [key: string]: any; }, context: string) => { + scope.setContext(context, value); + }); + }); +}; diff --git a/src/modules/core/sentry/universal.ts b/src/modules/core/sentry/universal.ts new file mode 100644 index 00000000..8b9dc832 --- /dev/null +++ b/src/modules/core/sentry/universal.ts @@ -0,0 +1,37 @@ +import { UserSession } from '@/modules/core/userSession/useUserSession'; +import * as Sentry from '@sentry/node'; + +/** + * Configure Sentry tags related to the current user. + * + * Allows to track all Sentry events related to a particular user. + * The tracking remains anonymous, there are no personal information being tracked, only internal ids. + * + * @param userSession + * @see https://www.npmjs.com/package/@sentry/node + */ +export const configureSentryUserMetadata = (userSession: UserSession): void => { + if (process.env.SENTRY_DSN) { + Sentry.configureScope((scope) => { + scope.setTag('userId', userSession?.id); + scope.setTag('userDeviceId', userSession?.deviceId); + scope.setContext('user', userSession); + }); + } +}; + +/** + * Configure Sentry tags for the currently used lang/locale. + * + * @param lang + * @param locale + * @see https://www.npmjs.com/package/@sentry/node + */ +export const configureSentryI18n = (lang: string, locale: string): void => { + if (process.env.SENTRY_DSN) { + Sentry.configureScope((scope) => { // See https://www.npmjs.com/package/@sentry/node + scope.setTag('lang', lang); + scope.setTag('locale', locale); + }); + } +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d6cc5486..d88e99ba 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,7 +6,7 @@ import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; import { sendWebVitals } from '@/modules/core/amplitude/amplitudeBrowserClient'; import '@/modules/core/fontAwesome/fontAwesome'; -import '@/modules/core/sentry/sentry'; +import '@/modules/core/sentry/init'; import { NextWebVitalsMetrics } from '@/modules/core/webVitals/types/NextWebVitalsMetrics'; import { NextWebVitalsMetricsReport } from '@/modules/core/webVitals/types/NextWebVitalsMetricsReport'; import size from 'lodash.size'; diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index a48ef4c2..699dd384 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -1,3 +1,4 @@ +import { FLUSH_TIMEOUT } from '@/modules/core/sentry/config'; import * as Sentry from '@sentry/node'; import { NextPageContext } from 'next'; import NextError, { ErrorProps as NextErrorProps } from 'next/error'; @@ -89,6 +90,8 @@ const ErrorPage = (props: ErrorPageProps): JSX.Element => { * What's the point of getInitialProps when using SSG or hybrid apps? * * @param props + * + * @see https://github.com/vercel/next.js/blob/canary/examples/with-sentry/pages/_error.js */ ErrorPage.getInitialProps = async (props: NextPageContext): Promise => { const { @@ -123,6 +126,9 @@ ErrorPage.getInitialProps = async (props: NextPageContext): Promise if (err) { Sentry.captureException(err); + // It's necessary to flush all events when running on the server, because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + return errorInitialProps; } } else { @@ -149,6 +155,9 @@ ErrorPage.getInitialProps = async (props: NextPageContext): Promise new Error(`_error.js getInitialProps missing data at path: ${asPath}`), ); + // It's necessary to flush all events when running on the server, because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + return errorInitialProps; }; diff --git a/src/pages/api/error.ts b/src/pages/api/error.ts index 11e7ef29..57fda585 100644 --- a/src/pages/api/error.ts +++ b/src/pages/api/error.ts @@ -4,7 +4,9 @@ import { AMPLITUDE_EVENTS, } from '@/modules/core/amplitude/events'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { configureReq } from '@/modules/core/sentry/sentry'; +import { FLUSH_TIMEOUT } from '@/modules/core/sentry/config'; +import Sentry from '@/modules/core/sentry/init'; +import { configureReq } from '@/modules/core/sentry/server'; import { NextApiRequest, NextApiResponse, @@ -38,6 +40,9 @@ export const error = async (req: NextApiRequest, res: NextApiResponse): Promise< Sentry.captureException(e); logger.error(e.message); + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.json({ error: true, message: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? undefined : e.message, diff --git a/src/pages/api/preview.ts b/src/pages/api/preview.ts index da78ac22..da506394 100644 --- a/src/pages/api/preview.ts +++ b/src/pages/api/preview.ts @@ -5,7 +5,9 @@ import { } from '@/modules/core/amplitude/events'; import { filterExternalAbsoluteUrl } from '@/modules/core/js/url'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { configureReq } from '@/modules/core/sentry/sentry'; +import { FLUSH_TIMEOUT } from '@/modules/core/sentry/config'; +import Sentry from '@/modules/core/sentry/init'; +import { configureReq } from '@/modules/core/sentry/server'; import appendQueryParameter from 'append-query'; import { NextApiRequest, @@ -101,12 +103,18 @@ export const preview = async (req: EndpointRequest, res: NextApiResponse): Promi Sentry.captureMessage('Preview mode is not allowed in production', Sentry.Severity.Warning); } + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.writeHead(307, { Location: safeRedirectUrl }); res.end(); } catch (e) { Sentry.captureException(e); logger.error(e.message); + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.json({ error: true, message: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? undefined : e.message, diff --git a/src/pages/api/startVercelDeployment.ts b/src/pages/api/startVercelDeployment.ts index 253ccd83..38f4ddd3 100644 --- a/src/pages/api/startVercelDeployment.ts +++ b/src/pages/api/startVercelDeployment.ts @@ -6,10 +6,9 @@ import { } from '@/modules/core/amplitude/events'; import dispatchWorkflowByPath from '@/modules/core/githubActions/dispatchWorkflowByPath'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { - ALERT_TYPES, - configureReq, -} from '@/modules/core/sentry/sentry'; +import { ALERT_TYPES } from '@/modules/core/sentry/config'; +import Sentry from '@/modules/core/sentry/init'; +import { configureReq } from '@/modules/core/sentry/server'; import size from 'lodash.size'; import { NextApiRequest, diff --git a/src/pages/api/status.ts b/src/pages/api/status.ts index cf6e9ad0..e03126f3 100644 --- a/src/pages/api/status.ts +++ b/src/pages/api/status.ts @@ -4,7 +4,9 @@ import { AMPLITUDE_EVENTS, } from '@/modules/core/amplitude/events'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { configureReq } from '@/modules/core/sentry/sentry'; +import { FLUSH_TIMEOUT } from '@/modules/core/sentry/config'; +import Sentry from '@/modules/core/sentry/init'; +import { configureReq } from '@/modules/core/sentry/server'; import { NextApiRequest, NextApiResponse, @@ -61,6 +63,9 @@ export const status = async (req: NextApiRequest, res: NextApiResponse): Promise Sentry.captureException(e); logger.error(e.message); + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.json({ error: true, message: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? undefined : e.message, diff --git a/src/pages/api/webhooks/deploymentCompleted.ts b/src/pages/api/webhooks/deploymentCompleted.ts index bb4e0910..f5891cd0 100644 --- a/src/pages/api/webhooks/deploymentCompleted.ts +++ b/src/pages/api/webhooks/deploymentCompleted.ts @@ -5,10 +5,12 @@ import { } from '@/modules/core/amplitude/events'; import { convertRequestBodyToJSObject } from '@/modules/core/api/convertRequestBodyToJSObject'; import { createLogger } from '@/modules/core/logging/logger'; -import Sentry, { +import { ALERT_TYPES, - configureReq, -} from '@/modules/core/sentry/sentry'; + FLUSH_TIMEOUT, +} from '@/modules/core/sentry/config'; +import Sentry from '@/modules/core/sentry/init'; +import { configureReq } from '@/modules/core/sentry/server'; import { NextApiRequest, NextApiResponse, @@ -118,12 +120,18 @@ export const deploymentCompleted = async (req: EndpointRequest, res: NextApiResp }); }); + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.status(200); res.end(); } catch (e) { Sentry.captureException(e); logger.error(e.message); + // It's necessary to flush all events because Vercel runs on AWS Lambda, see https://vercel.com/docs/platform/limits#streaming-responses + await Sentry.flush(FLUSH_TIMEOUT); + res.status(500); res.end(); }