Skip to content

Commit abe7929

Browse files
authored
Merge branch 'main' into jm/banner-colour-palettes
2 parents bc33d6d + 398c180 commit abe7929

File tree

12 files changed

+846
-258
lines changed

12 files changed

+846
-258
lines changed

.github/workflows/ab-testing-checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
FASTLY_AB_TESTING_CONFIG: ${{ secrets.FASTLY_AB_TESTING_CONFIG }}
2828
FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }}
2929
steps:
30-
- uses: actions/checkout@v4
30+
- uses: actions/checkout@v5
3131

3232
# https://github.com/denoland/setup-deno#latest-stable-for-a-major
3333
- uses: denoland/setup-deno@v1

.github/workflows/ab-testing-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
run:
2525
working-directory: ab-testing
2626
steps:
27-
- uses: actions/checkout@v4
27+
- uses: actions/checkout@v5
2828

2929
# https://github.com/denoland/setup-deno#latest-stable-for-a-major
3030
- uses: denoland/setup-deno@v1

ab-testing/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ The algorithm allocates tests available MVT IDs based on the audience size and s
7474
However, the allocation is completely separate for each audience space, so if you have a test in space `A` and move it to space `B`, it will be allocated different MVT IDs.
7575

7676
The state of the AB tests is stored in Fastly dictionaries, which are updated when the `deploy` task is run. Logic in fastly VCL will then use these dictionaries to determine which users are in which test groups and set appropriate headers and/or cookies.
77+
78+
See the [fastly-edge-cache documentation](https://github.com/guardian/fastly-edge-cache/blob/main/theguardiancom/docs/ab-testing.md) for even more details.

ab-testing/frontend/src/lib/components/AudienceBreakdown.svelte

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
1818
const BAR_HEIGHT = 40;
1919
20-
// Account for legend bar and vertical padding in chart height
21-
const chartHeight = tests.length * BAR_HEIGHT + BAR_HEIGHT + 16;
20+
const BAR_MARGIN_X = 0.1;
21+
const BAR_MARGIN_Y = 2;
2222
2323
const testSpaces = ['A', 'B', 'C'];
2424
25+
const chartHeight = testSpaces.length * BAR_HEIGHT + BAR_HEIGHT + 16;
26+
2527
const testsBySpace = testSpaces.map((space) => {
2628
if (space === 'A') {
2729
return tests.filter(
@@ -33,32 +35,32 @@
3335
});
3436
3537
function getBars(testList: ABTest[], rowPosition: number) {
36-
return testList.reduce<Array<ABTestBarData>>(
37-
(barsList, test, index) => {
38-
const previousBar = barsList.slice(-1)[0];
39-
const offset: number = Number(previousBar?.width ?? 0);
40-
const rowYLevel = index + rowPosition;
41-
const testSize = getSize(test);
42-
43-
return [
44-
...barsList,
45-
{
46-
x: offset,
47-
y: rowYLevel * BAR_HEIGHT + BAR_HEIGHT,
48-
width: testSize,
49-
name: test.name,
50-
segments: `${offset}% to ${offset + testSize}%`,
51-
},
52-
];
53-
},
54-
[],
55-
);
38+
return testList.reduce<Array<ABTestBarData>>((barsList, test) => {
39+
const previousBarsWidth = barsList.reduce(
40+
(acc, bar) => acc + bar.width,
41+
0,
42+
);
43+
const offset: number = Number(previousBarsWidth);
44+
const rowYLevel = rowPosition;
45+
const testSize = getSize(test);
46+
47+
return [
48+
...barsList,
49+
{
50+
x: offset,
51+
y: rowYLevel * BAR_HEIGHT + BAR_HEIGHT,
52+
width: testSize,
53+
name: test.name,
54+
segments: `${offset}% to ${offset + testSize}%`,
55+
},
56+
];
57+
}, []);
5658
}
5759
5860
function getAllRows(testsBySpace: ABTest[][]) {
5961
return testsBySpace.reduce<Array<ABTestBarData>>(
60-
(barsList, testsInSpace) => {
61-
return [...barsList, ...getBars(testsInSpace, barsList.length)];
62+
(barsList, testsInSpace, i) => {
63+
return [...barsList, ...getBars(testsInSpace, i)];
6264
},
6365
[],
6466
);
@@ -86,13 +88,13 @@
8688
</svg>
8789
{#each getAllRows(testsBySpace) as bar}
8890
<svg
89-
x={`${bar.x}%`}
91+
x={`${bar.x + BAR_MARGIN_X}%`}
9092
y={bar.y}
91-
width={`${bar.width}%`}
93+
width={`${bar.width - BAR_MARGIN_X * 2}%`}
9294
height={BAR_HEIGHT}
9395
>
9496
<g class="bar">
95-
<rect height={BAR_HEIGHT} width="100%" rx="4" />
97+
<rect height={BAR_HEIGHT - BAR_MARGIN_Y} width="100%" rx="4" />
9698
<text class="name" x="50%" y="50%">{bar.name}</text>
9799
<text class="segments" x="50%" y="50%">{bar.segments}</text>
98100
</g>

ab-testing/frontend/src/lib/components/TableFixed.svelte

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
function daysToExpiry(expires: string) {
1313
const today = new Date();
1414
const expiresDate = new Date(expires);
15-
const differenceInMilliseconds = expiresDate.getTime() - today.getTime();
15+
const differenceInMilliseconds =
16+
expiresDate.getTime() - today.getTime();
1617
const differenceInDays =
1718
differenceInMilliseconds / (1000 * 60 * 60 * 24);
1819
return Math.floor(differenceInDays);
@@ -21,43 +22,69 @@
2122

2223
<section class="tests">
2324
{#each tests as test}
25+
{@const expired = daysToExpiry(test.expirationDate) < 0}
2426
<table>
27+
<colgroup>
28+
<col span="1" style="width: 25%;" />
29+
<col span="1" style="width: 10%;" />
30+
<col span="1" style="width: 35%;" />
31+
<col span="1" style="width: 10%;" />
32+
<col span="1" style="width: 10%;" />
33+
<col span="1" style="width: 10%;" />
34+
</colgroup>
2535
<thead>
2636
<tr>
2737
<th scope="col">Name</th>
2838
<th scope="col">State</th>
29-
<th scope="col">Variants</th>
39+
<th scope="col">Test Groups</th>
3040
<th scope="col">Expires In</th>
3141
<th scope="col">Audience</th>
32-
<th scope="col">Offset</th>
3342
<th scope="col">Ophan</th>
3443
</tr>
3544
</thead>
3645
<tbody>
3746
<tr>
38-
<th scope="row" class="test-name">{test.name}</th>
39-
<td>{test.status}</td>
47+
<th scope="row" class="test-name"
48+
>{test.name} ({test.type})</th
49+
>
50+
<td
51+
class="status"
52+
class:off={test.status === 'OFF'}
53+
class:expired
54+
>
55+
{#if expired}
56+
EXPIRED
57+
{:else}
58+
{test.status}
59+
{/if}
60+
</td>
4061
<td>
4162
<TestVariants
63+
size={test.audienceSize * 100}
4264
testName={test.name}
4365
testGroups={test.groups}
4466
/>
4567
</td>
46-
<td>{daysToExpiry(test.expirationDate)} days</td>
68+
<td class:expired
69+
>{daysToExpiry(test.expirationDate)} days</td
70+
>
4771
<td>{test.audienceSize * 100}%</td>
48-
<td>0</td>
4972
<td><OphanLink testName={test.name} /></td>
5073
</tr>
5174
<tr>
5275
<th scope="row">Description</th>
53-
<td colspan="6">{test.description}</td>
76+
<td colspan="5">{test.description}</td>
5477
</tr>
5578
</tbody>
5679
</table>
5780
{/each}
5881
</section>
5982

6083
<style>
84+
:root {
85+
--ok-green: #00823b;
86+
--error-red: #d5281b;
87+
}
6188
.tests {
6289
border: 1px solid var(--border-grey);
6390
padding: 8px;
@@ -87,4 +114,20 @@
87114
.test-name {
88115
font-weight: 100;
89116
}
117+
118+
.status {
119+
text-transform: uppercase;
120+
font-weight: bold;
121+
122+
color: var(--ok-green);
123+
124+
&.off {
125+
color: #767676;
126+
}
127+
}
128+
129+
.expired {
130+
color: var(--error-red);
131+
font-weight: bold;
132+
}
90133
</style>

ab-testing/frontend/src/lib/components/TestVariants.svelte

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
22
interface Props {
33
testName: string;
44
testGroups: string[];
5+
size: number;
56
}
67
7-
const { testName, testGroups }: Props = $props();
8+
const { testName, testGroups, size }: Props = $props();
9+
10+
const formatter = new Intl.NumberFormat('en-US', {
11+
style: 'percent',
12+
minimumFractionDigits: 0,
13+
maximumFractionDigits: 2,
14+
});
815
</script>
916

1017
<div>
1118
<ul>
12-
{#each testGroups as group}
19+
{#each testGroups as group, i}
1320
<li>
1421
<a
15-
href={`http://www.theguardian.com/uk#ab-${testName}=${group}`}
16-
>
17-
{group}
18-
</a>
22+
href={`https://www.theguardian.com/ab-tests/opt/in/${testName}:${group}`}
23+
>
24+
{group} ({formatter.format(
25+
((1 / testGroups.length) * size) / 100,
26+
)})
27+
</a>{#if i < testGroups.length - 1}&nbsp;|&nbsp;{/if}
1928
</li>
2029
{/each}
2130
</ul>
@@ -28,7 +37,6 @@
2837
list-style: none;
2938
display: flex;
3039
flex-direction: row;
31-
justify-content: space-between;
3240
flex-wrap: wrap;
3341
}
3442
</style>

ab-testing/frontend/src/routes/+page.svelte

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,37 @@
22
import { ABTests } from '../../../abTest';
33
import Table from '$lib/components/TableFixed.svelte';
44
import AudienceBreakdown from '$lib/components/AudienceBreakdown.svelte';
5-
6-
const clientSideTests = ABTests.filter((test) => test.type === 'client');
7-
const serverSideTests = ABTests.filter((test) => test.type === 'server');
85
</script>
96

107
<h1 class="headline">A/B Tests</h1>
118
<section>
12-
<p>This page provides an overview of currently running A/B tests on theguardian.com. Please note that the audience segment allocations displayed for non-overlapping tests may not correspond to the actual allocation of MVT IDs, but simply represents how much of the audience is included in each test.</p>
13-
</section>
14-
<section>
15-
<h2 class="sub-headline">Client-side Tests</h2>
16-
<AudienceBreakdown tests={clientSideTests} />
17-
<Table tests={clientSideTests} />
9+
<p>
10+
This page provides an overview of currently running A/B tests on
11+
theguardian.com. Please note that the audience segment allocations
12+
displayed for non-overlapping tests may not correspond to the actual
13+
allocation of MVT IDs, but simply represents how much of the audience is
14+
included in each test.
15+
</p>
16+
<p>
17+
AB tests are defined in <a
18+
href="https://github.com/guardian/dotcom-rendering/blob/main/ab-testing/abTest.ts"
19+
>guardian/dotcom-rendering</a
20+
>
21+
</p>
22+
<p>
23+
Use the test group links in the table to opt in to those test groups,
24+
this will override any cookie based test assignment, and you will only
25+
be in that test until you opt out.
26+
</p>
27+
<p>
28+
<a href="https://www.theguardian.com/ab-tests/opt/out"
29+
>Use this link to opt out of any tests</a
30+
>
31+
</p>
1832
</section>
1933
<section>
20-
<h2 class="sub-headline">Server-side Tests</h2>
21-
<AudienceBreakdown tests={serverSideTests} />
22-
<Table tests={serverSideTests} />
34+
<AudienceBreakdown tests={ABTests} />
35+
<Table tests={ABTests} />
2336
</section>
2437

2538
<style>

apps-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"@types/react-test-renderer": "18.3.0",
7474
"@types/thrift": "0.10.17",
7575
"aws-cdk": "2.1030.0",
76-
"aws-cdk-lib": "2.219.0",
76+
"aws-cdk-lib": "2.220.0",
7777
"babel-loader": "10.0.0",
7878
"buffer": "6.0.3",
7979
"clean-css": "5.3.3",

dotcom-rendering/docs/development/ab-testing-in-dcr.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ When the config is merged, the A/B test will be automatically deployed and be av
1010

1111
Ab test on/off state is controlled only by the config. Expired tests will cause the ab testing validation to fail, they will also not be served. In effect expired tests are turned off "automatically", but their config needs to be cleaned up.
1212

13+
The test will appear in https://frontend.gutools.co.uk/analytics/ab-testing once the config is deployed.
14+
1315
## Putting code changes behind an A/B test (group)
1416

1517
### Use in Components
@@ -62,6 +64,19 @@ The ab test API is also available on the window object as `window.guardian.modul
6264

6365
Server side tests are also available in the CAPI object e.g. `CAPIArticle.config.serverSideABTests`.
6466

67+
## Forcing yourself into a test
68+
69+
Use the opt-in and opt-out URL fragments to force yourself into or out of a test.
70+
71+
When opted-in, the test will override any mvt based assignment and you'll only be in the opted-in test group.
72+
73+
When opted-out, you'll return to random/mvt based assignment.
74+
75+
These links are also in the [frontend admin](https://frontend.gutools.co.uk/analytics/ab-testing).
76+
77+
- Opt-in Example: `https://theguardian.com/ab-tests/opt/in/commercial-test-example:variant`
78+
- Opt-out: `https://theguardian.com/ab-tests/opt/out`
79+
6580
# Legacy A/B testing in DCR
6681

6782
> [!WARNING]

dotcom-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"ajv": "8.17.1",
100100
"ajv-formats": "2.1.1",
101101
"aws-cdk": "2.1030.0",
102-
"aws-cdk-lib": "2.219.0",
102+
"aws-cdk-lib": "2.220.0",
103103
"body-parser": "1.20.3",
104104
"browserslist": "4.24.4",
105105
"buffer": "6.0.3",

0 commit comments

Comments
 (0)