Skip to content

Commit 82f3a68

Browse files
committed
Auxia control group
1 parent ed068ac commit 82f3a68

File tree

7 files changed

+122
-71
lines changed

7 files changed

+122
-71
lines changed

dotcom-rendering/src/components/SignInGate/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export interface AuxiaProxyGetTreatmentsPayload {
137137
showDefaultGate: ShowGateValues; // [3]
138138
gateDisplayCount: number;
139139
hideSupportMessagingTimestamp: number | undefined; // [4]
140+
isInAuxiaControlGroup: boolean;
140141
}
141142

142143
// [1]

dotcom-rendering/src/components/StickyBottomBanner.importable.tsx

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import type { BannerProps } from '@guardian/support-dotcom-components/dist/share
99
import { useEffect, useState } from 'react';
1010
import type { ArticleCounts } from '../lib/articleCount';
1111
import { getArticleCounts } from '../lib/articleCount';
12-
import type { EditionId } from '../lib/edition';
1312
import type {
1413
CandidateConfig,
1514
MaybeFC,
1615
SlotConfig,
1716
} from '../lib/messagePicker';
1817
import { pickMessage } from '../lib/messagePicker';
18+
import { useAB } from '../lib/useAB';
1919
import { useIsSignedIn } from '../lib/useAuthStatus';
2020
import { useBraze } from '../lib/useBraze';
2121
import { useCountryCode } from '../lib/useCountryCode';
@@ -33,6 +33,7 @@ import {
3333
ReaderRevenueBanner,
3434
} from './StickyBottomBanner/ReaderRevenueBanner';
3535
import type { CanShowFunctionType } from './StickyBottomBanner/ReaderRevenueBanner';
36+
import type { CanShowSignInGateProps } from './StickyBottomBanner/SignInGatePortal';
3637
import {
3738
canShowSignInGatePortal,
3839
SignInGatePortal,
@@ -179,39 +180,21 @@ const buildRRBannerConfigWith = ({
179180
};
180181

181182
const buildSignInGateConfig = (
182-
isSignedIn: boolean | undefined,
183-
isPaidContent: boolean,
184-
isPreview: boolean,
185-
contentType: string,
186-
sectionId: string,
187-
tags: TagType[],
188-
pageId: string,
189-
contributionsServiceUrl: string,
190-
editionId: EditionId,
183+
canShowProps: CanShowSignInGateProps,
191184
host?: string,
192185
): CandidateConfig<AuxiaGateDisplayData> => ({
193186
candidate: {
194187
id: 'sign-in-gate-portal',
195188
canShow: async () => {
196-
return await canShowSignInGatePortal(
197-
isSignedIn,
198-
isPaidContent,
199-
isPreview,
200-
pageId,
201-
contributionsServiceUrl,
202-
editionId,
203-
contentType,
204-
sectionId,
205-
tags,
206-
);
189+
return await canShowSignInGatePortal(canShowProps);
207190
},
208191
show: (meta: AuxiaGateDisplayData) => () => (
209192
<SignInGatePortal
210193
host={host}
211-
isPaidContent={isPaidContent}
212-
isPreview={isPreview}
213-
pageId={pageId}
214-
contributionsServiceUrl={contributionsServiceUrl}
194+
isPaidContent={canShowProps.isPaidContent}
195+
isPreview={canShowProps.isPreview}
196+
pageId={canShowProps.pageId}
197+
contributionsServiceUrl={canShowProps.contributionsServiceUrl}
215198
auxiaGateDisplayData={meta}
216199
/>
217200
),
@@ -285,6 +268,11 @@ export const StickyBottomBanner = ({
285268
const countryCode = useCountryCode('sticky-bottom-banner');
286269
const isSignedIn = useIsSignedIn();
287270
const ophanPageViewId = usePageViewId(renderingTarget);
271+
const abTestAPI = useAB()?.api;
272+
const isInAuxiaControlGroup = !!abTestAPI?.isUserInVariant(
273+
'NoAuxiaSignInGate',
274+
'control',
275+
);
288276

289277
const [SelectedBanner, setSelectedBanner] = useState<MaybeFC | null>(null);
290278
const [asyncArticleCounts, setAsyncArticleCounts] =
@@ -340,15 +328,18 @@ export const StickyBottomBanner = ({
340328
);
341329

342330
const signInGate = buildSignInGateConfig(
343-
isSignedIn,
344-
isPaidContent,
345-
isPreview,
346-
contentType,
347-
sectionId,
348-
tags,
349-
pageId,
350-
contributionsServiceUrl,
351-
editionId,
331+
{
332+
isSignedIn,
333+
isPaidContent,
334+
isPreview,
335+
contentType,
336+
sectionId,
337+
tags,
338+
pageId,
339+
contributionsServiceUrl,
340+
editionId,
341+
isInAuxiaControlGroup,
342+
},
352343
host,
353344
);
354345

@@ -392,6 +383,7 @@ export const StickyBottomBanner = ({
392383
ophanPageViewId,
393384
pageId,
394385
host,
386+
isInAuxiaControlGroup,
395387
]);
396388

397389
if (SelectedBanner) {

dotcom-rendering/src/components/StickyBottomBanner/SignInGatePortal.test.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// under test is evaluated.
33
import { buildAuxiaGateDisplayData } from '../../lib/auxia';
44
import type { AuxiaGateDisplayData } from '../SignInGate/types';
5+
import type { CanShowSignInGateProps } from './SignInGatePortal';
56
import { canShowSignInGatePortal } from './SignInGatePortal';
67

78
// Mock the auxia module (Jest hoists jest.mock calls so placing it after imports is fine).
@@ -15,6 +16,19 @@ Object.defineProperty(document, 'getElementById', {
1516
value: mockGetElementById,
1617
});
1718

19+
const canShowProps: CanShowSignInGateProps = {
20+
isSignedIn: false,
21+
isPaidContent: false,
22+
isPreview: false,
23+
pageId: 'page-id',
24+
contributionsServiceUrl: 'https://contributions.local',
25+
isInAuxiaControlGroup: false,
26+
editionId: 'UK',
27+
contentType: 'Article',
28+
sectionId: 'section',
29+
tags: [],
30+
};
31+
1832
describe('SignInGatePortal', () => {
1933
beforeEach(() => {
2034
jest.clearAllMocks();
@@ -24,7 +38,7 @@ describe('SignInGatePortal', () => {
2438
it('should return false when sign-in gate placeholder does not exist', async () => {
2539
mockGetElementById.mockReturnValue(null);
2640

27-
const result = await canShowSignInGatePortal(false, false, false);
41+
const result = await canShowSignInGatePortal(canShowProps);
2842

2943
expect(result).toEqual({ show: false });
3044
expect(mockGetElementById).toHaveBeenCalledWith('sign-in-gate');
@@ -34,7 +48,10 @@ describe('SignInGatePortal', () => {
3448
const mockElement = document.createElement('div');
3549
mockGetElementById.mockReturnValue(mockElement);
3650

37-
const result = await canShowSignInGatePortal(true, false, false);
51+
const result = await canShowSignInGatePortal({
52+
...canShowProps,
53+
isSignedIn: true,
54+
});
3855

3956
expect(result).toEqual({ show: false });
4057
});
@@ -43,7 +60,10 @@ describe('SignInGatePortal', () => {
4360
const mockElement = document.createElement('div');
4461
mockGetElementById.mockReturnValue(mockElement);
4562

46-
const result = await canShowSignInGatePortal(false, true, false);
63+
const result = await canShowSignInGatePortal({
64+
...canShowProps,
65+
isPaidContent: true,
66+
});
4767

4868
expect(result).toEqual({ show: false });
4969
});
@@ -52,7 +72,10 @@ describe('SignInGatePortal', () => {
5272
const mockElement = document.createElement('div');
5373
mockGetElementById.mockReturnValue(mockElement);
5474

55-
const result = await canShowSignInGatePortal(false, false, true);
75+
const result = await canShowSignInGatePortal({
76+
...canShowProps,
77+
isPreview: true,
78+
});
5679

5780
expect(result).toEqual({ show: false });
5881
});
@@ -83,17 +106,7 @@ describe('SignInGatePortal', () => {
83106
>
84107
).mockResolvedValue(auxiaReturn);
85108

86-
const result = await canShowSignInGatePortal(
87-
false, // isSignedIn
88-
false, // isPaidContent
89-
false, // isPreview
90-
'page-id', // pageId
91-
'https://contributions.local', // contributionsServiceUrl
92-
'UK', // editionId
93-
'Article', // contentType (must be a valid content type)
94-
'section', // sectionId
95-
[], // tags
96-
);
109+
const result = await canShowSignInGatePortal(canShowProps);
97110

98111
expect(result).toEqual({ show: true, meta: auxiaReturn });
99112
});
@@ -123,17 +136,10 @@ describe('SignInGatePortal', () => {
123136
>
124137
).mockResolvedValue(auxiaReturn);
125138

126-
const result = await canShowSignInGatePortal(
127-
undefined,
128-
false,
129-
false,
130-
'page-id',
131-
'https://contributions.local',
132-
'UK',
133-
'Article',
134-
'section',
135-
[],
136-
);
139+
const result = await canShowSignInGatePortal({
140+
...canShowProps,
141+
isSignedIn: undefined,
142+
});
137143

138144
expect(result).toEqual({ show: true, meta: auxiaReturn });
139145
});

dotcom-rendering/src/components/StickyBottomBanner/SignInGatePortal.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,30 @@ export const SignInGatePortal = ({
140140
* This replicates the logic from SignInGateSelector but is adapted
141141
* for use within the message picker system.
142142
*/
143-
export const canShowSignInGatePortal = async (
144-
isSignedIn: boolean | undefined,
145-
isPaidContent: boolean,
146-
isPreview: boolean,
147-
pageId?: string,
148-
contributionsServiceUrl?: string,
149-
editionId?: EditionId,
150-
contentType?: string,
151-
sectionId?: string,
152-
tags?: TagType[],
153-
): Promise<CanShowResult<AuxiaGateDisplayData>> => {
143+
export interface CanShowSignInGateProps {
144+
isSignedIn: boolean | undefined;
145+
isPaidContent: boolean;
146+
isPreview: boolean;
147+
pageId: string;
148+
contributionsServiceUrl: string;
149+
isInAuxiaControlGroup: boolean;
150+
editionId?: EditionId;
151+
contentType?: string;
152+
sectionId?: string;
153+
tags?: TagType[];
154+
}
155+
export const canShowSignInGatePortal = async ({
156+
isSignedIn,
157+
isPaidContent,
158+
isPreview,
159+
pageId,
160+
contributionsServiceUrl,
161+
isInAuxiaControlGroup,
162+
editionId,
163+
contentType,
164+
sectionId,
165+
tags,
166+
}: CanShowSignInGateProps): Promise<CanShowResult<AuxiaGateDisplayData>> => {
154167
// Check if the sign-in gate placeholder exists in the DOM
155168
const targetElement = document.getElementById('sign-in-gate');
156169

@@ -185,6 +198,7 @@ export const canShowSignInGatePortal = async (
185198
sectionId,
186199
tags,
187200
retrieveLastGateDismissedCount('AuxiaSignInGate'),
201+
isInAuxiaControlGroup,
188202
);
189203

190204
return {

dotcom-rendering/src/experiments/ab-tests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ABTest } from '@guardian/ab-core';
22
import { abTestTest } from './tests/ab-test-test';
33
import { auxiaSignInGate } from './tests/auxia-sign-in-gate';
4+
import { noAuxiaSignInGate } from './tests/no-auxia-sign-in-gate';
45
import { signInGateMainControl } from './tests/sign-in-gate-main-control';
56
import { signInGateMainVariant } from './tests/sign-in-gate-main-variant';
67
import { userBenefitsApi } from './tests/user-benefits-api';
@@ -13,4 +14,5 @@ export const tests: ABTest[] = [
1314
signInGateMainControl,
1415
userBenefitsApi,
1516
auxiaSignInGate,
17+
noAuxiaSignInGate,
1618
];
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ABTest } from '@guardian/ab-core';
2+
3+
/**
4+
* The requirement:
5+
* 1. keep a fixed 5% of the global audience excluded from Auxia (though they can still see gates)
6+
* 2. track participation of this 5% in the pageview table, regardless of whether they see gates
7+
*
8+
* The solution:
9+
* - On every article view, put 5% of the audience into a special NoAuxiaSignInGate AB test. This means the page tracks membership of the "test" through ophan. There are no variants in this test, it's just a way to track these browsers.
10+
* - The API call for the sign-in gate includes a flag indicating that the browser is in the NoAuxiaSignInGate test, and it excludes them from Auxia based on this.
11+
*
12+
* This enables us to query the pageview table for browsers in the NoAuxiaSignInGate group using the existing ab_test_array field.
13+
*/
14+
export const noAuxiaSignInGate: ABTest = {
15+
id: 'NoAuxiaSignInGate',
16+
start: '2025-11-01',
17+
expiry: '2027-11-01',
18+
author: 'Growth Team',
19+
description:
20+
'Defines a control group who should not have sign-in gate journeys handled by Auxia',
21+
audience: 0.05,
22+
audienceOffset: 0,
23+
audienceCriteria: 'All users',
24+
successMeasure: 'Control group for Auxia sign-in gate testing',
25+
canRun: () => true,
26+
variants: [
27+
{
28+
id: 'control',
29+
test: (): void => {},
30+
},
31+
],
32+
};

dotcom-rendering/src/lib/auxia.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const fetchProxyGetTreatments = async (
7878
showDefaultGate: ShowGateValues,
7979
gateDisplayCount: number,
8080
hideSupportMessagingTimestamp: number | undefined,
81+
isInAuxiaControlGroup: boolean,
8182
): Promise<AuxiaProxyGetTreatmentsResponse> => {
8283
const articleIdentifier = `www.theguardian.com/${pageId}`;
8384
const url = `${contributionsServiceUrl}/auxia/get-treatments`;
@@ -100,6 +101,7 @@ const fetchProxyGetTreatments = async (
100101
showDefaultGate,
101102
gateDisplayCount,
102103
hideSupportMessagingTimestamp,
104+
isInAuxiaControlGroup,
103105
};
104106

105107
const params = { method: 'POST', headers, body: JSON.stringify(payload) };
@@ -168,6 +170,7 @@ export const buildAuxiaGateDisplayData = async (
168170
sectionId: string,
169171
tags: TagType[],
170172
gateDismissCount: number,
173+
isInAuxiaControlGroup: boolean,
171174
): Promise<AuxiaGateDisplayData | undefined> => {
172175
const readerPersonalData = await decideAuxiaProxyReaderPersonalData();
173176
const tagIds = tags.map((tag) => tag.id);
@@ -195,6 +198,7 @@ export const buildAuxiaGateDisplayData = async (
195198
showDefaultGate,
196199
gateDisplayCount,
197200
hideSupportMessagingTimestamp,
201+
isInAuxiaControlGroup,
198202
);
199203

200204
if (response.status && response.data) {

0 commit comments

Comments
 (0)