Repository: guardian/dotcom-rendering
Pull request #13897: [AB Testing PoC] Build ab-testing config into key-values
Author: login: Jakeii
Labels: name: Commercial :moneybag:
Created: 2025-05-01T15:55:08Z | Merged: 2025-05-22T13:37:44Z

## What does this change?
Tweaks to config discussed with the team, groups are now a list of strings and size is defined at the test level and dived by the number of variants.

~~Tests will be assigned mvt ranges based on their index in the array, the size of the test and the number of groups.~~

Tests will be assigned to mvt ranges based on their `audienceSize` and `audienceOffset`.

I've removed the `allowOverlap: true | false` in favour of an `audienceSpace: 'A' | 'B'`, which I think is more explicit about what is going on. 

For 100% tests to run concurrently with other tests, they should be assigned to a different test space. This means that some users will be in multiple tests at the same time, this is what happens at the moment for both client and server side tests, but I think this is more obvious that it is happening.

If someone were to attempt to add a test that exceeds the available mvt space they would get an error, and prompted to use another test space.



Diff:
diff --git a/.github/workflows/ab-testing.yml b/.github/workflows/ab-testing.yml
index cb217850988..2eedd2c6be3 100644
--- a/.github/workflows/ab-testing.yml
+++ b/.github/workflows/ab-testing.yml
@@ -17,7 +17,7 @@ jobs:
       # https://github.com/denoland/setup-deno#latest-stable-for-a-major
       - uses: denoland/setup-deno@v1
         with:
-          deno-version: v2.2
+          deno-version: v2.3
 
       - name: Test
         run: deno test
diff --git a/ab-testing/abTest.ts b/ab-testing/abTest.ts
index cd15b6b2b9d..4713b65b303 100644
--- a/ab-testing/abTest.ts
+++ b/ab-testing/abTest.ts
@@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space A]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space B]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
 export const ABTests: ABTest[] = [
 	// Example client side AB test definition
 	{
@@ -8,13 +27,24 @@ export const ABTests: ABTest[] = [
 			'Show new ad block ask component in ad slots when we detect ad blocker usage',
 		owners: ['commercial.dev@guardian.co.uk'],
 		status: 'ON',
-		expirationDate: new Date('2025-05-30'),
+		expirationDate: new Date('2025-06-30'),
 		type: 'client',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 5 / 100 },
-			{ id: 'variant', size: 5 / 100 },
-		],
+		audienceSize: 10 / 100,
+		groups: ['control', 'variant'],
+	},
+	{
+		name: 'commercial-ad-block-ask-4',
+		description:
+			'Show new ad block ask component in ad slots when we detect ad blocker usage',
+		owners: ['commercial.dev@guardian.co.uk'],
+		status: 'ON',
+		expirationDate: new Date('2025-06-30'),
+		type: 'client',
+		highImpact: false,
+		audienceSize: 20 / 100,
+		audienceOffset: 10,
+		groups: ['control', 'variant'],
 	},
 	// Example server side AB test definition
 	{
@@ -26,13 +56,43 @@ export const ABTests: ABTest[] = [
 			'dotcom.platform@theguardian.com',
 		],
 		status: 'ON',
-		expirationDate: new Date('2025-05-30'),
+		expirationDate: new Date('2025-06-02'),
 		type: 'server',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 50 / 100 },
-			{ id: 'variant', size: 50 / 100 },
+		audienceSize: 100 / 100,
+		groups: ['control', 'variant'],
+		audienceSpace: 'B',
+	},
+	{
+		name: 'commercial-some-test-2',
+		description:
+			'Allows viewing the beta version of the Europe network front',
+		owners: [
+			'project.fairground@theguardian.com',
+			'dotcom.platform@theguardian.com',
 		],
-		allowOverlap: true,
+		status: 'ON',
+		expirationDate: new Date('2025-06-02'),
+		type: 'server',
+		highImpact: false,
+		audienceSize: 10 / 100,
+		audienceOffset: 30,
+		groups: ['control', 'variant'],
+	},
+	{
+		name: 'commercial-some-test',
+		description:
+			'Allows viewing the beta version of the Europe network front',
+		owners: [
+			'project.fairground@theguardian.com',
+			'dotcom.platform@theguardian.com',
+		],
+		status: 'ON',
+		expirationDate: new Date('2025-06-02'),
+		type: 'server',
+		highImpact: false,
+		audienceSize: 10 / 100,
+		audienceOffset: 40,
+		groups: ['control', 'variant'],
 	},
 ];
diff --git a/ab-testing/deno.json b/ab-testing/deno.json
index c3fe0c1aeeb..1b2e303415e 100644
--- a/ab-testing/deno.json
+++ b/ab-testing/deno.json
@@ -1,7 +1,8 @@
 {
 	"tasks": {
 		"validate": "deno run scripts/validation/index.ts",
-		"upload": "deno run --allow-read=dist --allow-env=FASTLY_API_TOKEN,SERVICE_ID,MVT_DICTIONARY_ID,AB_TESTS_DICTIONARY_ID --allow-net=api.fastly.com:443 scripts/deploy/index.ts --mvts=dist/mvts.json --ab-tests=dist/ab-tests.json"
+		"upload": "deno run --allow-read=dist --allow-env=FASTLY_API_TOKEN,SERVICE_ID,MVT_DICTIONARY_ID,AB_TESTS_DICTIONARY_ID --allow-net=api.fastly.com:443 scripts/deploy/index.ts --mvts=dist/mvts.json --ab-tests=dist/ab-tests.json",
+		"build": "deno run --allow-write scripts/build/index.ts --mvts=dist/mvts.json --ab-tests=dist/ab-tests.json"
 	},
 	"imports": {
 		"@std/assert": "jsr:@std/assert@^1.0.13",
diff --git a/ab-testing/deno.lock b/ab-testing/deno.lock
index 888139e4601..672242e23e8 100644
--- a/ab-testing/deno.lock
+++ b/ab-testing/deno.lock
@@ -1,202 +1,32 @@
 {
-  "version": "4",
-  "specifiers": {
-    "jsr:@std/assert@*": "1.0.12",
-    "jsr:@std/assert@1": "1.0.12",
-    "jsr:@std/assert@^1.0.12": "1.0.12",
-    "jsr:@std/assert@^1.0.13": "1.0.13",
-    "jsr:@std/fs@*": "1.0.17",
-    "jsr:@std/internal@^1.0.6": "1.0.6",
-    "jsr:@std/path@*": "1.0.9",
-    "jsr:@std/testing@*": "1.0.11",
-    "jsr:@superstruct/core@*": "2.0.2",
-    "jsr:@superstruct/core@2.0.2": "2.0.2",
-    "npm:@types/node@*": "22.12.0"
-  },
-  "jsr": {
-    "@std/assert@1.0.12": {
-      "integrity": "08009f0926dda9cbd8bef3a35d3b6a4b964b0ab5c3e140a4e0351fbf34af5b9a",
-      "dependencies": [
-        "jsr:@std/internal"
-      ]
-    },
-    "@std/assert@1.0.13": {
-      "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
-      "dependencies": [
-        "jsr:@std/internal"
-      ]
-    },
-    "@std/fs@1.0.17": {
-      "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b"
-    },
-    "@std/internal@1.0.6": {
-      "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
-    },
-    "@std/path@1.0.9": {
-      "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
-    },
-    "@std/testing@1.0.11": {
-      "integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48",
-      "dependencies": [
-        "jsr:@std/assert@^1.0.12"
-      ]
-    },
-    "@superstruct/core@2.0.2": {
-      "integrity": "b018ea1e2c89d03280a53c1b3344e98b7cad4625522dc754b4ef2d246154ea8f"
-    }
-  },
-  "npm": {
-    "@types/node@22.12.0": {
-      "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
-      "dependencies": [
-        "undici-types"
-      ]
-    },
-    "undici-types@6.20.0": {
-      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
-    }
-  },
-  "redirects": {
-    "https://deno.land/std/fs/mod.ts": "https://deno.land/std@0.224.0/fs/mod.ts",
-    "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts",
-    "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts",
-    "https://deno.land/std/testing/mock.ts": "https://deno.land/std@0.224.0/testing/mock.ts"
-  },
-  "remote": {
-    "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
-    "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
-    "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
-    "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
-    "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
-    "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
-    "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
-    "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
-    "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
-    "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
-    "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
-    "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
-    "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
-    "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
-    "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
-    "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
-    "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
-    "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
-    "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
-    "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
-    "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
-    "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
-    "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
-    "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
-    "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
-    "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
-    "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
-    "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
-    "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
-    "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
-    "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412",
-    "https://deno.land/std@0.224.0/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd",
-    "https://deno.land/std@0.224.0/fs/_is_same_path.ts": "709c95868345fea051c58b9e96af95cff94e6ae98dfcff2b66dee0c212c4221f",
-    "https://deno.land/std@0.224.0/fs/_is_subdir.ts": "c68b309d46cc8568ed83c000f608a61bbdba0943b7524e7a30f9e450cf67eecd",
-    "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e",
-    "https://deno.land/std@0.224.0/fs/copy.ts": "7ab12a16adb65d155d4943c88081ca16ce3b0b5acada64c1ce93800653678039",
-    "https://deno.land/std@0.224.0/fs/empty_dir.ts": "e400e96e1d2c8c558a5a1712063bd43939e00619c1d1cc29959babc6f1639418",
-    "https://deno.land/std@0.224.0/fs/ensure_dir.ts": "51a6279016c65d2985f8803c848e2888e206d1b510686a509fa7cc34ce59d29f",
-    "https://deno.land/std@0.224.0/fs/ensure_file.ts": "67608cf550529f3d4aa1f8b6b36bf817bdc40b14487bf8f60e61cbf68f507cf3",
-    "https://deno.land/std@0.224.0/fs/ensure_link.ts": "5c98503ebfa9cc05e2f2efaa30e91e60b4dd5b43ebbda82f435c0a5c6e3ffa01",
-    "https://deno.land/std@0.224.0/fs/ensure_symlink.ts": "cafe904cebacb9a761977d6dbf5e3af938be946a723bb394080b9a52714fafe4",
-    "https://deno.land/std@0.224.0/fs/eol.ts": "18c4ac009d0318504c285879eb7f47942643f13619e0ff070a0edc59353306bd",
-    "https://deno.land/std@0.224.0/fs/exists.ts": "3d38cb7dcbca3cf313be343a7b8af18a87bddb4b5ca1bd2314be12d06533b50f",
-    "https://deno.land/std@0.224.0/fs/expand_glob.ts": "2e428d90acc6676b2aa7b5c78ef48f30641b13f1fe658e7976c9064fb4b05309",
-    "https://deno.land/std@0.224.0/fs/mod.ts": "c25e6802cbf27f3050f60b26b00c2d8dba1cb7fcdafe34c66006a7473b7b34d4",
-    "https://deno.land/std@0.224.0/fs/move.ts": "ca205d848908d7f217353bc5c623627b1333490b8b5d3ef4cab600a700c9bd8f",
-    "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303",
-    "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
-    "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
-    "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
-    "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
-    "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
-    "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
-    "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c",
-    "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
-    "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b",
-    "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf",
-    "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d",
-    "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
-    "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3",
-    "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607",
-    "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a",
-    "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883",
-    "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0",
-    "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15",
-    "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e",
-    "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643",
-    "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36",
-    "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c",
-    "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441",
-    "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac",
-    "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069",
-    "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972",
-    "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7",
-    "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141",
-    "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a",
-    "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0",
-    "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd",
-    "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352",
-    "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f",
-    "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a",
-    "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d",
-    "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0",
-    "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
-    "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1",
-    "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00",
-    "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2",
-    "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1",
-    "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40",
-    "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f",
-    "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede",
-    "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
-    "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63",
-    "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25",
-    "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604",
-    "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91",
-    "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6",
-    "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a",
-    "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c",
-    "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf",
-    "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf",
-    "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0",
-    "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add",
-    "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d",
-    "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b",
-    "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40",
-    "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808",
-    "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660",
-    "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
-    "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5",
-    "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9",
-    "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef",
-    "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6",
-    "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01",
-    "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8",
-    "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a",
-    "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
-    "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf",
-    "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25",
-    "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604",
-    "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780",
-    "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6",
-    "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707",
-    "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7",
-    "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
-    "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
-    "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
-    "https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c",
-    "https://deno.land/std@0.224.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db"
-  },
-  "workspace": {
-    "dependencies": [
-      "jsr:@std/assert@^1.0.13",
-      "jsr:@superstruct/core@2.0.2"
-    ]
-  }
+	"version": "5",
+	"specifiers": {
+		"jsr:@std/assert@^1.0.13": "1.0.13",
+		"jsr:@std/cli@*": "1.0.17",
+		"jsr:@std/internal@^1.0.6": "1.0.6",
+		"jsr:@superstruct/core@2.0.2": "2.0.2"
+	},
+	"jsr": {
+		"@std/assert@1.0.13": {
+			"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
+			"dependencies": [
+				"jsr:@std/internal"
+			]
+		},
+		"@std/cli@1.0.17": {
+			"integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b"
+		},
+		"@std/internal@1.0.6": {
+			"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
+		},
+		"@superstruct/core@2.0.2": {
+			"integrity": "b018ea1e2c89d03280a53c1b3344e98b7cad4625522dc754b4ef2d246154ea8f"
+		}
+	},
+	"workspace": {
+		"dependencies": [
+			"jsr:@std/assert@^1.0.13",
+			"jsr:@superstruct/core@2.0.2"
+		]
+	}
 }
diff --git a/ab-testing/docs/config-build-example.md b/ab-testing/docs/config-build-example.md
index be7a392c098..fbe08d8d6ae 100644
--- a/ab-testing/docs/config-build-example.md
+++ b/ab-testing/docs/config-build-example.md
@@ -11,10 +11,8 @@ This config:
 		expirationDate: new Date('2025-05-31'),
 		type: 'client',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 5 / 100 },
-			{ id: 'variant', size: 5 / 100 }
-		],
+		audienceSize: 10 / 100,
+		groups: ['control', 'variant'],
 	},
 	{
 		name: 'commercial-some-other-test',
@@ -25,25 +23,8 @@ This config:
 		expirationDate: new Date('2025-05-31'),
 		type: 'client',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 5 / 100 },
-			{ id: 'variant', size: 5 / 100 }
-		],
-	},
-	{
-		name: 'commercial-some-100-perc-test',
-		description:
-			'test all the things',
-		owners: ['commercial.dev@guardian.co.uk'],
-		status: 'ON',
-		expirationDate: new Date('2025-05-31'),
-		type: 'client',
-		highImpact: false,
-		groups: [
-			{ id: 'control', size: 50 / 100 },
-			{ id: 'variant', size: 50 / 100 }
-		],
-		allowOverlap: true,
+		audienceSize: 10 / 100,
+		groups: ['control', 'variant'],
 	},
 	// Example server side AB test definition
 	{
@@ -58,43 +39,41 @@ This config:
 		expirationDate: new Date('2025-04-02'),
 		type: 'server',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 50 / 100 },
-			{ id: 'variant', size: 50 / 100 }
-		],
-		allowOverlap: true,
+		audienceSize: 100 / 100,
+		groups: ['control', 'variant'],
+		audienceSpace: 'B',
 	},
 ```
 
 Could result in the following key-values to be stored in the dictionary:
 | key | value |
 | -------- | ------- |
-| mvt0 | 0=commercial-ad-block-ask:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt1 | 0=commercial-ad-block-ask:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt2 | 0=commercial-ad-block-ask:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt3 | 0=commercial-ad-block-ask:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt4 | 0=commercial-ad-block-ask:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt5 | 0=commercial-ad-block-ask:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt6 | 0=commercial-ad-block-ask:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt7 | 0=commercial-ad-block-ask:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt8 | 0=commercial-ad-block-ask:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt9 | 0=commercial-ad-block-ask:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt10 | 0=commercial-some-other-test:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt11 | 0=commercial-some-other-test:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt12 | 0=commercial-some-other-test:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt13 | 0=commercial-some-other-test:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt14 | 0=commercial-some-other-test:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt15 | 0=commercial-some-other-test:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt16 | 0=commercial-some-other-test:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt17 | 0=commercial-some-other-test:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt18 | 0=commercial-some-other-test:control,1=commercial-some-100-perc-test:control,2=fronts-curation-europe-beta-front:control |
-| mvt19 | 0=commercial-some-other-test:variant,1=commercial-some-100-perc-test:variant,2=fronts-curation-europe-beta-front:variant |
-| mvt20 | 0=commercial-some-100-perc-test:control,1=fronts-curation-europe-beta-front:control |
-| mvt21 | 0=commercial-some-100-perc-test:variant,1=fronts-curation-europe-beta-front:variant |
-| mvt22 | 0=commercial-some-100-perc-test:control,1=fronts-curation-europe-beta-front:control |
-| mvt23 | 0=commercial-some-100-perc-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt0 | 0=commercial-ad-block-ask:control,1=fronts-curation-europe-beta-front:control |
+| mvt1 | 0=commercial-ad-block-ask:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt2 | 0=commercial-ad-block-ask:control,1=fronts-curation-europe-beta-front:control |
+| mvt3 | 0=commercial-ad-block-ask:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt4 | 0=commercial-ad-block-ask:control,1=fronts-curation-europe-beta-front:control |
+| mvt5 | 0=commercial-ad-block-ask:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt6 | 0=commercial-ad-block-ask:control,1=fronts-curation-europe-beta-front:control |
+| mvt7 | 0=commercial-ad-block-ask:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt8 | 0=commercial-ad-block-ask:control,1=fronts-curation-europe-beta-front:control |
+| mvt9 | 0=commercial-ad-block-ask:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt10 | 0=commercial-some-other-test:control,1=fronts-curation-europe-beta-front:control |
+| mvt11 | 0=commercial-some-other-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt12 | 0=commercial-some-other-test:control,1=fronts-curation-europe-beta-front:control |
+| mvt13 | 0=commercial-some-other-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt14 | 0=commercial-some-other-test:control,1=fronts-curation-europe-beta-front:control |
+| mvt15 | 0=commercial-some-other-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt16 | 0=commercial-some-other-test:control,1=fronts-curation-europe-beta-front:control |
+| mvt17 | 0=commercial-some-other-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt18 | 0=commercial-some-other-test:control,1=fronts-curation-europe-beta-front:control |
+| mvt19 | 0=commercial-some-other-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt20 | 0=fronts-curation-europe-beta-front:control |
+| mvt21 | 0=fronts-curation-europe-beta-front:variant |
+| mvt22 | 0=fronts-curation-europe-beta-front:control |
+| mvt23 | 0=fronts-curation-europe-beta-front:variant |
 | ... | |
-| mvt96 | 0=commercial-some-100-perc-test:control,1=fronts-curation-europe-beta-front:control |
-| mvt97 | 0=commercial-some-100-perc-test:variant,1=fronts-curation-europe-beta-front:variant |
-| mvt98 | 0=commercial-some-100-perc-test:control,1=fronts-curation-europe-beta-front:control |
-| mvt99 | 0=commercial-some-100-perc-test:variant,1=fronts-curation-europe-beta-front:variant |
+| mvt96 | 0=fronts-curation-europe-beta-front:control |
+| mvt97 | 0=fronts-curation-europe-beta-front:variant |
+| mvt98 | 0=fronts-curation-europe-beta-front:control |
+| mvt99 | 0=fronts-curation-europe-beta-front:variant |
diff --git a/ab-testing/scripts/build/build-ab-tests-dict.ts b/ab-testing/scripts/build/build-ab-tests-dict.ts
new file mode 100644
index 00000000000..377ce169ecb
--- /dev/null
+++ b/ab-testing/scripts/build/build-ab-tests-dict.ts
@@ -0,0 +1,13 @@
+import { ABTest } from '../../types.ts';
+import { stringify } from '../lib.ts';
+
+const buildABTestDict = (tests: ABTest[]) =>
+	tests.map((test) => ({
+		item_key: test.name,
+		item_value: stringify({
+			exp: Math.floor(test.expirationDate.getTime() / 1000),
+			type: test.type,
+		}),
+	}));
+
+export { buildABTestDict };
diff --git a/ab-testing/scripts/build/build-mvt-dict.test.ts b/ab-testing/scripts/build/build-mvt-dict.test.ts
new file mode 100644
index 00000000000..2fcef4ffa12
--- /dev/null
+++ b/ab-testing/scripts/build/build-mvt-dict.test.ts
@@ -0,0 +1,120 @@
+import { assertEquals, assertThrows } from 'jsr:@std/assert';
+import { testSpaceToMVTs, abTestsToMVTs } from './build-mvt-dict.ts';
+import { ABTest } from '../../types.ts';
+
+// Mock ABTest objects for testing
+const createTest = (
+	name: string,
+	audienceSize: number,
+	groups = ['control'],
+	audienceOffset: number = 0,
+	audienceSpace: 'A' | 'B' = 'A',
+) =>
+	({
+		name,
+		audienceSize,
+		groups,
+		audienceOffset,
+		audienceSpace,
+	}) as unknown as ABTest;
+
+Deno.test('testSpaceToMVTs - empty array', () => {
+	const result = testSpaceToMVTs([]);
+	assertEquals(result, {});
+});
+
+Deno.test('testSpaceToMVTs - single test with one group', () => {
+	const test = createTest('Test1', 0.1, ['variant']);
+	const result = testSpaceToMVTs([test]);
+
+	// Size 0.1 should create 100 entries (0.1 * 1000)
+	assertEquals(Object.keys(result).length, 100);
+	assertEquals(result[`mvt:0`], 'Test1:variant');
+	assertEquals(result[`mvt:99`], 'Test1:variant');
+});
+
+Deno.test('testSpaceToMVTs - single test with multiple groups', () => {
+	const test = createTest('Test1', 0.5, ['control', 'variant']); // 2 groups
+	const result = testSpaceToMVTs([test]);
+
+	// Should have 500 entries
+	assertEquals(Object.keys(result).length, 500);
+	assertEquals(result['mvt:0'], 'Test1:control');
+	assertEquals(result['mvt:249'], 'Test1:control');
+	assertEquals(result['mvt:250'], 'Test1:variant');
+	assertEquals(result['mvt:499'], 'Test1:variant');
+});
+
+Deno.test('testSpaceToMVTs - multiple tests', () => {
+	const test1 = createTest('Test1', 0.2, ['control', 'variant']);
+	const test2 = createTest('Test2', 0.2, ['control', 'variant'], 0.2);
+
+	const result = testSpaceToMVTs([test1, test2]);
+
+	// Should have 200 + 200 = 400 entries
+	assertEquals(Object.keys(result).length, 400);
+	assertEquals(result['mvt:0'], 'Test1:control');
+	assertEquals(result['mvt:99'], 'Test1:control');
+	assertEquals(result['mvt:100'], 'Test1:variant');
+	assertEquals(result['mvt:199'], 'Test1:variant');
+	assertEquals(result['mvt:200'], 'Test2:control');
+	assertEquals(result['mvt:299'], 'Test2:control');
+	assertEquals(result['mvt:300'], 'Test2:variant');
+	assertEquals(result['mvt:399'], 'Test2:variant');
+});
+
+Deno.test('abTestsToMVTs - empty array', () => {
+	const result = abTestsToMVTs([]);
+	assertEquals(result, {});
+});
+
+Deno.test('abTestsToMVTs - only normal tests', () => {
+	const test = createTest('Test1', 0.001, ['control']); // 1 entry
+
+	const result = abTestsToMVTs([test]);
+
+	assertEquals(Object.keys(result).length, 1);
+	assertEquals(result['mvt:0'][0], 'Test1:control');
+});
+
+Deno.test('abTestsToMVTs - overlapping tests added to existing slots', () => {
+	// Create 1000 normal test entries to fill all slots
+	const normalTest = createTest('Normal', 1, ['control']); // 1000 entries
+
+	// Create overlapping test, the entries should be added to the first slots
+	const overlapTest = createTest('Overlap', 1, ['variant'], 0, 'B'); // 1000 entries
+
+	const result = abTestsToMVTs([normalTest, overlapTest]);
+
+	// First slot should now have 2 entries
+	assertEquals(result['mvt:0'].length, 2);
+	assertEquals(result['mvt:0'][0], 'Normal:control');
+	assertEquals(result['mvt:0'][1], 'Overlap:variant');
+
+	// Second slot should also have 2 entries
+	assertEquals(result['mvt:1'].length, 2);
+	assertEquals(result['mvt:1'][0], 'Normal:control');
+	assertEquals(result['mvt:1'][1], 'Overlap:variant');
+
+	assertEquals(result['mvt:999'].length, 2);
+	assertEquals(result['mvt:999'][0], 'Normal:control');
+	assertEquals(result['mvt:999'][1], 'Overlap:variant');
+});
+
+Deno.test('abTestsToMVTs - throw error when exceeding capacity', () => {
+	// Create 1000 normal test entries
+	const normalTest = createTest('Normal', 1, ['control']); // 1000 entries
+
+	// Create 1001 secondary test entries (exceeds 1000)
+	const overlapTest1 = createTest('Overlap1', 1, ['variant1'], 0, 'B');
+	const overlapTest2 = createTest('Overlap2', 0.001, ['variant2'], 1, 'B');
+
+	// We expect an error when all mvt slots have 2 groups
+	assertThrows(
+		() => {
+			abTestsToMVTs([normalTest, overlapTest1, overlapTest2]);
+		},
+		Error,
+		'Test space 1 test sizes add up to > 100%.',
+	);
+});
diff --git a/ab-testing/scripts/build/build-mvt-dict.ts b/ab-testing/scripts/build/build-mvt-dict.ts
new file mode 100644
index 00000000000..ab95d92410c
--- /dev/null
+++ b/ab-testing/scripts/build/build-mvt-dict.ts
@@ -0,0 +1,106 @@
+import { ABTest } from '../../types.ts';
+import { stringify } from '../lib.ts';
+
+const MAX_MVT_GROUPS = 1000;
+
+type MVTGroups = Record<string, string[]>;
+
+/**
+ * Convert an ab test space to mvt key values
+ *
+ * example output:
+ * mvt:0 - Test1:control
+ * mvt:1 - Test1:control
+ * mvt:2 - Test1:variant
+ * mvt:3 - Test1:variant
+ */
+const testSpaceToMVTs = (tests: ABTest[]): Record<string, string> =>
+	tests
+		.sort((a, b) => a.audienceOffset ?? 0 - (b.audienceOffset ?? 0))
+		.reduce<Record<string, string>>((acc, test) => {
+			const testOffset = (test.audienceOffset ?? 0) * MAX_MVT_GROUPS;
+			const groups = test.groups.reduce<Record<string, string>>(
+				(groupAcc, group, groupIndex) => {
+					const groupSize = Math.floor(
+						(test.audienceSize / test.groups.length) *
+							MAX_MVT_GROUPS,
+					);
+					const groupOffset = testOffset + groupSize * groupIndex;
+
+					const groupMVTs = Array.from(
+						{
+							length: groupSize,
+						},
+						(_, i) => [
+							`mvt:${i + groupOffset}`,
+							`${test.name}:${group}`,
+						],
+					);
+
+					return {
+						...groupAcc,
+						...Object.fromEntries(groupMVTs),
+					};
+				},
+				{},
+			);
+
+			return {
+				...acc,
+				...groups,
+			};
+		}, {});
+
+/**
+ * Convert an array of ab tests to mvt key values
+ * @param abTests - Array of ABTest objects
+ * @returns MVTGroups - Object with mvt keys and their corresponding test groups
+ */
+const abTestsToMVTs = (abTests: ABTest[]): MVTGroups => {
+	const primaryTests = abTests.filter(
+		(test) =>
+			test.audienceSpace === undefined || test.audienceSpace === 'A',
+	);
+	const secondaryTests = abTests.filter((test) => test.audienceSpace === 'B');
+
+	const primaryMVTGroups = testSpaceToMVTs(primaryTests);
+	const secondaryMVTGroups = testSpaceToMVTs(secondaryTests);
+
+	if (Object.keys(primaryMVTGroups).length > MAX_MVT_GROUPS) {
+		throw new Error(
+			`Test space 0 test sizes add up to > 100%, try moving the test to another space.`,
+		);
+	}
+
+	if (Object.keys(secondaryMVTGroups).length > MAX_MVT_GROUPS) {
+		throw new Error(`Test space 1 test sizes add up to > 100%.`);
+	}
+
+	const result: MVTGroups = {};
+
+	// Process primary tests first
+	Object.entries(primaryMVTGroups).forEach(([mvtKey, testValue]) => {
+		result[mvtKey] = [testValue];
+	});
+
+	// Add secondary tests to existing slots
+	Object.entries(secondaryMVTGroups).forEach(([mvtKey, testValue]) => {
+		if (result[mvtKey]) {
+			result[mvtKey].push(testValue);
+		} else {
+			result[mvtKey] = [testValue];
+		}
+	});
+
+	return result;
+};
+
+const buildMVTDict = (tests: ABTest[]) => {
+	const mvtKVs = abTestsToMVTs(tests);
+	return Object.entries(mvtKVs).map(([key, value]) => ({
+		item_key: key,
+		item_value: stringify(value),
+	}));
+};
+
+export { buildMVTDict, testSpaceToMVTs, abTestsToMVTs };
diff --git a/ab-testing/scripts/build/index.ts b/ab-testing/scripts/build/index.ts
new file mode 100644
index 00000000000..ec766347d8c
--- /dev/null
+++ b/ab-testing/scripts/build/index.ts
@@ -0,0 +1,28 @@
+import { ABTests } from '../../abTest.ts';
+import { buildABTestDict } from './build-ab-tests-dict.ts';
+import { buildMVTDict } from './build-mvt-dict.ts';
+import { parseArgs } from 'jsr:@std/cli/parse-args';
+
+const flags = parseArgs(Deno.args, {
+	string: ['mvts', 'ab-tests'],
+});
+
+if (!flags['mvts'] || !flags['ab-tests']) {
+	console.error(
+		'Please provide the path to the mvt and ab test groups dictionaries',
+	);
+	Deno.exit(1);
+}
+
+const abTestDict = buildABTestDict(ABTests);
+
+const mvtDict = buildMVTDict(ABTests);
+
+// write the abTestDictArray to a file
+await Deno.writeTextFile(
+	flags['ab-tests'],
+	JSON.stringify(abTestDict, null, 2),
+);
+
+// write the mvtKVsArray to a file
+await Deno.writeTextFile(flags['mvts'], JSON.stringify(mvtDict, null, 2));
diff --git a/ab-testing/scripts/lib.ts b/ab-testing/scripts/lib.ts
new file mode 100644
index 00000000000..95c4f1c8d62
--- /dev/null
+++ b/ab-testing/scripts/lib.ts
@@ -0,0 +1,6 @@
+const stringify = (obj: Record<string, string | number> | Array<string>) =>
+	Object.entries(obj)
+		.map(([key, value]) => `${key}=${value}`)
+		.join(',');
+
+export { stringify };
diff --git a/ab-testing/scripts/validation/limitServerSide.test.ts b/ab-testing/scripts/validation/limitServerSide.test.ts
index 40f0d67c0ca..9301075636c 100644
--- a/ab-testing/scripts/validation/limitServerSide.test.ts
+++ b/ab-testing/scripts/validation/limitServerSide.test.ts
@@ -16,10 +16,8 @@ Deno.test(
 			expirationDate: new Date(),
 			type: 'server',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 5 / 100 },
-				{ id: 'variant', size: 5 / 100 },
-			],
+			audienceSize: 10 / 100,
+			groups: ['control', 'variant'],
 		};
 		assertThrows(() => {
 			limitServerSideTests(
@@ -42,10 +40,8 @@ Deno.test(
 			expirationDate: new Date(),
 			type: 'server',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 5 / 100 },
-				{ id: 'variant', size: 5 / 100 },
-			],
+			audienceSize: 10 / 100,
+			groups: ['control', 'variant'],
 		};
 		assertEquals(
 			limitServerSideTests(
diff --git a/ab-testing/scripts/validation/validExpiration.test.ts b/ab-testing/scripts/validation/validExpiration.test.ts
index 27c053e70af..fb4e04b52ca 100644
--- a/ab-testing/scripts/validation/validExpiration.test.ts
+++ b/ab-testing/scripts/validation/validExpiration.test.ts
@@ -46,10 +46,8 @@ Deno.test(
 			expirationDate: futureWeekday(),
 			type: 'client',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 5 / 100 },
-				{ id: 'variant', size: 5 / 100 },
-			],
+			audienceSize: 10 / 100,
+			groups: ['control', 'variant'],
 		};
 
 		assertEquals(allExpirationsValid([futureWeekdayTest]), true);
@@ -67,10 +65,8 @@ Deno.test(
 			expirationDate: futureWeekend(),
 			type: 'client',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 5 / 100 },
-				{ id: 'variant', size: 5 / 100 },
-			],
+			audienceSize: 10 / 100,
+			groups: ['control', 'variant'],
 		};
 
 		assertThrows(() => allExpirationsValid([futureWeekendTes]));
@@ -88,10 +84,8 @@ Deno.test(
 			expirationDate: pastDay(),
 			type: 'client',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 5 / 100 },
-				{ id: 'variant', size: 5 / 100 },
-			],
+			audienceSize: 10 / 100,
+			groups: ['control', 'variant'],
 		};
 
 		assertThrows(() => allExpirationsValid([pastTest]));
diff --git a/ab-testing/scripts/validation/variantOverlap.test.ts b/ab-testing/scripts/validation/variantOverlap.test.ts
index 1cda18c566c..3563216fc2e 100644
--- a/ab-testing/scripts/validation/variantOverlap.test.ts
+++ b/ab-testing/scripts/validation/variantOverlap.test.ts
@@ -11,19 +11,45 @@ Deno.test('noVariantOverlap - disallows variant overlap', () => {
 		expirationDate: new Date(),
 		type: 'client',
 		highImpact: false,
-		groups: [
-			{ id: 'control', size: 50 / 100 },
-			{ id: 'variant', size: 50 / 100 },
-		],
+		audienceSize: 100 / 100,
+		groups: ['control', 'variant'],
 	};
 
 	assertThrows(() => noVariantOverlap([overlapTest, overlapTest]));
 });
 
+Deno.test('noVariantOverlap - disallows partial overlap', () => {
+	const overlapTest1: ABTest = {
+		name: 'commercial-big-overlap',
+		description: 'Overlap the variants',
+		owners: ['commercial.dev@guardian.co.uk'],
+		status: 'ON',
+		expirationDate: new Date(),
+		type: 'client',
+		highImpact: false,
+		audienceSize: 50 / 100,
+		groups: ['control', 'variant'],
+	};
+
+	const overlapTest2: ABTest = {
+		name: 'commercial-big-overlap',
+		description: 'Overlap the variants',
+		owners: ['commercial.dev@guardian.co.uk'],
+		status: 'ON',
+		expirationDate: new Date(),
+		type: 'client',
+		highImpact: false,
+		audienceSize: 50 / 100,
+		audienceOffset: 0.25,
+		groups: ['control', 'variant'],
+	};
+	assertThrows(() => noVariantOverlap([overlapTest1, overlapTest2]));
+});
+
 Deno.test(
-	'noVariantOverlap - allows variant overlap with the allowOverlap setting',
+	'noVariantOverlap - allows variant overlap with secondary test space',
 	() => {
-		const overlapTest: ABTest = {
+		const primaryTest: ABTest = {
 			name: 'commercial-big-overlap',
 			description: 'Overlap the variants',
 			owners: ['commercial.dev@guardian.co.uk'],
@@ -31,31 +57,23 @@ Deno.test(
 			expirationDate: new Date(),
 			type: 'client',
 			highImpact: false,
-			groups: [
-				{ id: 'control', size: 50 / 100 },
-				{ id: 'variant', size: 50 / 100 },
-			],
-			allowOverlap: true,
+			audienceSize: 100 / 100,
+			groups: ['control', 'variant'],
+			audienceSpace: 'B',
 		};
 
-		assertEquals(noVariantOverlap([overlapTest, overlapTest]), true);
+		const secondaryTest: ABTest = {
+			name: 'commercial-big-overlap',
+			description: 'Overlap the variants',
+			owners: ['commercial.dev@guardian.co.uk'],
+			status: 'ON',
+			expirationDate: new Date(),
+			type: 'client',
+			highImpact: false,
+			audienceSize: 100 / 100,
+			groups: ['control', 'variant'],
+		};
+
+		assertEquals(noVariantOverlap([primaryTest, secondaryTest]), true);
 	},
 );
-
-Deno.test('noVariantOverlap - allows tests with no variant overlap', () => {
-	const overlapTest: ABTest = {
-		name: 'commercial-no-overlap',
-		description: 'Do not overlap the variants',
-		owners: ['commercial.dev@guardian.co.uk'],
-		status: 'ON',
-		expirationDate: new Date(),
-		type: 'client',
-		highImpact: false,
-		groups: [
-			{ id: 'control', size: 5 / 100 },
-			{ id: 'variant', size: 5 / 100 },
-		],
-	};
-
-	assertEquals(noVariantOverlap([overlapTest, overlapTest]), true);
-});
diff --git a/ab-testing/scripts/validation/variantOverlap.ts b/ab-testing/scripts/validation/variantOverlap.ts
index 4910e454145..dcd576af3bf 100644
--- a/ab-testing/scripts/validation/variantOverlap.ts
+++ b/ab-testing/scripts/validation/variantOverlap.ts
@@ -1,26 +1,43 @@
 import { ABTest } from '../../types.ts';
 
-export function noVariantOverlap(tests: ABTest[]) {
-	const allTestPercentages = tests.reduce<number>((percentageSum, test) => {
-		// Skip tests with allowed overlap
-		if (test.allowOverlap) {
-			return percentageSum;
-		}
-		const newTotal = test.groups.reduce<number>((total, group) => {
-			return total + group.size;
-		}, percentageSum);
+interface TestRange {
+	name: string;
+	start: number;
+	end: number;
+}
 
-		if (newTotal > 1) {
-			throw new Error(
-				`Test variant space exceeded when checking ${test.name}`,
+export const findOverlappingTests = (tests: ABTest[]) => {
+	return tests.reduce<TestRange[]>(
+		(acc, { name, audienceOffset, audienceSize }) => {
+			const start = audienceOffset ?? 0;
+			const end = (audienceOffset ?? 0) + audienceSize;
+			const overlap = acc.find(
+				({ start: startAcc, end: endAcc }) =>
+					(startAcc <= start && endAcc >= start) ||
+					(startAcc <= end && endAcc >= end) ||
+					(startAcc >= start && endAcc <= end),
 			);
-		}
+			if (overlap) {
+				throw new Error(`Test ${name} overlaps with ${overlap.name}`);
+			}
+			return [...acc, { name, start, end }];
+		},
+		[],
+	);
+};
+export function noVariantOverlap(tests: ABTest[]) {
+	const allPrimaryTests = tests.filter(
+		(test) =>
+			test.audienceSpace === undefined || test.audienceSpace === 'A',
+	);
+
+	findOverlappingTests(allPrimaryTests);
+
+	const allSecordaryTests = tests.filter(
+		(test) => test.audienceSpace === 'B',
+	);
 
-		return newTotal;
-	}, 0);
+	findOverlappingTests(allSecordaryTests);
 
-	if (allTestPercentages <= 1) {
-		return true;
-	}
-	throw new Error(`Test variants exceed 100%`);
+	return true;
 }
diff --git a/ab-testing/types.ts b/ab-testing/types.ts
index ff1e401a4a8..eb9d64e4e06 100644
--- a/ab-testing/types.ts
+++ b/ab-testing/types.ts
@@ -2,12 +2,6 @@ type Team = 'commercial' | 'webex';
 
 type TestName = `${Team}-${string}`;
 
-type TestGroup = {
-	id: String;
-	/** Percentage of users in this group */
-	size: number;
-};
-
 export type ABTest = {
 	/** Name of the AB test */
 	name: TestName;
@@ -25,8 +19,16 @@ export type ABTest = {
 	 * Would be nice to know who changed the status last and when
 	 */
 	status: 'ON' | 'OFF';
+	/** The size of the test, all variants will be divided equally to fit */
+	audienceSize: number;
+	/** The offset of the test, adjust to ensure that tests do not overlap */
+	audienceOffset?: number;
+	/**
+	 * Each test space represents 100% of the audience
+	 * Having multiple test spaces allows deliberate overlapping of test audiences
+	 * Defaults to A
+	 */
+	audienceSpace?: 'A' | 'B';
 	/** Test group definition */
-	groups: Array<TestGroup>;
-	/** Can this test overlap other tests? Required for 100% tests */
-	allowOverlap?: boolean;
+	groups: string[];
 };


Reviews:
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:18:31Z
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:28:44Z
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:44:28Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T14:51:02Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T14:52:03Z
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:30:45Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T14:20:59Z
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:11:59Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:08:33Z
user: login: GHaberis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:17:53Z
user: login: cemms1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:14:57Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T13:19:48Z
user: login: Jakeii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T13:20:53Z

Review comments:
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: GHaberis
body: Does this mean a side effect of changing the `ABTests` array is we potentially change the index of a test and therefore it’s MVT range? Inadvertently moving users in and out of a running tests? 
created_at: 2025-05-08T13:18:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
path: ab-testing/abTest.ts
user: login: GHaberis
body: So the test spaces allow us to reuse MVT ranges for multiple tests running concurrently, and we're capping to 2 concurrent tests for now by only offering 2 test spaces: 0 and 1?
created_at: 2025-05-08T13:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+import { assertEquals, assertThrows } from 'jsr:@std/assert';
+import { testsToArray, abTestsToMVTs } from './build-mvt-dict.ts';
+import { ABTest } from '../../types.ts';
+
+// Mock ABTest objects for testing
+const createTest = (
+	name: string,
+	size: number,
+	groups = ['control'],
+	testSpace: 0 | 1 = 0,
+) =>
+	({
+		name,
+		size,
+		groups,
+		testSpace,
+	}) as ABTest;
+
+Deno.test('testsToArray - empty array', () => {
+	const result = testsToArray([]);
+	assertEquals(result, []);
+});
+
+Deno.test('testsToArray - single test with one group', () => {
+	const test = createTest('Test1', 0.1, ['variant']);
+	const result = testsToArray([test]);
+
+	// Size 0.1 should create 100 entries (0.1 * 1000)
+	assertEquals(result.length, 100);
+	assertEquals(result[0], 'Test1:variant');
+	assertEquals(result[99], 'Test1:variant');
+});
+
+Deno.test('testsToArray - single test with multiple groups', () => {
+	const test = createTest('Test1', 0.5, ['control', 'variant']); // 2 groups
+	const result = testsToArray([test]);
+
+	// Should have 500 entries
+	assertEquals(result.length, 500);
+	assertEquals(result[0], 'Test1:control');
+	assertEquals(result[249], 'Test1:control');
+	assertEquals(result[250], 'Test1:variant');
+	assertEquals(result[499], 'Test1:variant');
+});
+
+Deno.test('testsToArray - multiple tests', () => {
+	const test1 = createTest('Test1', 0.2, ['control', 'variant']);
+	const test2 = createTest('Test2', 0.2, ['control', 'variant'], 1);
+
+	const result = testsToArray([test1, test2]);
+
+	// Should have 300 + 200 = 500 entries
path: ab-testing/scripts/build/build-mvt-dict.test.ts
user: login: GHaberis
body: I think this comment is out of date, it should be 200 + 200 = 400 entries?
created_at: 2025-05-08T13:44:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: Jakeii
body: Yeah good point, perhaps we should have the tests pick a percentage range to use?
e.g. a 10% test would have something like:
```
audienceRange: [0, 10],
```

or maybe we need to keep the `audienceOffset`.
created_at: 2025-05-08T14:51:02Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
path: ab-testing/abTest.ts
user: login: Jakeii
body: Yes we decided we wouldn't need more than 2 overlapping test "spaces" but we could increase if needed.
created_at: 2025-05-08T14:52:03Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: GHaberis
body: I think the suggestion of defining the `audienceRange` is a good solution, it fixes the issue I described and I think it helps to have the audience range visible in config. 

Putting the `audienceRange` in config does introduce the possibility of 2 tests targeting the same range, would we want validate to prevent this? 

I'm actually thinking that we're introducing a mechanism for setting up multiple tests targeting the same range with the "spaces" logic, maybe this spaces logic becomes redundant if we have `audienceRange`  as a mechanism for setting up overlaps if we want then. We'd lose the ability to limit overlapping tests to 2 if we did this (without extra validation), but I'm not sure how important that restriction is?
created_at: 2025-05-08T15:30:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: Jakeii
body: We do need to have a hard limit on overlapping as VCL code requires us to check for a fixed number of tests using the same mvtid.

I do think we should make creating a test that overlaps other tests very obvious that it does so.

I'm not 100% sure the `testSpaces` concept is the best way though!
created_at: 2025-05-09T14:20:58Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: GHaberis
body: OK, good to know that the restriction on overlapping tests in a audience range is necessary, given this we'd want to keep validation in place. The test spaces could still be useful, eg. during validation, however we'd use them as an implementation detail rather than part of the public API on a A/B test definition, which might mean less of a refactor?
created_at: 2025-05-12T13:11:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: Jakeii
body: I just tried swapping to something like `audienceRange` and it's a bit clunky, and likely to cause mistakes with test sizing/overlapping.
I think the `offset` that client side tests have is a bit less clunky and probably the the best for now?
created_at: 2025-05-13T16:08:33Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 import { ABTest } from './types.ts';
 
+/**
+ * Tests are defined here. They will be assigned mvt ranges based on their index in the array, the
+ * size of the test and the number of groups.
+ *
+ * For 100% tests to run concurrently with other tests, they should be assigned to different
+ * test spaces. This means that some users will be in multiple tests at the same time.
+ *
+ * Example:
+ * [Space 0]
+ * - 20% Test	control		MVT 0-99
+ * - 20% Test	variant		MVT 100-199
+ * - 50% Test	control		MVT 200-449
+ * - 50% Test	variant		MVT 450-699
+ *
+ * [Space 1]
+ * - 100% Test	control		MVT 0-499
+ * - 100% Test	variant		MVT 500-999
+ */
+
path: ab-testing/abTest.ts
user: login: GHaberis
body: @Jakeii ok good to know, an `offset` prop alongside an `audienceSize` prop is easy enough to understand 👍 
created_at: 2025-05-14T09:17:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,8 +19,14 @@ export type ABTest = {
 	 * Would be nice to know who changed the status last and when
 	 */
 	status: 'ON' | 'OFF';
+	/** The size of the test, all variants will be divided equally to fit */
+	audienceSize: number;
+	/** The offset of the test, adjust to ensure that tests do not overlap */
+	audienceOffset?: number;
+	/**
+	 * Defaults to 0, all the tests in a test space must add up to 100%
+	 **/
+	audienceSpace?: 0 | 1;
path: ab-testing/types.ts
user: login: cemms1
body: I think 'A' or 'B' might be easier to understand at first glance?
created_at: 2025-05-21T09:39:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,8 +19,14 @@ export type ABTest = {
 	 * Would be nice to know who changed the status last and when
 	 */
 	status: 'ON' | 'OFF';
+	/** The size of the test, all variants will be divided equally to fit */
+	audienceSize: number;
+	/** The offset of the test, adjust to ensure that tests do not overlap */
+	audienceOffset?: number;
+	/**
+	 * Defaults to 0, all the tests in a test space must add up to 100%
+	 **/
path: ab-testing/types.ts
user: login: cemms1
body: ```suggestion
	/**
	 * Each test space represents 100% of the audience 
	 * Having multiple test spaces allows deliberate overlapping of test audiences
	 * Defaults to 0
	 */
```

Do the tests _need_ to total 100% or is this the maximum value?
created_at: 2025-05-21T09:42:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,105 @@
+import { ABTest } from '../../types.ts';
+import { stringify } from '../lib.ts';
+
+const MAX_MVT_GROUPS = 1000;
+
+type MVTGroups = Record<string, string[]>;
+
+/**
+ * Convert an ab test space to mvt key values
+ *
+ * example output:
+ * mvt:0 - Test1:control
+ * mvt:1 - Test1:control
+ * mvt:2 - Test1:variant
+ * mvt:3 - Test1:variant
+ */
+const testSpaceToMVTs = (tests: ABTest[]): Record<string, string> =>
+	tests
+		.sort((a, b) => a.audienceOffset ?? 0 - (b.audienceOffset ?? 0))
path: ab-testing/scripts/build/build-mvt-dict.ts
user: login: cemms1
body: _Nitpick_ (sorry, not essential)
```suggestion
		.sort((a, b) => (a.audienceOffset ?? 0) - (b.audienceOffset ?? 0))
```
created_at: 2025-05-21T14:42:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+import { assertEquals, assertThrows } from 'jsr:@std/assert';
+import { testSpaceToMVTs, abTestsToMVTs } from './build-mvt-dict.ts';
+import { ABTest } from '../../types.ts';
+
+// Mock ABTest objects for testing
+const createTest = (
+	name: string,
+	audienceSize: number,
+	groups = ['control'],
+	audienceOffset: number = 0,
+	audienceSpace: 0 | 1 = 0,
+) =>
+	({
+		name,
+		audienceSize,
+		groups,
+		audienceOffset,
+		audienceSpace,
+	}) as unknown as ABTest;
+
+Deno.test('testSpaceToMVTs - empty array', () => {
+	const result = testSpaceToMVTs([]);
+	assertEquals(result, {});
+});
+
+Deno.test('testSpaceToMVTs - single test with one group', () => {
+	const test = createTest('Test1', 0.1, ['variant']);
+	const result = testSpaceToMVTs([test]);
+
+	// Size 0.1 should create 100 entries (0.1 * 1000)
+	assertEquals(Object.keys(result).length, 100);
+	assertEquals(result[`mvt:0`], 'Test1:variant');
+	assertEquals(result[`mvt:99`], 'Test1:variant');
+});
+
+Deno.test('testSpaceToMVTs - single test with multiple groups', () => {
+	const test = createTest('Test1', 0.5, ['control', 'variant']); // 2 groups
+	const result = testSpaceToMVTs([test]);
+
+	// Should have 500 entries
+	assertEquals(Object.keys(result).length, 500);
+	assertEquals(result['mvt:0'], 'Test1:control');
+	assertEquals(result['mvt:249'], 'Test1:control');
+	assertEquals(result['mvt:250'], 'Test1:variant');
+	assertEquals(result['mvt:499'], 'Test1:variant');
+});
+
+Deno.test('testSpaceToMVTs - multiple tests', () => {
+	const test1 = createTest('Test1', 0.2, ['control', 'variant']);
+	const test2 = createTest('Test2', 0.2, ['control', 'variant'], 0.2);
+
+	const result = testSpaceToMVTs([test1, test2]);
+
+	// Should have 200 + 200 = 400 entries
+	assertEquals(Object.keys(result).length, 400);
+	assertEquals(result['mvt:0'], 'Test1:control');
+	assertEquals(result['mvt:99'], 'Test1:control');
+	assertEquals(result['mvt:100'], 'Test1:variant');
+	assertEquals(result['mvt:199'], 'Test1:variant');
+	assertEquals(result['mvt:200'], 'Test2:control');
+	assertEquals(result['mvt:299'], 'Test2:control');
+	assertEquals(result['mvt:300'], 'Test2:variant');
+	assertEquals(result['mvt:399'], 'Test2:variant');
+});
+
+Deno.test('abTestsToMVTs - empty array', () => {
+	const result = abTestsToMVTs([]);
+	assertEquals(result, {});
+});
+
+Deno.test('abTestsToMVTs - only normal tests', () => {
+	const test = createTest('Test1', 0.001, ['control']); // 1 entry
+
+	const result = abTestsToMVTs([test]);
+
+	assertEquals(Object.keys(result).length, 1);
+	assertEquals(result['mvt:0'][0], 'Test1:control');
+});
+
+Deno.test('abTestsToMVTs - overlapping tests added to existing slots', () => {
+	// Create 1000 normal test entries to fill all slots
+	const normalTest = createTest('Normal', 1, ['control']); // 1000 entries
+
+	// Create 2 overlapping test entries, these should be added to the first slots
path: ab-testing/scripts/build/build-mvt-dict.test.ts
user: login: cemms1
body: ```suggestion
	// Create overlapping test, the entries should be added to the first slots
```
created_at: 2025-05-21T14:59:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,8 +19,14 @@ export type ABTest = {
 	 * Would be nice to know who changed the status last and when
 	 */
 	status: 'ON' | 'OFF';
+	/** The size of the test, all variants will be divided equally to fit */
+	audienceSize: number;
+	/** The offset of the test, adjust to ensure that tests do not overlap */
+	audienceOffset?: number;
+	/**
+	 * Defaults to 0, all the tests in a test space must add up to 100%
+	 **/
+	audienceSpace?: 0 | 1;
path: ab-testing/types.ts
user: login: Jakeii
body: I like this better yeah
created_at: 2025-05-22T13:19:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+import { ABTest } from '../../types.ts';
+import { stringify } from '../lib.ts';
+
+const MAX_MVT_GROUPS = 1000;
+
+type MVTGroups = Record<string, string[]>;
+
+/**
+ * Convert an ab test space to mvt key values
+ *
+ * example output:
+ * mvt:0 - Test1:control
+ * mvt:1 - Test1:control
+ * mvt:2 - Test1:variant
+ * mvt:3 - Test1:variant
+ */
+const testSpaceToMVTs = (tests: ABTest[]): Record<string, string> =>
+	tests
+		.sort((a, b) => a.audienceOffset ?? 0 - (b.audienceOffset ?? 0))
path: ab-testing/scripts/build/build-mvt-dict.ts
user: login: Jakeii
body: eslint removes the extra brackets!😢
created_at: 2025-05-22T13:20:52Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-01T16:00:33Z
body: **Size Change:** 0 B 

**Total Size:** 1.04 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `dotcom-rendering/dist/1032.client.web.********************.js` | 3.31 kB |
| `dotcom-rendering/dist/117.client.web.********************.js` | 2.88 kB |
| `dotcom-rendering/dist/1209.client.web.********************.js` | 3.13 kB |
| `dotcom-rendering/dist/1656.client.web.********************.js` | 49.5 kB |
| `dotcom-rendering/dist/1662.client.web.********************.js` | 7.97 kB |
| `dotcom-rendering/dist/2045.client.web.********************.js` | 2.63 kB |
| `dotcom-rendering/dist/2161.client.web.********************.js` | 3.9 kB |
| `dotcom-rendering/dist/223.client.web.********************.js` | 527 B |
| `dotcom-rendering/dist/2270.client.web.********************.js` | 3.65 kB |
| `dotcom-rendering/dist/2309.client.web.********************.js` | 3.27 kB |
| `dotcom-rendering/dist/2360.client.web.********************.js` | 19.8 kB |
| `dotcom-rendering/dist/3031.client.web.********************.js` | 2.77 kB |
| `dotcom-rendering/dist/3292.client.web.********************.js` | 2.11 kB |
| `dotcom-rendering/dist/3384.client.web.********************.js` | 3.86 kB |
| `dotcom-rendering/dist/3473.client.web.********************.js` | 3.67 kB |
| `dotcom-rendering/dist/3485.client.web.********************.js` | 4.67 kB |
| `dotcom-rendering/dist/3598.client.web.********************.js` | 3.65 kB |
| `dotcom-rendering/dist/3605.client.web.********************.js` | 15.1 kB |
| `dotcom-rendering/dist/3619.client.web.********************.js` | 4.45 kB |
| `dotcom-rendering/dist/3929.client.web.********************.js` | 3.45 kB |
| `dotcom-rendering/dist/4063.client.web.********************.js` | 5.48 kB |
| `dotcom-rendering/dist/4221.client.web.********************.js` | 2.63 kB |
| `dotcom-rendering/dist/4258.client.web.********************.js` | 4.08 kB |
| `dotcom-rendering/dist/4306.client.web.********************.js` | 22.7 kB |
| `dotcom-rendering/dist/4308.client.web.********************.js` | 3.57 kB |
| `dotcom-rendering/dist/4324.client.web.********************.js` | 619 B |
| `dotcom-rendering/dist/4457.client.web.********************.js` | 6.23 kB |
| `dotcom-rendering/dist/4501.client.web.********************.js` | 4.29 kB |
| `dotcom-rendering/dist/5118.client.web.********************.js` | 5.82 kB |
| `dotcom-rendering/dist/5140.client.web.********************.js` | 5.42 kB |
| `dotcom-rendering/dist/5144.client.web.********************.js` | 2.55 kB |
| `dotcom-rendering/dist/5448.client.web.********************.js` | 4.37 kB |
| `dotcom-rendering/dist/545.client.web.********************.js` | 531 B |
| `dotcom-rendering/dist/562.client.web.********************.js` | 11.8 kB |
| `dotcom-rendering/dist/5715.client.web.********************.js` | 3.48 kB |
| `dotcom-rendering/dist/5880.client.web.********************.js` | 3.13 kB |
| `dotcom-rendering/dist/5923.client.web.********************.js` | 2.68 kB |
| `dotcom-rendering/dist/6038.client.web.********************.js` | 2.76 kB |
| `dotcom-rendering/dist/6217.client.web.********************.js` | 3.75 kB |
| `dotcom-rendering/dist/6238.client.web.********************.js` | 3.1 kB |
| `dotcom-rendering/dist/6262.client.web.********************.js` | 11.8 kB |
| `dotcom-rendering/dist/6501.client.web.********************.js` | 65.6 kB |
| `dotcom-rendering/dist/6555.client.web.********************.js` | 5.45 kB |
| `dotcom-rendering/dist/6562.client.web.********************.js` | 439 B |
| `dotcom-rendering/dist/6722.client.web.********************.js` | 3.06 kB |
| `dotcom-rendering/dist/6750.client.web.********************.js` | 10.6 kB |
| `dotcom-rendering/dist/6873.client.web.********************.js` | 4.13 kB |
| `dotcom-rendering/dist/695.client.web.********************.js` | 4.51 kB |
| `dotcom-rendering/dist/7017.client.web.********************.js` | 157 B |
| `dotcom-rendering/dist/7116.client.web.********************.js` | 23.2 kB |
| `dotcom-rendering/dist/732.client.web.********************.js` | 3.09 kB |
| `dotcom-rendering/dist/7398.client.web.********************.js` | 3.57 kB |
| `dotcom-rendering/dist/7708.client.web.********************.js` | 5.05 kB |
| `dotcom-rendering/dist/7735.client.web.********************.js` | 16.5 kB |
| `dotcom-rendering/dist/7927.client.web.********************.js` | 2.76 kB |
| `dotcom-rendering/dist/8068.client.web.********************.js` | 2.88 kB |
| `dotcom-rendering/dist/808.client.web.********************.js` | 3.28 kB |
| `dotcom-rendering/dist/8151.client.web.********************.js` | 4.17 kB |
| `dotcom-rendering/dist/8212.client.web.********************.js` | 5.04 kB |
| `dotcom-rendering/dist/8348.client.web.********************.js` | 20.5 kB |
| `dotcom-rendering/dist/8385.client.web.********************.js` | 3.88 kB |
| `dotcom-rendering/dist/8436.client.web.********************.js` | 3.63 kB |
| `dotcom-rendering/dist/8602.client.web.********************.js` | 3.39 kB |
| `dotcom-rendering/dist/8610.client.web.********************.js` | 3.9 kB |
| `dotcom-rendering/dist/8619.client.web.********************.js` | 4.44 kB |
| `dotcom-rendering/dist/864.client.web.********************.js` | 3.17 kB |
| `dotcom-rendering/dist/8942.client.web.********************.js` | 4.44 kB |
| `dotcom-rendering/dist/9120.client.web.********************.js` | 2.89 kB |
| `dotcom-rendering/dist/9135.client.web.********************.js` | 3.54 kB |
| `dotcom-rendering/dist/9376.client.web.********************.js` | 2.52 kB |
| `dotcom-rendering/dist/967.client.web.********************.js` | 2.87 kB |
| `dotcom-rendering/dist/9822.client.web.********************.js` | 7.67 kB |
| `dotcom-rendering/dist/9870.client.web.********************.js` | 4.77 kB |
| `dotcom-rendering/dist/Accessibility-importable.client.web.********************.js` | 8.42 kB |
| `dotcom-rendering/dist/AdBlockAsk-importable.client.web.********************.js` | 2.99 kB |
| `dotcom-rendering/dist/AdPortals-importable.client.web.********************.js` | 4.76 kB |
| `dotcom-rendering/dist/AlreadyVisited-importable.client.web.********************.js` | 424 B |
| `dotcom-rendering/dist/AppsEpic-importable.client.web.********************.js` | 3.61 kB |
| `dotcom-rendering/dist/AppsFooter-importable.client.web.********************.js` | 2.69 kB |
| `dotcom-rendering/dist/AppsLightboxImage-importable.client.web.********************.js` | 2.66 kB |
| `dotcom-rendering/dist/AppsLightboxImageStore-importable.client.web.********************.js` | 2.6 kB |
| `dotcom-rendering/dist/AudioAtomWrapper-importable.client.web.********************.js` | 2.76 kB |
| `dotcom-rendering/dist/AudioPlayerWrapper-importable.client.web.********************.js` | 6.61 kB |
| `dotcom-rendering/dist/AustralianTerritorySwitcher-importable.client.web.********************.js` | 4.61 kB |
| `dotcom-rendering/dist/Branding-importable.client.web.********************.js` | 2.89 kB |
| `dotcom-rendering/dist/braze-web-sdk-core.client.web.********************.js` | 67.7 kB |
| `dotcom-rendering/dist/BrazeMessaging-importable.client.web.********************.js` | 1.68 kB |
| `dotcom-rendering/dist/CalloutBlockComponent-importable.client.web.********************.js` | 6.73 kB |
| `dotcom-rendering/dist/CalloutEmbedBlockComponent-importable.client.web.********************.js` | 5.77 kB |
| `dotcom-rendering/dist/CardCommentCount-importable.client.web.********************.js` | 2.67 kB |
| `dotcom-rendering/dist/Carousel-importable.client.web.********************.js` | 6.67 kB |
| `dotcom-rendering/dist/CarouselForNewsletters-importable.client.web.********************.js` | 4.57 kB |
| `dotcom-rendering/dist/ChartAtom-importable.client.web.********************.js` | 539 B |
| `dotcom-rendering/dist/CommentCount-importable.client.web.********************.js` | 2.3 kB |
| `dotcom-rendering/dist/CrosswordComponent-importable.client.web.********************.js` | 2.87 kB |
| `dotcom-rendering/dist/DiscussionApps-importable.client.web.********************.js` | 1.07 kB |
| `dotcom-rendering/dist/DiscussionMeta-importable.client.web.********************.js` | 2.4 kB |
| `dotcom-rendering/dist/DiscussionWeb-importable.client.web.********************.js` | 3.46 kB |
| `dotcom-rendering/dist/DocumentBlockComponent-importable.client.web.********************.js` | 2.85 kB |
| `dotcom-rendering/dist/Dropdown-importable.client.web.********************.js` | 1.72 kB |
| `dotcom-rendering/dist/EditionSwitcherBanner-importable.client.web.********************.js` | 4.45 kB |
| `dotcom-rendering/dist/EmbedBlockComponent-importable.client.web.********************.js` | 3.98 kB |
| `dotcom-rendering/dist/EnhancePinnedPost-importable.client.web.********************.js` | 2.02 kB |
| `dotcom-rendering/dist/FetchOnwardsData-importable.client.web.********************.js` | 1.94 kB |
| `dotcom-rendering/dist/FilterKeyEventsToggle-importable.client.web.********************.js` | 3.72 kB |
| `dotcom-rendering/dist/FocusStyles-importable.client.web.********************.js` | 618 B |
| `dotcom-rendering/dist/FollowWrapper-importable.client.web.********************.js` | 2.53 kB |
| `dotcom-rendering/dist/FootballMatchesPageWrapper-importable.client.web.********************.js` | 7.31 kB |
| `dotcom-rendering/dist/FootballTablesCompetitionSelect-importable.client.web.********************.js` | 3.26 kB |
| `dotcom-rendering/dist/FooterLabel-importable.client.web.********************.js` | 364 B |
| `dotcom-rendering/dist/FooterReaderRevenueLinks-importable.client.web.********************.js` | 3.43 kB |
| `dotcom-rendering/dist/frameworks.client.web.********************.js` | 20.9 kB |
| `dotcom-rendering/dist/FrontSubNav-importable.client.web.********************.js` | 7.51 kB |
| `dotcom-rendering/dist/GetCricketScoreboard-importable.client.web.********************.js` | 6.29 kB |
| `dotcom-rendering/dist/GetMatchNav-importable.client.web.********************.js` | 11.5 kB |
| `dotcom-rendering/dist/GetMatchStats-importable.client.web.********************.js` | 8.12 kB |
| `dotcom-rendering/dist/GetMatchTabs-importable.client.web.********************.js` | 2.58 kB |
| `dotcom-rendering/dist/guardian-braze-components-banner.client.web.********************.js` | 15.9 kB |
| `dotcom-rendering/dist/guardian-braze-components-end-of-article.client.web.********************.js` | 10.2 kB |
| `dotcom-rendering/dist/GuideAtomWrapper-importable.client.web.********************.js` | 782 B |
| `dotcom-rendering/dist/index.client.web.********************.js` | 46.3 kB |
| `dotcom-rendering/dist/InstagramBlockComponent-importable.client.web.********************.js` | 2.9 kB |
| `dotcom-rendering/dist/InteractiveAtomMessenger-importable.client.web.********************.js` | 852 B |
| `dotcom-rendering/dist/InteractiveBlockComponent-importable.client.web.********************.js` | 8.82 kB |
| `dotcom-rendering/dist/InteractiveContentsBlockComponent-importable.client.web.********************.js` | 3.78 kB |
| `dotcom-rendering/dist/KeyEventsCarousel-importable.client.web.********************.js` | 5.69 kB |
| `dotcom-rendering/dist/KnowledgeQuizAtom-importable.client.web.********************.js` | 3.19 kB |
| `dotcom-rendering/dist/LatestLinks-importable.client.web.********************.js` | 7.84 kB |
| `dotcom-rendering/dist/LightboxHash-importable.client.web.********************.js` | 436 B |
| `dotcom-rendering/dist/LightboxLayout-importable.client.web.********************.js` | 6.58 kB |
| `dotcom-rendering/dist/LiveBlogEpic-importable.client.web.********************.js` | 3.56 kB |
| `dotcom-rendering/dist/LiveblogGutterAskWrapper-importable.client.web.********************.js` | 2.49 kB |
| `dotcom-rendering/dist/LiveblogNotifications-importable.client.web.********************.js` | 4.84 kB |
| `dotcom-rendering/dist/Liveness-importable.client.web.********************.js` | 4.7 kB |
| `dotcom-rendering/dist/LoopVideo-importable.client.web.********************.js` | 3.67 kB |
| `dotcom-rendering/dist/ManyNewsletterSignUp-importable.client.web.********************.js` | 7.68 kB |
| `dotcom-rendering/dist/MapEmbedBlockComponent-importable.client.web.********************.js` | 6.04 kB |
| `dotcom-rendering/dist/Metrics-importable.client.web.********************.js` | 2.7 kB |
| `dotcom-rendering/dist/MostViewedFooter-importable.client.web.********************.js` | 3.85 kB |
| `dotcom-rendering/dist/MostViewedFooterData-importable.client.web.********************.js` | 5.97 kB |
| `dotcom-rendering/dist/MostViewedRightWithAd-importable.client.web.********************.js` | 5.24 kB |
| `dotcom-rendering/dist/OnwardsUpper-importable.client.web.********************.js` | 5.32 kB |
| `dotcom-rendering/dist/PersonalityQuizAtom-importable.client.web.********************.js` | 3.35 kB |
| `dotcom-rendering/dist/ProfileAtom-importable.client.web.********************.js` | 545 B |
| `dotcom-rendering/dist/ProfileAtomWrapper-importable.client.web.********************.js` | 805 B |
| `dotcom-rendering/dist/PulsingDot-importable.client.web.********************.js` | 750 B |
| `dotcom-rendering/dist/QandaAtom-importable.client.web.********************.js` | 542 B |
| `dotcom-rendering/dist/ReaderRevenueDev-importable.client.web.********************.js` | 470 B |
| `dotcom-rendering/dist/readerRevenueDevUtils.client.web.********************.js` | 1.69 kB |
| `dotcom-rendering/dist/RelativeTime-importable.client.web.********************.js` | 2.56 kB |
| `dotcom-rendering/dist/RichLinkComponent-importable.client.web.********************.js` | 6.14 kB |
| `dotcom-rendering/dist/ScrollableFeature-importable.client.web.********************.js` | 6.6 kB |
| `dotcom-rendering/dist/ScrollableHighlights-importable.client.web.********************.js` | 7.16 kB |
| `dotcom-rendering/dist/ScrollableMedium-importable.client.web.********************.js` | 2.09 kB |
| `dotcom-rendering/dist/ScrollableSmall-importable.client.web.********************.js` | 2.16 kB |
| `dotcom-rendering/dist/SecureSignup-importable.client.web.********************.js` | 4.21 kB |
| `dotcom-rendering/dist/SendTargetingParams-importable.client.web.********************.js` | 2.21 kB |
| `dotcom-rendering/dist/sentry.client.web.********************.js` | 802 B |
| `dotcom-rendering/dist/SetABTests-importable.client.web.********************.js` | 3.91 kB |
| `dotcom-rendering/dist/SetAdTargeting-importable.client.web.********************.js` | 487 B |
| `dotcom-rendering/dist/ShareButton-importable.client.web.********************.js` | 2.18 kB |
| `dotcom-rendering/dist/shimport.client.web.********************.js` | 2.8 kB |
| `dotcom-rendering/dist/ShowHideContainers-importable.client.web.********************.js` | 897 B |
| `dotcom-rendering/dist/ShowMore-importable.client.web.********************.js` | 919 B |
| `dotcom-rendering/dist/SignInGateMain.client.web.********************.js` | 1.11 kB |
| `dotcom-rendering/dist/SignInGateMainCheckoutComplete.client.web.********************.js` | 2.6 kB |
| `dotcom-rendering/dist/SignInGateSelector-importable.client.web.********************.js` | 4.99 kB |
| `dotcom-rendering/dist/SlideshowCarousel-importable.client.web.********************.js` | 4.56 kB |
| `dotcom-rendering/dist/SlotBodyEnd-importable.client.web.********************.js` | 4.91 kB |
| `dotcom-rendering/dist/SpotifyBlockComponent-importable.client.web.********************.js` | 5.8 kB |
| `dotcom-rendering/dist/StickyBottomBanner-importable.client.web.********************.js` | 6.14 kB |
| `dotcom-rendering/dist/SubNav-importable.client.web.********************.js` | 2.44 kB |
| `dotcom-rendering/dist/TableOfContents-importable.client.web.********************.js` | 3.66 kB |
| `dotcom-rendering/dist/TimelineAtom-importable.client.web.********************.js` | 1.23 kB |
| `dotcom-rendering/dist/Titlepiece-importable.client.web.********************.js` | 13.6 kB |
| `dotcom-rendering/dist/TopBar-importable.client.web.********************.js` | 8.85 kB |
| `dotcom-rendering/dist/TopBarSupport-importable.client.web.********************.js` | 2.5 kB |
| `dotcom-rendering/dist/TweetBlockComponent-importable.client.web.********************.js` | 1.13 kB |
| `dotcom-rendering/dist/UnsafeEmbedBlockComponent-importable.client.web.********************.js` | 2.92 kB |
| `dotcom-rendering/dist/VideoFacebookBlockComponent-importable.client.web.********************.js` | 6.06 kB |
| `dotcom-rendering/dist/VineBlockComponent-importable.client.web.********************.js` | 3.33 kB |
| `dotcom-rendering/dist/YoutubeBlockComponent-importable.client.web.********************.js` | 843 B |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-01T16:07:11Z
body: ### [Deploy build 18688 of `dotcom:rendering-all` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=dotcom%3Arendering-all&build=18688&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 18688 of `dotcom:rendering-all` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=dotcom%3Arendering-all&build=18688&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 18688 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=dotcom%3Arendering-all&build=18688&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=dotcom%3Arendering-all&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for dotcom:rendering-all -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T10:01:58Z
body: ### [Deploy build 42895 of `Mobile::mobile-apps-rendering` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=Mobile%3A%3Amobile-apps-rendering&build=42895&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 42895 of `Mobile::mobile-apps-rendering` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=Mobile%3A%3Amobile-apps-rendering&build=42895&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 42895 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=Mobile%3A%3Amobile-apps-rendering&build=42895&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=Mobile%3A%3Amobile-apps-rendering&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for Mobile::mobile-apps-rendering -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T12:18:37Z
body: Hello :wave:! When you're ready to run Chromatic, please apply the `run_chromatic` label to this PR.

You will need to reapply the label each time you want to run Chromatic.

[Click here to see the Chromatic project.](https://www.chromatic.com/builds?appId=63e251470cfbe61776b0ef19)
author_association: NONE
user: login: GHaberis
created_at: 2025-05-08T14:04:40Z
body: I found the unit tests very helpful for understanding how the audience (MVT range) is being allocated amongst the tests 👏 
author_association: CONTRIBUTOR