From a1111c4d6a305ed6b4dd5bb7c161575806ff5736 Mon Sep 17 00:00:00 2001 From: Jake Lee Kennedy Date: Fri, 10 May 2024 16:14:11 +0100 Subject: [PATCH] Separate adblock detection logic from adblock ask and record in `Metrics` (#11400) --- .../src/components/Metrics.importable.tsx | 10 ++ dotcom-rendering/src/lib/detect-adblock.ts | 69 +++++++++++ dotcom-rendering/src/lib/useAdBlockAsk.ts | 108 +++--------------- dotcom-rendering/src/lib/useDetectAdBlock.ts | 32 ++++++ 4 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 dotcom-rendering/src/lib/detect-adblock.ts create mode 100644 dotcom-rendering/src/lib/useDetectAdBlock.ts diff --git a/dotcom-rendering/src/components/Metrics.importable.tsx b/dotcom-rendering/src/components/Metrics.importable.tsx index ec22befac26..000fd82ba41 100644 --- a/dotcom-rendering/src/components/Metrics.importable.tsx +++ b/dotcom-rendering/src/components/Metrics.importable.tsx @@ -1,6 +1,7 @@ import type { ABTest, ABTestAPI } from '@guardian/ab-core'; import { bypassCommercialMetricsSampling, + EventTimer, initCommercialMetrics, } from '@guardian/commercial'; import { @@ -13,6 +14,7 @@ import { adBlockAsk } from '../experiments/tests/ad-block-ask'; import { integrateIma } from '../experiments/tests/integrate-ima'; import { useAB } from '../lib/useAB'; import { useAdBlockInUse } from '../lib/useAdBlockInUse'; +import { useDetectAdBlock } from '../lib/useDetectAdBlock'; import { useOnce } from '../lib/useOnce'; import { usePageViewId } from '../lib/usePageViewId'; import type { ServerSideTests } from '../types/config'; @@ -89,6 +91,7 @@ const useDev = () => { export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { const abTestApi = useAB()?.api; const adBlockerInUse = useAdBlockInUse(); + const detectedAdBlocker = useDetectAdBlock(); const { renderingTarget } = useConfig(); const browserId = useBrowserId(); @@ -151,6 +154,12 @@ export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { const bypassSampling = shouldBypassSampling(abTestApi); + // This is a new detection method we are trying, so we want to record it separately to `adBlockerInUse` + EventTimer.get().setProperty( + 'detectedAdBlocker', + detectedAdBlocker, + ); + initCommercialMetrics({ pageViewId, browserId, @@ -171,6 +180,7 @@ export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { [ abTestApi, adBlockerInUse, + detectedAdBlocker, browserId, commercialMetricsEnabled, isDev, diff --git a/dotcom-rendering/src/lib/detect-adblock.ts b/dotcom-rendering/src/lib/detect-adblock.ts new file mode 100644 index 00000000000..24ff18a322a --- /dev/null +++ b/dotcom-rendering/src/lib/detect-adblock.ts @@ -0,0 +1,69 @@ +// cache the promise so we only make the requests once +let detectByRequests: Promise | undefined; + +/** + * Make a HEAD request to a URL that is typically blocked by ad-blockers + */ +const requestDoubleclick = async (timeoutMs: number) => { + try { + const response = await fetch('https://www3.doubleclick.net', { + method: 'HEAD', + mode: 'no-cors', + cache: 'no-store', + signal: AbortSignal.timeout(timeoutMs), + }); + + // A redirect is another clue we may be being ad-blocked + if (response.redirected) { + return false; + } + + return true; + } catch (err) { + return false; + } +}; + +/** + * Make a HEAD request to a URL that should succeed, even when using an + * ad-blocker + */ +const requestGuardian = async (timeoutMs: number) => { + try { + await fetch('https://www.theguardian.com', { + method: 'HEAD', + mode: 'no-cors', + cache: 'no-store', + signal: AbortSignal.timeout(timeoutMs), + }); + return true; + } catch (err) { + return false; + } +}; + +/** + * Attempt to detect presence of an ad-blocker + * + * This implementation of this is likely to be tweaked before launching the test + * proper + */ +export const detectByRequestsOnce = async (): Promise => { + if (detectByRequests) { + return detectByRequests; + } + detectByRequests = Promise.all([ + requestDoubleclick(1000), + /** + * We set this request with a much smaller timeout than the one we + * expect to be ad-blocked. This should reduce the chance that request + * fails and this one succeeds due to poor network connectivity + */ + requestGuardian(250), + ]).then( + ([doubleclickSuccess, guardianSuccess]) => + !doubleclickSuccess && guardianSuccess, + ); + + return detectByRequests; +}; diff --git a/dotcom-rendering/src/lib/useAdBlockAsk.ts b/dotcom-rendering/src/lib/useAdBlockAsk.ts index b9294891676..59cdee5b28e 100644 --- a/dotcom-rendering/src/lib/useAdBlockAsk.ts +++ b/dotcom-rendering/src/lib/useAdBlockAsk.ts @@ -3,6 +3,7 @@ import { getConsentFor, onConsentChange } from '@guardian/libs'; import { useEffect, useState } from 'react'; import { adFreeDataIsPresent } from '../client/userFeatures/user-features-lib'; import { useAB } from './useAB'; +import { useDetectAdBlock } from './useDetectAdBlock'; const useIsInAdBlockAskVariant = (): boolean => { const abTestAPI = useAB()?.api; @@ -10,67 +11,6 @@ const useIsInAdBlockAskVariant = (): boolean => { return isInVariant; }; -/** - * Make a HEAD request to a URL that is typically blocked by ad-blockers - */ -const requestDoubleclick = async (timeoutMs: number) => { - try { - const response = await fetch('https://www3.doubleclick.net', { - method: 'HEAD', - mode: 'no-cors', - cache: 'no-store', - signal: AbortSignal.timeout(timeoutMs), - }); - - // A redirect is another clue we may be being ad-blocked - if (response.redirected) { - return false; - } - - return true; - } catch (err) { - return false; - } -}; - -/** - * Make a HEAD request to a URL that should succeed, even when using an - * ad-blocker - */ -const requestGuardian = async (timeoutMs: number) => { - try { - await fetch('https://www.theguardian.com', { - method: 'HEAD', - mode: 'no-cors', - cache: 'no-store', - signal: AbortSignal.timeout(timeoutMs), - }); - return true; - } catch (err) { - return false; - } -}; - -/** - * Attempt to detect presence of an ad-blocker - * - * This implementation of this is likely to be tweaked before launching the test - * proper - */ -const detectByRequests = async () => { - const [doubleclickSuccess, guardianSuccess] = await Promise.all([ - requestDoubleclick(1000), - /** - * We set this request with a much smaller timeout than the one we - * expect to be ad-blocked. This should reduce the chance that request - * fails and this one succeeds due to poor network connectivity - */ - requestGuardian(250), - ]); - - return !doubleclickSuccess && guardianSuccess; -}; - export const useAdblockAsk = ({ slotId, shouldHideReaderRevenue, @@ -81,7 +21,7 @@ export const useAdblockAsk = ({ isPaidContent: boolean; }): boolean => { const isInVariant = useIsInAdBlockAskVariant(); - const [adBlockerDetected, setAdBlockerDetected] = useState(false); + const adBlockerDetected = useDetectAdBlock(); const [isAdFree, setIsAdFree] = useState(false); const [hasConsentForGoogletag, setHasConsentForGoogletag] = useState(false); @@ -107,36 +47,22 @@ export const useAdblockAsk = ({ }, []); useEffect(() => { - const makeRequest = async () => { - // Only perform the detection check in the variant of the AB test and if we haven't already detected an ad-blocker - if (isInVariant && !adBlockerDetected) { - EventTimer.get().setProperty('detectedAdBlocker', false); - - if (await detectByRequests()) { - setAdBlockerDetected(true); - - // Is the reader/content eligible for displaying such a message - if (canDisplayAdBlockAsk) { - // Some ad-blockers will remove slots from the DOM, while others don't - // This clean-up ensures that any space we've reserved for an ad is removed, - // in order to properly layout the ask. - document - .getElementById(slotId) - ?.closest('.ad-slot-container') - ?.remove(); - EventTimer.get().setProperty( - 'didDisplayAdBlockAsk', - true, - ); - } - - // Record ad block detection in commercial metrics - EventTimer.get().setProperty('detectedAdBlocker', true); - } + // Only perform the detection check in the variant of the AB test, if we haven't already detected an ad-blocker and the reader/content is eligible for displaying such a message + if (isInVariant) { + EventTimer.get().setProperty('didDisplayAdBlockAsk', false); + + if (adBlockerDetected && canDisplayAdBlockAsk) { + // Some ad-blockers will remove slots from the DOM, while others don't + // This clean-up ensures that any space we've reserved for an ad is removed, + // in order to properly layout the ask. + document + .getElementById(slotId) + ?.closest('.ad-slot-container') + ?.remove(); + EventTimer.get().setProperty('didDisplayAdBlockAsk', true); } - }; - void makeRequest(); + } }, [isInVariant, adBlockerDetected, slotId, canDisplayAdBlockAsk]); - return adBlockerDetected; + return adBlockerDetected && canDisplayAdBlockAsk && isInVariant; }; diff --git a/dotcom-rendering/src/lib/useDetectAdBlock.ts b/dotcom-rendering/src/lib/useDetectAdBlock.ts new file mode 100644 index 00000000000..bdbd216a440 --- /dev/null +++ b/dotcom-rendering/src/lib/useDetectAdBlock.ts @@ -0,0 +1,32 @@ +import { getConsentFor, onConsentChange } from '@guardian/libs'; +import { useEffect, useState } from 'react'; +import { detectByRequestsOnce } from './detect-adblock'; + +export const useDetectAdBlock = (): boolean => { + const [adBlockerDetected, setAdBlockerDetected] = useState(false); + const [hasConsentForGoogletag, setHasConsentForGoogletag] = useState(false); + + useEffect(() => { + onConsentChange((consentState) => { + if (consentState.tcfv2) { + return setHasConsentForGoogletag( + getConsentFor('googletag', consentState), + ); + } + setHasConsentForGoogletag(true); + }); + }, []); + + useEffect(() => { + const makeRequest = async () => { + if (hasConsentForGoogletag) { + const detectByRequests = await detectByRequestsOnce(); + console.log('AdBlocker detected:', detectByRequests); + setAdBlockerDetected(detectByRequests); + } + }; + void makeRequest(); + }, [hasConsentForGoogletag]); + + return adBlockerDetected; +};