diff --git a/dotcom-rendering/src/components/FrontPage.tsx b/dotcom-rendering/src/components/FrontPage.tsx
index c7e9aa095f3..6e98976db7b 100644
--- a/dotcom-rendering/src/components/FrontPage.tsx
+++ b/dotcom-rendering/src/components/FrontPage.tsx
@@ -79,6 +79,7 @@ export const FrontPage = ({ front, NAV }: Props) => {
diff --git a/dotcom-rendering/src/components/SetABTests.island.tsx b/dotcom-rendering/src/components/SetABTests.island.tsx
index 480188fc896..47569faa840 100644
--- a/dotcom-rendering/src/components/SetABTests.island.tsx
+++ b/dotcom-rendering/src/components/SetABTests.island.tsx
@@ -4,10 +4,12 @@ import { getOphan } from '../client/ophan/ophan';
import { ABTests } from '../experiments/lib/ab-tests';
import { isServer } from '../lib/isServer';
import { setABTests } from '../lib/useAB';
+import type { EditorialAbTest } from '../types/front';
import { useConfig } from './ConfigContext';
type Props = {
serverSideABTests: Record;
+ editorialAbTests?: EditorialAbTest[];
};
const errorReporter = (e: unknown) =>
@@ -27,7 +29,7 @@ const errorReporter = (e: unknown) =>
*
* Does not render **anything**.
*/
-export const SetABTests = ({ serverSideABTests }: Props) => {
+export const SetABTests = ({ serverSideABTests, editorialAbTests }: Props) => {
const { renderingTarget } = useConfig();
const [ophan, setOphan] = useState>>();
@@ -46,14 +48,15 @@ export const SetABTests = ({ serverSideABTests }: Props) => {
const abTests = new ABTests(
isServer
? {
+ editorialAbTests,
serverSideABTests,
isServer: true,
}
- : { isServer: false },
+ : { editorialAbTests, isServer: false },
);
setABTests(abTests);
return abTests;
- }, [serverSideABTests]);
+ }, [serverSideABTests, editorialAbTests]);
useEffect(() => {
if (!ophan) {
diff --git a/dotcom-rendering/src/experiments/lib/ab-tests.ts b/dotcom-rendering/src/experiments/lib/ab-tests.ts
index e8154630036..4d0ac092ff1 100644
--- a/dotcom-rendering/src/experiments/lib/ab-tests.ts
+++ b/dotcom-rendering/src/experiments/lib/ab-tests.ts
@@ -1,6 +1,7 @@
import { activeABtests } from '@guardian/ab-testing-config';
import { isUndefined } from '@guardian/libs';
import { getABTestParticipations } from '../../client/abTesting';
+import { EditorialAbTest } from '../../types/front';
export interface ABTestAPI {
getParticipations: () => ABParticipations;
@@ -28,7 +29,9 @@ type OphanRecordFunction = (send: Record) => void;
type ErrorReporter = (e: unknown) => void;
-type ABTestsConfig =
+type ABTestsConfig = {
+ editorialAbTests?: EditorialAbTest[];
+} & (
| {
isServer: true;
serverSideABTests: Record;
@@ -36,7 +39,8 @@ type ABTestsConfig =
| {
isServer: false;
serverSideABTests?: never;
- };
+ }
+);
/**
* generate an A/B event for Ophan
@@ -52,8 +56,15 @@ const makeABEvent = (variantName: string, complete: boolean): OphanABEvent => {
export class ABTests implements ABTestAPI {
private participations: ABParticipations;
+ private editorialParticipations: EditorialAbTest[] | undefined;
+
+ constructor({
+ isServer,
+ serverSideABTests,
+ editorialAbTests,
+ }: ABTestsConfig) {
+ this.editorialParticipations = editorialAbTests;
- constructor({ isServer, serverSideABTests }: ABTestsConfig) {
if (isServer) {
this.participations = serverSideABTests;
} else {
@@ -79,9 +90,25 @@ export class ABTests implements ABTestAPI {
): void {
ophanRecord({
abTestRegister: this.buildOphanPayload(errorReporter),
+ editorialAbTestRegister: this.buildOphanEditorialPayload(),
});
}
+ private buildOphanEditorialPayload(): OphanABPayload {
+ return (
+ this.editorialParticipations?.reduce(
+ (eventLog, test) => {
+ const variantId = test.variantMeta[0]?.id;
+
+ if (!variantId) return eventLog;
+ eventLog[test.testUuid] = makeABEvent(variantId, false);
+ return eventLog;
+ },
+ {},
+ ) ?? {}
+ );
+ }
+
private shouldReportToOphan(testId: string): boolean {
const activeTest = activeABtests.find(({ name }) => name === testId);
return activeTest?.shouldReportToOphan
diff --git a/dotcom-rendering/src/frontend/schemas/feFront.json b/dotcom-rendering/src/frontend/schemas/feFront.json
index 635b4daaed9..6602bfe0bdb 100644
--- a/dotcom-rendering/src/frontend/schemas/feFront.json
+++ b/dotcom-rendering/src/frontend/schemas/feFront.json
@@ -4459,4 +4459,4 @@
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
-}
\ No newline at end of file
+}
diff --git a/dotcom-rendering/src/frontend/schemas/feTagPage.json b/dotcom-rendering/src/frontend/schemas/feTagPage.json
index 5d960526579..8edae4318b9 100644
--- a/dotcom-rendering/src/frontend/schemas/feTagPage.json
+++ b/dotcom-rendering/src/frontend/schemas/feTagPage.json
@@ -2149,4 +2149,4 @@
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
-}
\ No newline at end of file
+}
diff --git a/dotcom-rendering/src/server/handler.front.web.ts b/dotcom-rendering/src/server/handler.front.web.ts
index 6e94ff02f33..82a2c747319 100644
--- a/dotcom-rendering/src/server/handler.front.web.ts
+++ b/dotcom-rendering/src/server/handler.front.web.ts
@@ -13,7 +13,7 @@ import {
import { groupTrailsByDates } from '../model/groupTrailsByDates';
import { getSpeedFromTrails } from '../model/slowOrFastByTrails';
import { validateAsFEFront, validateAsFETagPage } from '../model/validate';
-import type { Front } from '../types/front';
+import type { EditorialAbTest, Front } from '../types/front';
import type { FETagType } from '../types/tag';
import type { TagPage } from '../types/tagPage';
import { makePrefetchHeader } from './lib/header';
@@ -23,6 +23,94 @@ const enhanceFront = (body: unknown): Front => {
const data: FEFront = validateAsFEFront(body);
const serverTime = Date.now();
+ /**
+ * mocked for spike purposes. In reality, we'd need to extract all tests available across all trails and then flatten into an array with the correct shape.
+ */
+ const mockedEditorialAbTests: EditorialAbTest[] = [
+ {
+ testUuid: '123',
+ expiryDate: Date.now() - 10 * 60 * 1000, // ten mins ago,
+ frontsThisTestCanRunOn: ['US'],
+ hasManuallyEndedOnThisTrail: false,
+ variantMeta: [
+ {
+ id: 'A',
+ meta: {
+ headline: 'headline A',
+ },
+ },
+ {
+ id: 'B',
+ meta: {
+ headline: 'headline B',
+ },
+ },
+ ],
+ },
+ {
+ testUuid: '456',
+ expiryDate: Date.now() + 10 * 60 * 1000, // ten mins in the future,
+ frontsThisTestCanRunOn: ['UK'],
+ hasManuallyEndedOnThisTrail: false,
+ variantMeta: [
+ {
+ id: 'A',
+ meta: {
+ headline: 'An A headline',
+ },
+ },
+ {
+ id: 'B',
+ meta: {
+ headline: 'A B headline',
+ },
+ },
+ ],
+ },
+ {
+ testUuid: '789',
+ expiryDate: Date.now() + 60 * 60 * 1000, // an hour in the future,
+ frontsThisTestCanRunOn: ['US'],
+ hasManuallyEndedOnThisTrail: true,
+ variantMeta: [
+ {
+ id: 'A',
+ meta: {
+ headline: 'Headline from field A',
+ },
+ },
+ {
+ id: 'B',
+ meta: {
+ headline: 'Headline from field B',
+ },
+ },
+ ],
+ },
+ ];
+
+ const editorialAbTestBucket = 'B'; // lets pretend this was from data.config.serverSideABTests.editorialAbTest;
+ const cleanedEdAbTests = mockedEditorialAbTests.reduce(
+ (cleanedTests: EditorialAbTest[], test) => {
+ const { variantMeta, ...restOfTest } = test;
+
+ const newTest = {
+ ...restOfTest,
+ variantMeta: variantMeta.filter(
+ (variant) => variant.id === editorialAbTestBucket,
+ ),
+ };
+
+ return [...cleanedTests, newTest];
+ },
+ [],
+ );
+
+ const activeEditorialAbTests = cleanedEdAbTests.filter(
+ (test) =>
+ !test.hasManuallyEndedOnThisTrail &&
+ (!test.expiryDate || test.expiryDate > Date.now()),
+ );
const collections = enhanceCollections({
collections: data.pressedPage.collections,
@@ -54,6 +142,7 @@ const enhanceFront = (body: unknown): Front => {
deeplyRead: data.deeplyRead?.map((trail) => decideTrail(trail)),
canonicalUrl: data.canonicalUrl,
serverTime,
+ editorialAbTests: activeEditorialAbTests,
};
};
diff --git a/dotcom-rendering/src/types/front.ts b/dotcom-rendering/src/types/front.ts
index d69e654cd0c..63d886632c4 100644
--- a/dotcom-rendering/src/types/front.ts
+++ b/dotcom-rendering/src/types/front.ts
@@ -39,6 +39,7 @@ export interface Front {
webURL: string;
guardianBaseURL: string;
serverTime?: number;
+ editorialAbTests?: EditorialAbTest[];
}
interface PressedPage {
@@ -188,3 +189,11 @@ export type EditorialTest = {
frontsThisTestCanRunOn: string[];
hasManuallyEndedOnThisTrail: boolean;
};
+
+export type EditorialAbTest = {
+ testUuid: string;
+ expiryDate?: number;
+ variantMeta: VariantMeta[];
+ frontsThisTestCanRunOn: string[];
+ hasManuallyEndedOnThisTrail: boolean;
+};