Skip to content

Commit

Permalink
Separate adblock detection logic from adblock ask and record in `Metr…
Browse files Browse the repository at this point in the history
…ics` (#11400)
  • Loading branch information
Jakeii authored May 10, 2024
1 parent 5897864 commit a1111c4
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 91 deletions.
10 changes: 10 additions & 0 deletions dotcom-rendering/src/components/Metrics.importable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ABTest, ABTestAPI } from '@guardian/ab-core';
import {
bypassCommercialMetricsSampling,
EventTimer,
initCommercialMetrics,
} from '@guardian/commercial';
import {
Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -171,6 +180,7 @@ export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => {
[
abTestApi,
adBlockerInUse,
detectedAdBlocker,
browserId,
commercialMetricsEnabled,
isDev,
Expand Down
69 changes: 69 additions & 0 deletions dotcom-rendering/src/lib/detect-adblock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// cache the promise so we only make the requests once
let detectByRequests: Promise<boolean> | 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<boolean> => {
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;
};
108 changes: 17 additions & 91 deletions dotcom-rendering/src/lib/useAdBlockAsk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,14 @@ 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;
const isInVariant = !!abTestAPI?.isUserInVariant('AdBlockAsk', 'variant');
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,
Expand All @@ -81,7 +21,7 @@ export const useAdblockAsk = ({
isPaidContent: boolean;
}): boolean => {
const isInVariant = useIsInAdBlockAskVariant();
const [adBlockerDetected, setAdBlockerDetected] = useState<boolean>(false);
const adBlockerDetected = useDetectAdBlock();
const [isAdFree, setIsAdFree] = useState<boolean>(false);
const [hasConsentForGoogletag, setHasConsentForGoogletag] = useState(false);

Expand All @@ -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;
};
32 changes: 32 additions & 0 deletions dotcom-rendering/src/lib/useDetectAdBlock.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>(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;
};

0 comments on commit a1111c4

Please sign in to comment.