diff --git a/dotcom-rendering/src/client/sentryLoader/loadSentry.ts b/dotcom-rendering/src/client/sentryLoader/loadSentry.ts index e4b29fd00e..dd6e9635d2 100644 --- a/dotcom-rendering/src/client/sentryLoader/loadSentry.ts +++ b/dotcom-rendering/src/client/sentryLoader/loadSentry.ts @@ -1,93 +1,132 @@ import { isAdBlockInUse } from '@guardian/commercial'; import { log, startPerformanceMeasure } from '@guardian/libs'; import '../webpackPublicPath'; +import type { ReportError } from '../../types/sentry'; -/** Sentry errors are only sent to the console */ -const stubSentry = (): void => { - window.guardian.modules.sentry.reportError = (error) => { - console.error(error); - }; -}; +type ReportErrorError = Parameters[0]; +type ReportErrorFeature = Parameters[1]; +type ReportErrorTags = Parameters[2]; -/** - * Set up error handlers to inject and call Sentry. - * If no error happen, Sentry is not loaded. - */ -const loadSentryOnError = (): void => { - try { - // Downloading and initialising Sentry is asynchronous so we need a way - // to ensure injection only happens once and to capture any other errors that - // might happen while this script is loading - let injected = false; - const queue: Error[] = []; +type ErrorQueue = Array<{ + error: ReportErrorError; + feature: ReportErrorFeature; + tags?: ReportErrorTags; +}>; - // Function that gets called when an error happens before Sentry is ready - const injectSentry = async ( - error: Error | undefined, - feature: string, - ) => { - const { endPerformanceMeasure } = startPerformanceMeasure( - 'dotcom', - 'sentryLoader', - 'inject', - ); - // Remember this error for later - if (error) queue.push(error); +const loadSentryCreator = () => { + /** + * Downloading and initialising Sentry is asynchronous so we need a way + * to ensure initialisation only happens once and to queue and report any + * error that might happen while this script is loading + */ + let initialised = false; + const errorQueue: ErrorQueue = []; - // Only inject once - if (injected) { - return; - } - injected = true; + /** + * Function that gets called when an error happens before Sentry is ready + */ + const loadSentry = async ( + error: ReportErrorError | undefined, + feature: ReportErrorFeature = 'unknown', + tags?: ReportErrorTags, + ) => { + const { endPerformanceMeasure } = startPerformanceMeasure( + 'dotcom', + 'sentryLoader', + 'initialise', + ); + /** + * Queue this error for later + */ + if (error) errorQueue.push({ error, feature, tags }); - // Make this call blocking. We are queing errors while we wait for this code to run - // so we won't miss any and by waiting here we ensure we will never make calls we - // expect to be blocked - // Ad blocker detection can be expensive so it is checked here rather than in init - // to avoid blocking of the init flow - const adBlockInUse: boolean = await isAdBlockInUse(); - if (adBlockInUse) { - // Ad Blockers prevent calls to Sentry from working so don't try to load the lib - return; - } + /** + * Only initialise once + */ + if (initialised) { + return; + } + initialised = true; - // Load sentry.ts - const { reportError } = await import( - /* webpackChunkName: "lazy" */ - /* webpackChunkName: "sentry" */ './sentry' - ); + /** + * Make this call blocking. We are queing errors while we wait for this code to run + * so we won't miss any and by waiting here we ensure we will never make calls we + * expect to be blocked + * Ad blocker detection can be expensive so it is checked here rather than in init + * to avoid blocking of the init flow + */ + const adBlockInUse: boolean = await isAdBlockInUse(); + if (adBlockInUse) { + /** + * Ad Blockers prevent calls to Sentry from working so don't try to load the lib + */ + return; + } - // Sentry takes over control of the window.onerror and - // window.onunhandledrejection listeners but we need to - // manually redefine our own custom error reporting function - window.guardian.modules.sentry.reportError = reportError; + /** + * Dynamically load sentry.ts + */ + const { reportError } = await import( + /* webpackChunkName: "lazy" */ + /* webpackChunkName: "sentry" */ './sentry' + ); - // Now that we have the real reportError function available, - // send any queued errors - while (queue.length) { - const queuedError = queue.shift(); - if (queuedError) reportError(queuedError, feature); + /** + * Replace the lazy loader stub with our custom error reporting function + */ + window.guardian.modules.sentry.reportError = reportError; + + /** + * Now that we have the real reportError function available, + * report any queued errors + */ + while (errorQueue.length) { + const queuedError = errorQueue.shift(); + if (queuedError) { + reportError( + queuedError.error, + queuedError.feature, + queuedError.tags, + ); } - log('dotcom', `Injected Sentry in ${endPerformanceMeasure()}ms`); - }; + } + log('dotcom', `Initialise Sentry in ${endPerformanceMeasure()}ms`); + }; + return loadSentry; +}; + +/** + * Set up error handlers to initialise and call Sentry when an error occurs. + * If no error happens, Sentry is not loaded. + */ +const loadSentryOnError = (): void => { + try { + const loadSentry = loadSentryCreator(); - // This is how we lazy load Sentry. We setup custom functions and - // listeners to inject Sentry when an error happens + /** + * This is how we lazy load Sentry. We setup custom functions and + * listeners to initialise Sentry when an error happens + * + * Sentry will replace onerror and onunhandledrejection listeners + * with its own handlers once initialised + * + * reportError is replaced by loadSentry + */ window.onerror = (message, url, line, column, error) => - injectSentry(error, 'unknown'); + loadSentry(error); window.onunhandledrejection = ( event: undefined | { reason?: unknown }, - ) => - event?.reason instanceof Error && - injectSentry(event.reason, 'unknown'); - window.guardian.modules.sentry.reportError = (error, feature) => { - injectSentry(error, feature).catch((e) => - console.error(`injectSentry - error: ${String(e)}`), + ) => event?.reason instanceof Error && loadSentry(event.reason); + window.guardian.modules.sentry.reportError = (error, feature, tags) => { + loadSentry(error, feature, tags).catch((e) => + // eslint-disable-next-line no-console -- fallback to console.error + console.error(`loadSentryOnError error: ${String(e)}`), ); }; - } catch { - // We failed to setup Sentry :( + } catch (e) { + // eslint-disable-next-line no-console -- fallback to console.error + console.error(`loadSentryOnError error: ${String(e)}`); } }; -export { loadSentryOnError, stubSentry }; +export { loadSentryOnError }; diff --git a/dotcom-rendering/src/client/sentryLoader/sentry.ts b/dotcom-rendering/src/client/sentryLoader/sentry.ts index fe16034cb9..42a58f2c0d 100644 --- a/dotcom-rendering/src/client/sentryLoader/sentry.ts +++ b/dotcom-rendering/src/client/sentryLoader/sentry.ts @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import { CaptureConsole } from '@sentry/integrations'; import { BUILD_VARIANT, dcrJavascriptBundle } from '../../../webpack/bundles'; +import type { ReportError } from '../../types/sentry'; const allowUrls: BrowserOptions['allowUrls'] = [ /webpack-internal/, @@ -55,13 +56,7 @@ if ( Sentry.setTag('dcr.bundle', dcrJavascriptBundle('Variant')); } -export const reportError = ( - error: Error, - feature: string, - tags?: { - [key: string]: string; - }, -): void => { +export const reportError: ReportError = (error, feature, tags) => { Sentry.withScope(() => { Sentry.setTag('feature', feature); if (tags) { diff --git a/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts b/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts index 69e629e6ea..e6756297fe 100644 --- a/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts +++ b/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts @@ -1,5 +1,5 @@ import { BUILD_VARIANT, dcrJavascriptBundle } from '../../../webpack/bundles'; -import { loadSentryOnError, stubSentry } from './loadSentry'; +import { loadSentryOnError } from './loadSentry'; type IsSentryEnabled = { enableSentryReporting: boolean; @@ -32,6 +32,14 @@ const isSentryEnabled = ({ return true; }; +/** When stubbed errors are only sent to the console */ +const stubSentry = (): void => { + window.guardian.modules.sentry.reportError = (error, feature, tags) => { + // eslint-disable-next-line no-console -- fallback to console.error + console.error(error, feature, tags); + }; +}; + export const sentryLoader = (): Promise => { const { switches, isDev, tests } = window.guardian.config; const enableSentryReporting = !!switches.enableSentryReporting; diff --git a/dotcom-rendering/src/model/guardian.ts b/dotcom-rendering/src/model/guardian.ts index 76d49d3376..71748742d6 100644 --- a/dotcom-rendering/src/model/guardian.ts +++ b/dotcom-rendering/src/model/guardian.ts @@ -1,5 +1,6 @@ import type { EditionId } from '../lib/edition'; import type { ConfigType, ServerSideTests, Switches } from '../types/config'; +import type { ReportError } from '../types/sentry'; export interface Guardian { polyfilled: boolean; @@ -47,7 +48,7 @@ export interface Guardian { }; modules: { sentry: { - reportError: (error: Error, feature: string) => void; + reportError: ReportError; }; }; adBlockers: unknown; diff --git a/dotcom-rendering/src/types/sentry.ts b/dotcom-rendering/src/types/sentry.ts new file mode 100644 index 0000000000..7a7e3f358c --- /dev/null +++ b/dotcom-rendering/src/types/sentry.ts @@ -0,0 +1,7 @@ +export type ReportError = ( + error: Error, + feature: string, + tags?: { + [key: string]: string; + }, +) => void;