diff --git a/ab-testing/config/lib/constants.ts b/ab-testing/config/lib/constants.ts index 7e13ce0aeaa..8b993efa57b 100644 --- a/ab-testing/config/lib/constants.ts +++ b/ab-testing/config/lib/constants.ts @@ -13,10 +13,4 @@ const MVT_COUNT = 1000; */ const MAX_SERVER_SIDE_TESTS = 20; -/** - * The spaces used for test groups, each space covers the entire mvt space allowing for concurrent overlapping tests where necessary. - * If this is increased, the fastly VCL configuration will need to be updated to match. - */ -const AUDIENCE_SPACES = ["A", "B", "C"]; - -export { MVT_COUNT, MAX_SERVER_SIDE_TESTS, AUDIENCE_SPACES }; +export { MVT_COUNT, MAX_SERVER_SIDE_TESTS }; diff --git a/ab-testing/config/lib/fastly-subfield.test.ts b/ab-testing/config/lib/fastly-subfield.test.ts index 0cb157928eb..3485e589360 100644 --- a/ab-testing/config/lib/fastly-subfield.test.ts +++ b/ab-testing/config/lib/fastly-subfield.test.ts @@ -1,6 +1,6 @@ import { deepEqual, equal, throws } from "node:assert"; import test from "node:test"; -import { AUDIENCE_SPACES } from "./constants.ts"; +import { AudienceSpaces } from "../types.ts"; import { parseFastlySubfield, parseMVTValue, @@ -136,7 +136,7 @@ test("parseMVTValue", async (t) => { "group:0=test1,group:0:type=control,group:0:exp=50,group:1=test2,group:1:type=variant,group:1:exp=25,group:2=test3,group:2:type=variant,group:2:exp=75"; const result = parseMVTValue(subfield); - equal(result.length, AUDIENCE_SPACES.length); + equal(result.length, AudienceSpaces.length); deepEqual(result[0], { name: "test1", type: "control", exp: 50 }); deepEqual(result[1], { name: "test2", type: "variant", exp: 25 }); deepEqual(result[2], { name: "test3", type: "variant", exp: 75 }); @@ -148,7 +148,7 @@ test("parseMVTValue", async (t) => { const subfield = "group:0=test1,group:1:type=variant"; const result = parseMVTValue(subfield); - equal(result.length, AUDIENCE_SPACES.length); + equal(result.length, AudienceSpaces.length); deepEqual(result[0], { name: "test1", type: "undefined", @@ -171,7 +171,7 @@ test("parseMVTValue", async (t) => { const subfield = ""; const result = parseMVTValue(subfield); - equal(result.length, AUDIENCE_SPACES.length); + equal(result.length, AudienceSpaces.length); result.forEach((item) => { deepEqual(item, { name: "undefined", @@ -277,6 +277,8 @@ test("round-trip compatibility", async (t) => { { name: "control", type: "control", exp: 50 }, { name: "variant1", type: "variant", exp: 25 }, { name: "variant2", type: "variant", exp: 25 }, + { name: "variant3", type: "variant", exp: 25 }, + { name: "variant4", type: "variant", exp: 25 }, ]; const stringified = stringifyMVTValue(original); const parsed = parseMVTValue(stringified); diff --git a/ab-testing/config/lib/fastly-subfield.ts b/ab-testing/config/lib/fastly-subfield.ts index d3873cc8a48..64156f4b2a0 100644 --- a/ab-testing/config/lib/fastly-subfield.ts +++ b/ab-testing/config/lib/fastly-subfield.ts @@ -1,4 +1,4 @@ -import { AUDIENCE_SPACES } from "./constants.ts"; +import { AudienceSpaces } from "../types.ts"; import type { FastlyTestParams } from "./types.ts"; const validateValue = (value: string | number, allowedColons: number): void => { @@ -59,7 +59,7 @@ const parseFastlySubfield = (str: string): Record => { */ const parseMVTValue = (subfield: string): FastlyTestParams[] => { const value = parseFastlySubfield(subfield); - return AUDIENCE_SPACES.map((_, i) => { + return AudienceSpaces.map((_, i) => { return { name: String(value[`group:${i}`]), type: String(value[`group:${i}:type`]), diff --git a/ab-testing/config/scripts/build/calculate-mvt-updates.ts b/ab-testing/config/scripts/build/calculate-mvt-updates.ts index d6eef02102e..c99fcd1497f 100644 --- a/ab-testing/config/scripts/build/calculate-mvt-updates.ts +++ b/ab-testing/config/scripts/build/calculate-mvt-updates.ts @@ -1,10 +1,10 @@ -import { AUDIENCE_SPACES, MVT_COUNT } from "../../lib/constants.ts"; +import { MVT_COUNT } from "../../lib/constants.ts"; import type { AllSpace, AudienceSpace, FastlyTestParams, } from "../../lib/types.ts"; -import type { ABTest } from "../../types.ts"; +import { type ABTest, AudienceSpaces } from "../../types.ts"; import { TestGroupMVTManager } from "./test-group-mvt-manager.ts"; const getTestGroupName = ( @@ -104,27 +104,25 @@ const calculateAllSpaceUpdates = ( mvtGroups: AllSpace, tests: ABTest[], ): AllSpace => { - const updatedTestSpace: AudienceSpace[] = AUDIENCE_SPACES.map( - (space, i) => { - console.log(`Calculating updates for space: ${space}`); - const spaceTests = tests.filter( - (test) => (test.audienceSpace ?? "A") === space, // 'A' is the default space - ); + const updatedTestSpace: AudienceSpace[] = AudienceSpaces.map((space, i) => { + console.log(`Calculating updates for space: ${space}`); + const spaceTests = tests.filter( + (test) => (test.audienceSpace ?? "A") === space, // 'A' is the default space + ); - if (spaceTests.length === 0) { - console.log(`No tests for space: ${space}`); - return new Map(); - } + if (spaceTests.length === 0) { + console.log(`No tests for space: ${space}`); + return new Map(); + } - const spaceMVTGroups = new Map( - mvtGroups - .entries() - .map(([key, value]) => [key, value[i] as FastlyTestParams]), - ); + const spaceMVTGroups = new Map( + mvtGroups + .entries() + .map(([key, value]) => [key, value[i] as FastlyTestParams]), + ); - return calculateSpaceUpdates(spaceMVTGroups, spaceTests); - }, - ); + return calculateSpaceUpdates(spaceMVTGroups, spaceTests); + }); return updatedTestSpace.reduce((acc, curr) => { curr.forEach((value, key) => { diff --git a/ab-testing/config/scripts/validation/enoughSpace.test.ts b/ab-testing/config/scripts/validation/enoughSpace.test.ts index a2e40b84209..d434a34ed87 100644 --- a/ab-testing/config/scripts/validation/enoughSpace.test.ts +++ b/ab-testing/config/scripts/validation/enoughSpace.test.ts @@ -1,13 +1,13 @@ import { throws } from "node:assert"; import test from "node:test"; -import type { ABTest } from "../../types.ts"; +import type { ABTest, AudienceSpaceId } from "../../types.ts"; import { enoughSpace } from "./enoughSpace.ts"; // Helper function to create a test AB test object function createABTest( name: string, audienceSize: number, - audienceSpace?: "A" | "B" | "C", + audienceSpace?: AudienceSpaceId, ): ABTest { return { name: `commercial-${name}` as const, diff --git a/ab-testing/config/types.ts b/ab-testing/config/types.ts index ae5e8d3ab25..c234d942d7a 100644 --- a/ab-testing/config/types.ts +++ b/ab-testing/config/types.ts @@ -24,6 +24,10 @@ type Year = `${number}${number}${number}${number}`; type Month = `${number}${number}`; type Day = `${number}${number}`; +const AudienceSpaces = ["A", "B", "C", "D", "E"] as const; + +type AudienceSpaceId = (typeof AudienceSpaces)[number]; + type ABTest = { /** Name of the AB test */ name: TestName; @@ -47,7 +51,7 @@ type ABTest = { * Having multiple test spaces allows deliberate overlapping of test audiences * Defaults to A */ - audienceSpace?: "A" | "B" | "C"; + audienceSpace?: AudienceSpaceId; /** Test group definition */ groups: string[]; /** @@ -69,4 +73,12 @@ type ABTest = { shouldReportToOphan?: () => boolean; }; -export type { ABTest, FastlyTestParams, AudienceSpace, AllSpace }; +export { AudienceSpaces }; + +export type { + ABTest, + FastlyTestParams, + AudienceSpace, + AllSpace, + AudienceSpaceId, +}; diff --git a/ab-testing/frontend/src/lib/components/AudienceBreakdown.svelte b/ab-testing/frontend/src/lib/components/AudienceBreakdown.svelte index 474185c0627..c93c0da6193 100644 --- a/ab-testing/frontend/src/lib/components/AudienceBreakdown.svelte +++ b/ab-testing/frontend/src/lib/components/AudienceBreakdown.svelte @@ -1,5 +1,5 @@