From 6bacf0dbf87e092b8d978745c773c44d944db20b Mon Sep 17 00:00:00 2001 From: Juarez Mota Date: Tue, 20 Oct 2026 13:50:13 +0100 Subject: [PATCH] Add Auxia variant rollout tests and implement MVT ID handling --- dotcom-rendering/src/client/abTesting.test.ts | 27 ++++ .../src/components/Metrics.island.test.ts | 9 ++ .../src/components/Metrics.island.tsx | 4 +- .../ReaderRevenueBanner.test.ts | 117 ++++++++++++++++++ .../ReaderRevenueBanner.tsx | 95 +++++++++----- 5 files changed, 219 insertions(+), 33 deletions(-) create mode 100644 dotcom-rendering/src/client/abTesting.test.ts create mode 100644 dotcom-rendering/src/components/Metrics.island.test.ts create mode 100644 dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.test.ts diff --git a/dotcom-rendering/src/client/abTesting.test.ts b/dotcom-rendering/src/client/abTesting.test.ts new file mode 100644 index 00000000000..399b8383e4c --- /dev/null +++ b/dotcom-rendering/src/client/abTesting.test.ts @@ -0,0 +1,27 @@ +import { getCookie } from '@guardian/libs'; +import { getABTestParticipations } from './abTesting'; + +jest.mock('@guardian/libs', () => ({ + getCookie: jest.fn(), +})); + +const mockGetCookie = jest.mocked(getCookie); + +describe('getABTestParticipations', () => { + beforeEach(() => { + jest.clearAllMocks(); + window.guardian.config.serverSideABTests = { + 'growth-auxia-banner': 'variant', + 'server-side-test': 'control', + }; + mockGetCookie.mockReturnValue('client-side-test:variant'); + }); + + it('keeps the existing client and server test assignments intact', () => { + expect(getABTestParticipations()).toEqual({ + 'client-side-test': 'variant', + 'growth-auxia-banner': 'variant', + 'server-side-test': 'control', + }); + }); +}); diff --git a/dotcom-rendering/src/components/Metrics.island.test.ts b/dotcom-rendering/src/components/Metrics.island.test.ts new file mode 100644 index 00000000000..30d284cbccf --- /dev/null +++ b/dotcom-rendering/src/components/Metrics.island.test.ts @@ -0,0 +1,9 @@ +import { shouldCollectMetricsForTests } from './Metrics.island'; + +describe('forced test metrics collection', () => { + it('continues to collect metrics for the Auxia experiment Variant', () => { + expect(shouldCollectMetricsForTests(['growth-auxia-banner'])).toBe( + true, + ); + }); +}); diff --git a/dotcom-rendering/src/components/Metrics.island.tsx b/dotcom-rendering/src/components/Metrics.island.tsx index c3621be2aaa..9dbc9b43f33 100644 --- a/dotcom-rendering/src/components/Metrics.island.tsx +++ b/dotcom-rendering/src/components/Metrics.island.tsx @@ -26,7 +26,9 @@ const sampling = 1 / 100; /** defining this here allows to share this with other metrics */ const willRecordCoreWebVitals = Math.random() < sampling; -const shouldCollectMetricsForTests = (userTestParticipations: string[]) => { +export const shouldCollectMetricsForTests = ( + userTestParticipations: string[], +) => { const userParticipationConfigs = activeABtests.filter((test) => userTestParticipations.includes(test.name), ); diff --git a/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.test.ts b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.test.ts new file mode 100644 index 00000000000..b3ce088b553 --- /dev/null +++ b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.test.ts @@ -0,0 +1,117 @@ +import { getCookie } from '@guardian/libs'; +import { buildPayload, getAuxiaRolloutMvtId } from './ReaderRevenueBanner'; + +jest.mock('@guardian/libs', () => ({ + getCookie: jest.fn(), +})); + +jest.mock('../../lib/contributions', () => ({ + getAuthHeaders: jest.fn(), + getPurchaseInfo: jest.fn(), + hasOptedOutOfArticleCount: jest.fn(), + recentlyClosedBanner: jest.fn(), + setLocalNoBannerCachePeriod: jest.fn(), + shouldHideSupportMessaging: jest.fn(), + withinLocalNoBannerCachePeriod: jest.fn(), +})); + +jest.mock('../../lib/fetchEmail', () => ({ + lazyFetchEmailWithTimeout: jest.fn(), +})); + +const mockGetCookie = jest.mocked(getCookie); + +const buildBannerPayload = async ( + inAuxiaVariant: boolean, + userConsent: boolean, + mvtIdCookie: string | null, +) => { + mockGetCookie.mockImplementation(({ name }) => { + if (name === 'GU_mvt_id') return mvtIdCookie; + if (name === 'bwid') return 'browser-id'; + return null; + }); + + return buildPayload({ + isSignedIn: false, + contentType: 'Article', + shouldHideReaderRevenue: false, + isMinuteArticle: false, + isPaidContent: false, + isSensitive: false, + tags: [], + contributionsServiceUrl: 'https://contributions.example.com', + countryCode: 'GB', + optedOutOfArticleCount: false, + asyncArticleCounts: Promise.resolve(undefined), + userConsent, + hideSupportMessagingForUser: false, + inAuxiaVariant, + }); +}; + +describe('getAuxiaRolloutMvtId', () => { + it.each([ + [true, true, '123456', 123456], + [false, true, '123456', undefined], + [true, false, '123456', undefined], + [true, true, null, undefined], + [true, true, '', undefined], + [true, true, '1e3', undefined], + [true, true, '-1', undefined], + [true, true, '1000000', undefined], + ])( + 'only returns a valid MVT ID for a consented Variant reader', + (inAuxiaVariant, userConsent, mvtIdCookie, expected) => { + expect( + getAuxiaRolloutMvtId(inAuxiaVariant, userConsent, mvtIdCookie), + ).toBe(expected); + }, + ); +}); + +describe('banner request targeting payload', () => { + it.each([ + ['valid MVT ID', '123456', 123456], + ['missing MVT ID', null, undefined], + ['malformed MVT ID', '1e3', undefined], + ['out-of-range MVT ID', '1000000', undefined], + ])( + 'only includes the rollout MVT field for a consented Variant with %s', + async (_description, mvtIdCookie, expectedId) => { + const payload = await buildBannerPayload(true, true, mvtIdCookie); + expect(payload.targeting).toHaveProperty('browserId', 'browser-id'); + + if (expectedId === undefined) { + expect(payload.targeting).not.toHaveProperty( + 'auxiaRolloutMvtId', + ); + } else { + expect(payload.targeting).toHaveProperty( + 'auxiaRolloutMvtId', + expectedId, + ); + } + }, + ); + + it.each([ + ['Control', false, true], + ['unconsented Variant', true, false], + ])( + 'omits Auxia targeting for %s readers', + async (_description, inAuxiaVariant, userConsent) => { + const payload = await buildBannerPayload( + inAuxiaVariant, + userConsent, + '123456', + ); + + expect(payload.targeting).not.toHaveProperty('auxiaRolloutMvtId'); + expect(payload.targeting.browserId).toBeUndefined(); + expect(JSON.stringify(payload.targeting)).not.toContain( + '"browserId"', + ); + }, + ); +}); diff --git a/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx index 8b79029ea46..c813b8ba4b0 100644 --- a/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx +++ b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx @@ -63,6 +63,28 @@ type BuildPayloadProps = BaseProps & { hideSupportMessagingForUser: boolean; }; +const MAX_AUXIA_ROLLOUT_MVT_ID = 1_000_000; + +export const getAuxiaRolloutMvtId = ( + inAuxiaVariant: boolean | undefined, + userConsent: boolean, + mvtIdCookie: string | null, +): number | undefined => { + if ( + inAuxiaVariant !== true || + !userConsent || + mvtIdCookie === null || + !/^\d+$/.test(mvtIdCookie) + ) { + return undefined; + } + + const mvtId = Number(mvtIdCookie); + return Number.isInteger(mvtId) && mvtId < MAX_AUXIA_ROLLOUT_MVT_ID + ? mvtId + : undefined; +}; + type CanShowProps = BaseProps & { countryCode: CountryCode; remoteBannerConfig: boolean; @@ -130,7 +152,7 @@ export const hasRequiredConsents = (): Promise => .then(({ canTarget }: ConsentState) => canTarget) .catch(() => false); -const buildPayload = async ({ +export const buildPayload = async ({ isSignedIn, shouldHideReaderRevenue, isPaidContent, @@ -152,7 +174,7 @@ const buildPayload = async ({ isSensitive, }: BuildPayloadProps): Promise => { const getBrowserId = (): string | undefined => { - if (!inAuxiaVariant) { + if (inAuxiaVariant !== true) { return undefined; } if (!userConsent) { @@ -160,42 +182,51 @@ const buildPayload = async ({ } return getCookie({ name: 'bwid', shouldMemoize: true }) ?? undefined; }; + const auxiaRolloutMvtId = + inAuxiaVariant === true && userConsent + ? getAuxiaRolloutMvtId( + inAuxiaVariant, + userConsent, + getCookie({ name: 'GU_mvt_id', shouldMemoize: true }), + ) + : undefined; const articleCounts = await asyncArticleCounts; const weeklyArticleHistory = articleCounts?.weeklyArticleHistory; const articleCountToday = getArticleCountToday(articleCounts); - return { - targeting: { - shouldHideReaderRevenue, - isPaidContent, - showSupportMessaging: !hideSupportMessagingForUser, - engagementBannerLastClosedAt, - subscriptionBannerLastClosedAt, - signInBannerLastClosedAt, - abandonedBasketBannerLastClosedAt, - mvtId: Number( - getCookie({ name: 'GU_mvt_id', shouldMemoize: true }), - ), - countryCode, - weeklyArticleHistory, - articleCountToday, - hasOptedOutOfArticleCount: optedOutOfArticleCount, - sectionId, - tagIds: tags.map((tag) => tag.id), - contentType, - purchaseInfo: getPurchaseInfo(), - isSignedIn, - hasConsented: userConsent, - abandonedBasket: parseAbandonedBasket( - getCookie({ name: 'GU_CO_INCOMPLETE', shouldMemoize: true }), - ), - pageId, - inHoldbackGroup, - browserId: getBrowserId(), - isSensitive, - }, + const targeting: BannerPayload['targeting'] & { + auxiaRolloutMvtId?: number; + } = { + shouldHideReaderRevenue, + isPaidContent, + showSupportMessaging: !hideSupportMessagingForUser, + engagementBannerLastClosedAt, + subscriptionBannerLastClosedAt, + signInBannerLastClosedAt, + abandonedBasketBannerLastClosedAt, + mvtId: Number(getCookie({ name: 'GU_mvt_id', shouldMemoize: true })), + countryCode, + weeklyArticleHistory, + articleCountToday, + hasOptedOutOfArticleCount: optedOutOfArticleCount, + sectionId, + tagIds: tags.map((tag) => tag.id), + contentType, + purchaseInfo: getPurchaseInfo(), + isSignedIn, + hasConsented: userConsent, + abandonedBasket: parseAbandonedBasket( + getCookie({ name: 'GU_CO_INCOMPLETE', shouldMemoize: true }), + ), + pageId, + inHoldbackGroup, + browserId: getBrowserId(), + isSensitive, + ...(auxiaRolloutMvtId === undefined ? {} : { auxiaRolloutMvtId }), }; + + return { targeting }; }; export const canShowRRBanner: CanShowFunctionType<