diff --git a/.changeset/famous-poems-count.md b/.changeset/famous-poems-count.md new file mode 100644 index 000000000..39975ea86 --- /dev/null +++ b/.changeset/famous-poems-count.md @@ -0,0 +1,5 @@ +--- +'@guardian/commercial': minor +--- + +Refactor boot logic for readability diff --git a/src/commercial.ts b/src/commercial.ts index 9ddec9a35..2aecad423 100644 --- a/src/commercial.ts +++ b/src/commercial.ts @@ -1,6 +1,16 @@ +import type { ConsentState } from '@guardian/libs'; import { getConsentFor, onConsent } from '@guardian/libs'; import { commercialFeatures } from './lib/commercial-features'; +const shouldBootConsentless = (consentState: ConsentState) => { + return ( + window.guardian.config.switches.optOutAdvertising && + consentState.tcfv2 && + !getConsentFor('googletag', consentState) && + !commercialFeatures.adFree + ); +}; + /** * Choose whether to launch Googletag or Opt Out tag (ootag) based on consent state */ @@ -11,20 +21,20 @@ void (async () => { // - in TCF region // - no consent for Googletag // - the user is not a subscriber - if ( - window.guardian.config.switches.optOutAdvertising && - consentState.tcfv2 && - !getConsentFor('googletag', consentState) && - !commercialFeatures.adFree - ) { + if (shouldBootConsentless(consentState)) { void import( - /* webpackChunkName: "consentless" */ - './init/consentless' + /* webpackChunkName: "consentless-advertising" */ + './init/consentless-advertising' ).then(({ bootConsentless }) => bootConsentless(consentState)); + } else if (commercialFeatures.adFree) { + void import( + /* webpackChunkName: "ad-free" */ + './init/ad-free' + ).then(({ bootCommercialWhenReady }) => bootCommercialWhenReady()); } else { void import( - /* webpackChunkName: "consented" */ - './init/consented' + /* webpackChunkName: "consented-advertising" */ + './init/consented-advertising' ).then(({ bootCommercialWhenReady }) => bootCommercialWhenReady()); } })(); diff --git a/src/init/ad-free.ts b/src/init/ad-free.ts new file mode 100644 index 000000000..bc293fac8 --- /dev/null +++ b/src/init/ad-free.ts @@ -0,0 +1,29 @@ +import { bootCommercial } from '../lib/commercial-boot-utils'; +import { adFreeSlotRemove } from './consented/ad-free-slot-remove'; +import { init as initComscore } from './consented/comscore'; +import { init as initIpsosMori } from './consented/ipsos-mori'; +import { removeDisabledSlots as closeDisabledSlots } from './consented/remove-slots'; +import { initTeadsCookieless } from './consented/teads-cookieless'; +import { init as initTrackGpcSignal } from './consented/track-gpc-signal'; +import { init as initTrackScrollDepth } from './consented/track-scroll-depth'; + +// modules not related to ad loading +const commercialModules = [ + adFreeSlotRemove, + closeDisabledSlots, + initComscore, + initIpsosMori, + initTeadsCookieless, + initTrackScrollDepth, + initTrackGpcSignal, +]; + +const bootCommercialWhenReady = () => { + if (!!window.guardian.mustardCut || !!window.guardian.polyfilled) { + void bootCommercial(commercialModules); + } else { + window.guardian.queue.push(() => bootCommercial(commercialModules)); + } +}; + +export { bootCommercialWhenReady }; diff --git a/src/init/consented-advertising.ts b/src/init/consented-advertising.ts new file mode 100644 index 000000000..61f01e186 --- /dev/null +++ b/src/init/consented-advertising.ts @@ -0,0 +1,56 @@ +import { init as prepareAdVerification } from '../lib/ad-verification/prepare-ad-verification'; +import { bootCommercial } from '../lib/commercial-boot-utils'; +import { adFreeSlotRemove } from './consented/ad-free-slot-remove'; +import { init as initComscore } from './consented/comscore'; +import { initDfpListeners } from './consented/dfp-listeners'; +import { initDynamicAdSlots } from './consented/dynamic-ad-slots'; +import { initFillSlotListener } from './consented/fill-slot-listener'; +import { init as initIpsosMori } from './consented/ipsos-mori'; +import { init as initMessenger } from './consented/messenger'; +import { init as prepareA9 } from './consented/prepare-a9'; +import { init as prepareGoogletag } from './consented/prepare-googletag'; +import { initPermutive } from './consented/prepare-permutive'; +import { init as preparePrebid } from './consented/prepare-prebid'; +import { removeDisabledSlots as closeDisabledSlots } from './consented/remove-slots'; +import { initTeadsCookieless } from './consented/teads-cookieless'; +import { init as initThirdPartyTags } from './consented/third-party-tags'; +import { init as initTrackGpcSignal } from './consented/track-gpc-signal'; +import { init as initTrackScrollDepth } from './consented/track-scroll-depth'; +import { reloadPageOnConsentChange } from './shared/reload-page-on-consent-change'; +import { init as setAdTestCookie } from './shared/set-adtest-cookie'; +import { init as setAdTestInLabelsCookie } from './shared/set-adtest-in-labels-cookie'; + +// all modules needed for commercial code and ads to run +const commercialModules = [ + adFreeSlotRemove, + closeDisabledSlots, + initComscore, + initIpsosMori, + initTeadsCookieless, + initTrackScrollDepth, + initTrackGpcSignal, + initMessenger, + setAdTestCookie, + setAdTestInLabelsCookie, + reloadPageOnConsentChange, + preparePrebid, + initDfpListeners, + // Permutive init code must run before google tag enableServices() + // The permutive lib however is loaded async with the third party tags + () => initPermutive().then(prepareGoogletag), + initDynamicAdSlots, + prepareA9, + initFillSlotListener, + prepareAdVerification, + initThirdPartyTags, +]; + +const bootCommercialWhenReady = () => { + if (!!window.guardian.mustardCut || !!window.guardian.polyfilled) { + void bootCommercial(commercialModules); + } else { + window.guardian.queue.push(() => bootCommercial(commercialModules)); + } +}; + +export { bootCommercialWhenReady }; diff --git a/src/init/consented.ts b/src/init/consented.ts deleted file mode 100644 index 090b4b8e6..000000000 --- a/src/init/consented.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { log } from '@guardian/libs'; -import { init as prepareAdVerification } from '../lib/ad-verification/prepare-ad-verification'; -import { commercialFeatures } from '../lib/commercial-features'; -import { adSlotIdPrefix } from '../lib/dfp/dfp-env-globals'; -import { reportError } from '../lib/error/report-error'; -import { catchErrorsAndReport } from '../lib/error/robust'; -import { EventTimer } from '../lib/event-timer'; -import { adFreeSlotRemove } from './consented/ad-free-slot-remove'; -import { init as initComscore } from './consented/comscore'; -import { initDfpListeners } from './consented/dfp-listeners'; -import { initDynamicAdSlots } from './consented/dynamic-ad-slots'; -import { initFillSlotListener } from './consented/fill-slot-listener'; -import { init as initIpsosMori } from './consented/ipsos-mori'; -import { init as initMessenger } from './consented/messenger'; -import { init as prepareA9 } from './consented/prepare-a9'; -import { init as prepareGoogletag } from './consented/prepare-googletag'; -import { initPermutive } from './consented/prepare-permutive'; -import { init as preparePrebid } from './consented/prepare-prebid'; -import { removeDisabledSlots as closeDisabledSlots } from './consented/remove-slots'; -import { initTeadsCookieless } from './consented/teads-cookieless'; -import { init as initThirdPartyTags } from './consented/third-party-tags'; -import { init as initTrackGpcSignal } from './consented/track-gpc-signal'; -import { init as initTrackScrollDepth } from './consented/track-scroll-depth'; -import { reloadPageOnConsentChange } from './shared/reload-page-on-consent-change'; -import { init as setAdTestCookie } from './shared/set-adtest-cookie'; -import { init as setAdTestInLabelsCookie } from './shared/set-adtest-in-labels-cookie'; - -type Modules = Array<[`${string}-${string}`, () => Promise]>; - -const tags: Record = { - bundle: 'standalone', -}; -// modules necessary to load the first ads on the page -const commercialBaseModules: Modules = []; - -// remaining modules not necessary to load an ad -const commercialExtraModules: Modules = [ - ['cm-adFreeSlotRemoveFronts', adFreeSlotRemove], - ['cm-closeDisabledSlots', closeDisabledSlots], - ['cm-comscore', initComscore], - ['cm-ipsosmori', initIpsosMori], - ['cm-teadsCookieless', initTeadsCookieless], - ['cm-trackScrollDepth', initTrackScrollDepth], - ['cm-trackGpcSignal', initTrackGpcSignal], -]; - -if (!commercialFeatures.adFree) { - commercialBaseModules.push( - ['cm-messenger', initMessenger], - ['cm-setAdTestCookie', setAdTestCookie], - ['cm-setAdTestInLabelsCookie', setAdTestInLabelsCookie], - ['cm-reloadPageOnConsentChange', reloadPageOnConsentChange], - ['cm-prepare-prebid', preparePrebid], - // Permutive init code must run before google tag enableServices() - // The permutive lib however is loaded async with the third party tags - ['cm-dfp-listeners', initDfpListeners], - ['cm-prepare-googletag', () => initPermutive().then(prepareGoogletag)], - ['cm-dynamic-a-slots', initDynamicAdSlots], - ['cm-prepare-a9', prepareA9], - ['cm-prepare-fill-slot-listener', initFillSlotListener], - ); - commercialExtraModules.push( - ['cm-prepare-adverification', prepareAdVerification], - ['cm-thirdPartyTags', initThirdPartyTags], - ); -} - -const loadModules = (modules: Modules, eventName: string) => { - const modulePromises: Array> = []; - - modules.forEach((module) => { - const [moduleName, moduleInit] = module; - - catchErrorsAndReport( - [ - [ - moduleName, - function pushAfterComplete(): void { - const result = moduleInit(); - modulePromises.push(result); - }, - ], - ], - tags, - ); - }); - - return Promise.allSettled(modulePromises).then(() => { - EventTimer.get().mark(eventName); - }); -}; - -const recordCommercialMetrics = () => { - const eventTimer = EventTimer.get(); - eventTimer.mark('commercialBootEnd'); - eventTimer.mark('commercialModulesLoaded'); - // record the number of ad slots on the page - const adSlotsTotal = document.querySelectorAll( - `[id^="${adSlotIdPrefix}"]`, - ).length; - eventTimer.setProperty('adSlotsTotal', adSlotsTotal); - - // and the number of inline ad slots - const adSlotsInline = document.querySelectorAll( - `[id^="${adSlotIdPrefix}inline"]`, - ).length; - eventTimer.setProperty('adSlotsInline', adSlotsInline); -}; - -const bootCommercial = async (): Promise => { - log('commercial', '📦 standalone.commercial.ts', __webpack_public_path__); - if (process.env.COMMIT_SHA) { - log( - 'commercial', - `@guardian/commercial commit https://github.com/guardian/commercial/blob/${process.env.COMMIT_SHA}`, - ); - } - - // Init Commercial event timers - EventTimer.init(); - - catchErrorsAndReport( - [ - [ - 'ga-user-timing-commercial-start', - function runTrackPerformance() { - EventTimer.get().mark('commercialStart'); - EventTimer.get().mark('commercialBootStart'); - }, - ], - ], - tags, - ); - - // Stub the command queue - // @ts-expect-error -- it’s a stub, not the whole Googletag object - window.googletag = { - cmd: [], - }; - - try { - const allModules: Array> = [ - [commercialBaseModules, 'commercialBaseModulesLoaded'], - [commercialExtraModules, 'commercialExtraModulesLoaded'], - ]; - const promises = allModules.map((args) => loadModules(...args)); - - await Promise.all(promises).then(recordCommercialMetrics); - } catch (error) { - // report async errors in bootCommercial to Sentry with the commercial feature tag - reportError(error, 'commercial', tags); - } -}; - -const bootCommercialWhenReady = () => { - if (!!window.guardian.mustardCut || !!window.guardian.polyfilled) { - void bootCommercial(); - } else { - window.guardian.queue.push(bootCommercial); - } -}; - -export { bootCommercialWhenReady }; diff --git a/src/init/consentless.ts b/src/init/consentless-advertising.ts similarity index 100% rename from src/init/consentless.ts rename to src/init/consentless-advertising.ts diff --git a/src/lib/commercial-boot-utils.ts b/src/lib/commercial-boot-utils.ts new file mode 100644 index 000000000..709bc4151 --- /dev/null +++ b/src/lib/commercial-boot-utils.ts @@ -0,0 +1,59 @@ +import { log } from '@guardian/libs'; +import { adSlotIdPrefix } from './dfp/dfp-env-globals'; +import { reportError } from './error/report-error'; +import { EventTimer } from './event-timer'; + +const tags: Record = { + bundle: 'standalone', +}; + +const recordCommercialMetrics = () => { + const eventTimer = EventTimer.get(); + eventTimer.mark('commercialBootEnd'); + eventTimer.mark('commercialModulesLoaded'); + // record the number of ad slots on the page + const adSlotsTotal = document.querySelectorAll( + `[id^="${adSlotIdPrefix}"]`, + ).length; + eventTimer.setProperty('adSlotsTotal', adSlotsTotal); + + // and the number of inline ad slots + const adSlotsInline = document.querySelectorAll( + `[id^="${adSlotIdPrefix}inline"]`, + ).length; + eventTimer.setProperty('adSlotsInline', adSlotsInline); +}; + +const bootCommercial = async ( + modules: Array<() => Promise>, +): Promise => { + log('commercial', '📦 standalone.commercial.ts', __webpack_public_path__); + if (process.env.COMMIT_SHA) { + log( + 'commercial', + `@guardian/commercial commit https://github.com/guardian/commercial/blob/${process.env.COMMIT_SHA}`, + ); + } + + // Init Commercial event timers + EventTimer.init(); + EventTimer.get().mark('commercialStart'); + EventTimer.get().mark('commercialBootStart'); + + // Stub the command queue + // @ts-expect-error -- it’s a stub, not the whole Googletag object + window.googletag = { + cmd: [], + }; + + try { + return Promise.allSettled(modules.map((module) => module())).then( + recordCommercialMetrics, + ); + } catch (error) { + // report async errors in bootCommercial to Sentry with the commercial feature tag + reportError(error, 'commercial', tags); + } +}; + +export { bootCommercial }; diff --git a/src/lib/error/robust.spec.ts b/src/lib/error/robust.spec.ts deleted file mode 100644 index 7b826ef87..000000000 --- a/src/lib/error/robust.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { reportError } from './report-error'; -import { catchErrorsAndReport } from './robust'; -import type { Modules } from './robust'; - -jest.mock('./report-error', () => ({ - reportError: jest.fn(), -})); - -beforeEach(() => { - jest.spyOn(global.console, 'warn'); -}); - -afterEach(() => { - jest.spyOn(global.console, 'warn').mockRestore(); - jest.resetAllMocks(); -}); - -describe('robust', () => { - const ERROR = new Error('Deliberate test error'); - const tags = { tag: 'test' }; - - const throwError = () => { - throw ERROR; - }; - - test('catchErrorsAndReport with no errors', () => { - const runner = jest.fn(); - - const modules = [ - ['test-1', runner], - ['test-2', runner], - ['test-3', runner], - ] as Modules; - - catchErrorsAndReport(modules); - expect(runner).toHaveBeenCalledTimes(modules.length); - expect(reportError).not.toHaveBeenCalled(); - }); - - test('catchErrorsAndReport with one error', () => { - const runner = jest.fn(); - - const modules = [ - ['test-1', runner], - ['test-2', throwError], - ['test-3', runner], - ] as Modules; - - catchErrorsAndReport(modules, tags); - expect(runner).toHaveBeenCalledTimes(2); - expect(reportError).toHaveBeenCalledTimes(1); - expect(window.console.warn).toHaveBeenCalledTimes(1); - expect(reportError).toHaveBeenCalledWith(ERROR, 'commercial', { - tag: 'test', - module: 'test-2', - }); - }); -}); diff --git a/src/lib/error/robust.ts b/src/lib/error/robust.ts deleted file mode 100644 index f0cd7f708..000000000 --- a/src/lib/error/robust.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Swallows and reports exceptions. Designed to wrap around modules at the "bootstrap" level. - */ -import { reportError } from './report-error'; - -type ModuleFunction = () => void; -type Module = [string, ModuleFunction]; -type Modules = Module[]; - -const catchErrorsAndReport = ( - modules: Modules, - tags?: Record, -): void => { - modules.forEach(([name, fn]) => { - let error: Error | undefined; - - try { - fn(); - } catch (e) { - error = e instanceof Error ? e : new Error(String(e)); - } - - if (error) { - window.console.warn('Caught error.', error.stack); - reportError(error, 'commercial', { ...tags, module: name }); - } - }); -}; - -export { catchErrorsAndReport }; -export type { Modules };