Skip to content

Commit 0dd18f3

Browse files
Merge branch 'main' into dependabot/github_actions/guardian/dot-github/dot-github/workflows/require-label.yaml-3.1.0
2 parents 57fd32b + 98cbe02 commit 0dd18f3

22 files changed

Lines changed: 1042 additions & 2763 deletions

ab-testing/cdk/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
]
2222
},
2323
"devDependencies": {
24-
"@aws-sdk/client-s3": "3.931.0",
25-
"@aws-sdk/client-ssm": "3.621.0",
24+
"@aws-sdk/client-s3": "3.991.0",
25+
"@aws-sdk/client-ssm": "3.991.0",
2626
"@guardian/cdk": "62.0.1",
2727
"@guardian/eslint-config": "12.0.1",
2828
"@guardian/tsconfig": "1.0.1",

ab-testing/config/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
]
2929
},
3030
"devDependencies": {
31-
"@aws-sdk/client-s3": "3.931.0",
32-
"@aws-sdk/client-ssm": "3.621.0",
31+
"@aws-sdk/client-s3": "3.991.0",
32+
"@aws-sdk/client-ssm": "3.991.0",
3333
"@guardian/cdk": "62.0.1",
3434
"@guardian/eslint-config": "12.0.1",
3535
"@guardian/tsconfig": "1.0.1",

ab-testing/deploy-lambda/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"prettier:fix": "prettier . --write --cache"
1313
},
1414
"dependencies": {
15-
"@aws-sdk/client-s3": "3.981.0",
16-
"@aws-sdk/client-ssm": "3.981.0",
15+
"@aws-sdk/client-s3": "3.991.0",
16+
"@aws-sdk/client-ssm": "3.991.0",
1717
"@guardian/ab-testing-config": "workspace:ab-testing-config",
1818
"superstruct": "2.0.2"
1919
},

ab-testing/notification-lambda/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"prettier:fix": "prettier . --write --cache"
1414
},
1515
"dependencies": {
16-
"@aws-sdk/client-ses": "3.981.0",
17-
"@aws-sdk/credential-providers": "3.981.0",
16+
"@aws-sdk/client-ses": "3.991.0",
17+
"@aws-sdk/credential-providers": "3.991.0",
1818
"@guardian/ab-testing-config": "workspace:ab-testing-config"
1919
},
2020
"devDependencies": {

dotcom-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build-storybook": "storybook build"
2020
},
2121
"dependencies": {
22-
"@aws-sdk/client-cloudwatch": "3.981.0",
22+
"@aws-sdk/client-cloudwatch": "3.991.0",
2323
"@babel/helper-compilation-targets": "7.28.6",
2424
"@braze/web-sdk": "5.8.1",
2525
"@creditkarma/thrift-server-core": "1.0.4",
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { isInUsa } from '@guardian/commercial-core/geo/geo-utils';
2+
import type { Admiral, AdmiralEvent } from '@guardian/commercial-core/types';
3+
import { cmp, getCookie, log } from '@guardian/libs';
4+
import { useEffect } from 'react';
5+
import { useAB } from '../lib/useAB';
6+
7+
/**
8+
* Fetches AB test variant name for Admiral, as there are two variants
9+
*/
10+
const getAdmiralAbTestVariant = (
11+
ab: ReturnType<typeof useAB> | undefined,
12+
): string | undefined => {
13+
if (ab?.api.isUserInVariant('AdmiralAdblockRecovery', 'variant-detect')) {
14+
return 'variant-detect';
15+
}
16+
if (ab?.api.isUserInVariant('AdmiralAdblockRecovery', 'variant-recover')) {
17+
return 'variant-recover';
18+
}
19+
if (ab?.api.isUserInVariant('AdmiralAdblockRecovery', 'control')) {
20+
return 'control';
21+
}
22+
return undefined;
23+
};
24+
25+
/**
26+
* Sends component events to Ophan with the componentType of `AD_BLOCK_RECOVERY`
27+
* as well as sending the AB test participation
28+
*
29+
* @param ab - The AB test API from useAB hook
30+
* @param overrides allows overriding / setting values for `action` and `value`
31+
*/
32+
const recordAdmiralOphanEvent = (
33+
ab: ReturnType<typeof useAB> | undefined,
34+
{
35+
action,
36+
value,
37+
}: {
38+
action: 'INSERT' | 'DETECT' | 'VIEW' | 'CLOSE';
39+
value?: string;
40+
},
41+
) => {
42+
const abTestVariant = getAdmiralAbTestVariant(ab);
43+
44+
window.guardian.ophan?.record({
45+
componentEvent: {
46+
component: {
47+
componentType: 'AD_BLOCK_RECOVERY',
48+
id: 'admiral-adblock-recovery',
49+
},
50+
action,
51+
...(value && { value }),
52+
...(abTestVariant && {
53+
abTest: {
54+
name: 'AdmiralAdblockRecovery',
55+
variant: abTestVariant,
56+
},
57+
}),
58+
},
59+
});
60+
};
61+
62+
// Admiral event types
63+
type MeasureDetectedEvent = {
64+
adblocking: boolean;
65+
whitelisted: boolean;
66+
subscribed: boolean;
67+
};
68+
69+
type CandidateShownEvent = {
70+
candidateID: string;
71+
variantID?: string;
72+
candidateGroups: string[];
73+
};
74+
75+
type CandidateDismissedEvent = {
76+
candidateID: string;
77+
candidateGroups: string[];
78+
};
79+
80+
// Admiral event handlers
81+
const handleMeasureDetectedEvent = (
82+
ab: ReturnType<typeof useAB> | undefined,
83+
event: AdmiralEvent,
84+
): void => {
85+
const isMeasureDetectedEvent = (
86+
e: AdmiralEvent,
87+
): e is MeasureDetectedEvent =>
88+
typeof e === 'object' &&
89+
'adblocking' in e &&
90+
'whitelisted' in e &&
91+
'subscribed' in e;
92+
93+
if (!isMeasureDetectedEvent(event)) {
94+
log(
95+
'commercial',
96+
`🛡️ Admiral - Event is not of expected format of measure.detected ${JSON.stringify(
97+
event,
98+
)}`,
99+
);
100+
return;
101+
}
102+
103+
if (event.adblocking) {
104+
log(
105+
'commercial',
106+
'🛡️ Admiral - user has an adblocker and it is enabled',
107+
);
108+
recordAdmiralOphanEvent(ab, { action: 'DETECT', value: 'blocked' });
109+
}
110+
if (event.whitelisted) {
111+
log(
112+
'commercial',
113+
'🛡️ Admiral - user has seen Engage and subsequently disabled their adblocker',
114+
);
115+
recordAdmiralOphanEvent(ab, {
116+
action: 'DETECT',
117+
value: 'whitelisted',
118+
});
119+
}
120+
if (event.subscribed) {
121+
log(
122+
'commercial',
123+
'🛡️ Admiral - user has an active subscription to a transact plan',
124+
);
125+
}
126+
};
127+
128+
const handleCandidateShownEvent = (
129+
ab: ReturnType<typeof useAB> | undefined,
130+
event: AdmiralEvent,
131+
): void => {
132+
const isCandidateShownEvent = (e: AdmiralEvent): e is CandidateShownEvent =>
133+
typeof e === 'object' &&
134+
'candidateID' in e &&
135+
'variantID' in e &&
136+
'candidateGroups' in e;
137+
138+
if (isCandidateShownEvent(event)) {
139+
log(
140+
'commercial',
141+
`🛡️ Admiral - Launching candidate ${event.candidateID}`,
142+
);
143+
recordAdmiralOphanEvent(ab, {
144+
action: 'VIEW',
145+
value: event.candidateID,
146+
});
147+
} else {
148+
log(
149+
'commercial',
150+
`🛡️ Admiral - Event is not of expected format of candidate.shown ${JSON.stringify(
151+
event,
152+
)}`,
153+
);
154+
}
155+
};
156+
157+
const handleCandidateDismissedEvent = (
158+
ab: ReturnType<typeof useAB> | undefined,
159+
event: AdmiralEvent,
160+
): void => {
161+
const isCandidateDismissedEvent = (
162+
e: AdmiralEvent,
163+
): e is CandidateDismissedEvent =>
164+
typeof e === 'object' && 'candidateID' in e && 'candidateGroups' in e;
165+
166+
if (isCandidateDismissedEvent(event)) {
167+
log(
168+
'commercial',
169+
`🛡️ Admiral - Candidate ${event.candidateID} was dismissed`,
170+
);
171+
recordAdmiralOphanEvent(ab, {
172+
action: 'CLOSE',
173+
value: event.candidateID,
174+
});
175+
} else {
176+
log(
177+
'commercial',
178+
`🛡️ Admiral - Event is not of expected format of candidate.dismissed ${JSON.stringify(
179+
event,
180+
)}`,
181+
);
182+
}
183+
};
184+
185+
const setUpAdmiralEventLogger = (
186+
admiral: Admiral,
187+
ab: ReturnType<typeof useAB> | undefined,
188+
): void => {
189+
admiral('after', 'measure.detected', function (event) {
190+
handleMeasureDetectedEvent(ab, event);
191+
});
192+
193+
admiral('after', 'candidate.shown', function (event) {
194+
handleCandidateShownEvent(ab, event);
195+
});
196+
197+
admiral('after', 'candidate.dismissed', function (event) {
198+
handleCandidateDismissedEvent(ab, event);
199+
});
200+
};
201+
202+
// Check if Commercial has already initialized Admiral (bootstrap loaded, not just stub)
203+
const isComHandlingAdmiral = (): boolean => {
204+
// If window.admiral exists and has been initialized by bootstrap (not just the queue stub)
205+
// the bootstrap replaces the stub with a proper function that doesn't have .q property
206+
type AdmiralStub = Admiral & { q?: any[] };
207+
const w = window as Window & { admiral?: AdmiralStub };
208+
209+
const admiralExists = typeof w.admiral === 'function';
210+
const admiralAsRecord = w.admiral as unknown as Record<string, unknown>;
211+
const admiralIsOnlyStub = admiralExists && Array.isArray(admiralAsRecord.q);
212+
const admiralIsInitialized = admiralExists && !admiralIsOnlyStub;
213+
// Check for explicit DCR flag if set
214+
const commercialOwnsAdmiral =
215+
window.guardian.config.switches.dcrOwnsAdmiral === false;
216+
217+
if (admiralIsInitialized || commercialOwnsAdmiral) {
218+
log(
219+
'dotcom',
220+
'🛡️ Admiral - Commercial is handling Admiral, skipping commercial initialization',
221+
);
222+
return true;
223+
}
224+
return false;
225+
};
226+
227+
export const AdmiralScript = () => {
228+
const ab = useAB();
229+
const abTestVariant = getAdmiralAbTestVariant(ab);
230+
const isInVariant = abTestVariant?.startsWith('variant') ?? false;
231+
232+
useEffect(() => {
233+
/**
234+
* The Admiral bootstrap script should only run under the following conditions:
235+
*
236+
* - Should not run if the CMP is due to show
237+
* - Should only run in the US
238+
* - Should only run if in the variant of the AB test
239+
* - Should not run if the gu_hide_support_messaging cookie is set
240+
* - Should not run for content marked as: shouldHideAdverts, shouldHideReaderRevenue, isSensitive
241+
* - Should not run for paid-content sponsorship type (includes Hosted Content)
242+
* - Should not run for certain sections
243+
*/
244+
const page = window.guardian.config.page;
245+
246+
const shouldRun =
247+
!isComHandlingAdmiral() &&
248+
cmp.hasInitialised() &&
249+
!cmp.willShowPrivacyMessageSync() &&
250+
isInUsa() &&
251+
isInVariant &&
252+
!getCookie({
253+
name: 'gu_hide_support_messaging',
254+
shouldMemoize: true,
255+
}) &&
256+
!page.shouldHideAdverts &&
257+
!page.shouldHideReaderRevenue &&
258+
!page.isSensitive &&
259+
page.sponsorshipType !== 'paid-content' &&
260+
![
261+
'about',
262+
'info',
263+
'membership',
264+
'help',
265+
'guardian-live-australia',
266+
'gnm-archive',
267+
'guardian-labs',
268+
'thefilter',
269+
].includes(page.section ?? '');
270+
if (!shouldRun) return;
271+
272+
// Record INSERT Ophan event
273+
recordAdmiralOphanEvent(ab, { action: 'INSERT' });
274+
275+
// Initialize Admiral Adblock Recovery
276+
log('dotcom', '🛡️ Initialising Admiral Adblock Recovery');
277+
278+
// Set up window.admiral stub
279+
// This initializes admiral before the bootstrap script loads
280+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required for Admiral stub initialization
281+
type AdmiralStub = Admiral & { q?: any[] };
282+
const w = window as Window & { admiral?: AdmiralStub };
283+
284+
if (!w.admiral) {
285+
// Create the Admiral stub function with queue
286+
const stub = function (...args: unknown[]) {
287+
if (!stub.q) stub.q = [];
288+
stub.q.push(args);
289+
} as AdmiralStub;
290+
w.admiral = stub;
291+
}
292+
293+
log('dotcom', '🛡️ Setting up Admiral event logger');
294+
295+
// Set up Admiral event logging
296+
setUpAdmiralEventLogger(w.admiral, ab);
297+
298+
// Set AB test targeting
299+
if (abTestVariant) {
300+
w.admiral('targeting', 'set', 'guAbTest', abTestVariant);
301+
}
302+
303+
// Load Admiral bootstrap script
304+
const BASE_AJAX_URL = window.guardian.config.page.ajaxUrl;
305+
306+
const admiralScript = document.createElement('script');
307+
admiralScript.src = `${BASE_AJAX_URL}/commercial/admiral-bootstrap.js`;
308+
admiralScript.async = true;
309+
document.head.appendChild(admiralScript);
310+
311+
log('dotcom', '🛡️ Admiral initialization complete');
312+
313+
return () => {
314+
// Clean up Admiral bootstrap script
315+
admiralScript.parentNode?.removeChild(admiralScript);
316+
};
317+
}, [ab, isInVariant, abTestVariant]);
318+
319+
return null;
320+
};

dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
55
import { rootStyles } from '../lib/rootStyles';
66
import type { NavType } from '../model/extract-nav';
77
import type { DCRNewslettersPageType } from '../types/newslettersPage';
8+
import { AdmiralScript } from './AdmiralScript.importable';
89
import { AlreadyVisited } from './AlreadyVisited.importable';
910
import { useConfig } from './ConfigContext';
1011
import { FocusStyles } from './FocusStyles.importable';
@@ -47,6 +48,9 @@ export const AllEditorialNewslettersPage = ({
4748
<Island priority="feature" defer={{ until: 'idle' }}>
4849
<AlreadyVisited />
4950
</Island>
51+
<Island priority="feature" defer={{ until: 'idle' }}>
52+
<AdmiralScript />
53+
</Island>
5054
<Island priority="feature" defer={{ until: 'idle' }}>
5155
<FocusStyles />
5256
</Island>

0 commit comments

Comments
 (0)