Skip to content

Commit

Permalink
Merge pull request #12398 from guardian/ds/add-tags-to-injectSentry
Browse files Browse the repository at this point in the history
Update reportError Function to support Custom Tags in window Object
  • Loading branch information
dskamiotis committed Sep 18, 2024
2 parents 165b5c5 + 888ed8a commit 550c3e6
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 81 deletions.
183 changes: 111 additions & 72 deletions dotcom-rendering/src/client/sentryLoader/loadSentry.ts
Original file line number Diff line number Diff line change
@@ -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<ReportError>[0];
type ReportErrorFeature = Parameters<ReportError>[1];
type ReportErrorTags = Parameters<ReportError>[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 };
9 changes: 2 additions & 7 deletions dotcom-rendering/src/client/sentryLoader/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/,
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion dotcom-rendering/src/client/sentryLoader/sentryLoader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD_VARIANT, dcrJavascriptBundle } from '../../../webpack/bundles';
import { loadSentryOnError, stubSentry } from './loadSentry';
import { loadSentryOnError } from './loadSentry';

type IsSentryEnabled = {
enableSentryReporting: boolean;
Expand Down Expand Up @@ -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<void> => {
const { switches, isDev, tests } = window.guardian.config;
const enableSentryReporting = !!switches.enableSentryReporting;
Expand Down
3 changes: 2 additions & 1 deletion dotcom-rendering/src/model/guardian.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -47,7 +48,7 @@ export interface Guardian {
};
modules: {
sentry: {
reportError: (error: Error, feature: string) => void;
reportError: ReportError;
};
};
adBlockers: unknown;
Expand Down
7 changes: 7 additions & 0 deletions dotcom-rendering/src/types/sentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ReportError = (
error: Error,
feature: string,
tags?: {
[key: string]: string;
},
) => void;

0 comments on commit 550c3e6

Please sign in to comment.