Skip to content

Commit 1fe7ac7

Browse files
committed
Merge branch 'main' into link-follow-to-notifications
2 parents fc192bd + e2f32ab commit 1fe7ac7

279 files changed

Lines changed: 1075 additions & 969 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ab-testing/config/abTests.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const ABTests: ABTest[] = [
4747
name: "commercial-enable-spacefinder-on-interactives",
4848
description: "Enable spacefinder on interactive articles on mobile web",
4949
owners: ["commercial.dev@guardian.co.uk"],
50-
expirationDate: `2026-03-25`,
50+
expirationDate: "2026-04-09",
5151
type: "client",
5252
status: "ON",
5353
audienceSize: 0 / 100,
@@ -159,6 +159,18 @@ const ABTests: ABTest[] = [
159159
groups: ["control", "variant"],
160160
shouldForceMetricsCollection: true,
161161
},
162+
{
163+
name: "growth-auxia-banner",
164+
description: "Use Auxia API for deciding when to show a RR banner",
165+
owners: ["growth.dev@guardian.co.uk"],
166+
expirationDate: "2026-09-01",
167+
type: "client",
168+
status: "ON",
169+
audienceSize: 1,
170+
audienceSpace: "C",
171+
groups: ["control", "variant"],
172+
shouldForceMetricsCollection: true,
173+
},
162174
];
163175

164176
const activeABtests = ABTests.filter((test) => test.status === "ON");

dotcom-rendering/docs/architecture/026-better-partial-hydration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ PRs #[3629](https://github.com/guardian/dotcom-rendering/pull/3629) & [#3784](ht
3232
To create a new island you now:
3333

3434
1. Wrap you component on the server with `Island`.
35-
2. Add `.importable` to the component filename. Eg: `[MyThing].importable.tsx`
35+
2. Add `.island` to the component filename. Eg: `[MyThing].island.tsx`
3636

3737
This is simpler to use, harder to make mistakes with and is certain to only ever send the data to the client that is required.

dotcom-rendering/docs/contributing/how-to.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ decision document.
3636
To add an island:
3737

3838
1. Wrap your component on the server with an `<Island>` component.
39-
2. Add `.importable` to the component filename. Eg: `[MyThing].importable.tsx`
39+
2. Add `.island` to the component filename. Eg: `[MyThing].island.tsx`
4040
3. Specify what should trigger hydration (e.g. waiting until the component
4141
scrolls into view). See `Island.tsx` props for options.
4242

dotcom-rendering/docs/contributing/where-should-my-code-live.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ We use Islands when we want to load (P)react code on the client:
4242
</Island>
4343
```
4444

45-
To support this in the build system, we require that the files for components which are used in an island are suffixed with `.importable.tsx`.
46-
e.g `MyComponent.importable.tsx`. They also must always live in the `src/components` directory.
45+
To support this in the build system, we require that the files for components which are used in an island are suffixed with `.island.tsx`.
46+
e.g `MyComponent.island.tsx`. They also must always live in the `src/components` directory.
4747

48-
When we have different islands depending on the rendering target, the `.importable` should always come last, e.g `MyComponent.apps.importable.ts`.
48+
When we have different islands depending on the rendering target, the `.island` should always come last, e.g `MyComponent.apps.island.ts`.
4949

5050
## Scripts
5151

dotcom-rendering/docs/lightbox.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ We use React.createPortal to insert the resulting html from the `LightboxImages`
6161

6262
Captions in lightbox have sufficient deviation in style to have their own component.
6363

64-
### `LightboxHash.importable`
64+
### `LightboxHash.island`
6565

6666
This small file is placed in an `Island` inside `ArticlePage` and executed immediately. It is not deferred.
6767

@@ -72,7 +72,7 @@ Because we use the url as the source of truth for lightbox it means we close the
7272

7373
The fix here is to mutate the history state by adding a new entry so that when `history.back()` gets fired the reader ends up on the article.
7474

75-
### `LightboxJavascript.importable`
75+
### `LightboxJavascript`
7676

7777
This file contains the logic for how lightbox operates.
7878

dotcom-rendering/fixtures/manual/trails.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -702,11 +702,12 @@ export const selfHostedLoopVideo54Card = {
702702
{
703703
mimeType: 'video/mp4',
704704
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
705+
width: 500,
706+
height: 400,
705707
},
706708
],
709+
aspectRatio: 5 / 4,
707710
duration: 30,
708-
width: 500,
709-
height: 400,
710711
image: 'https://media.guim.co.uk/6537e163c9164d25ec6102641f6a04fa5ba76560/0_210_5472_3283/master/5472.jpg',
711712
},
712713
image: {
@@ -725,10 +726,11 @@ export const selfHostedLoopVideo45Card = {
725726
{
726727
mimeType: 'video/mp4',
727728
src: 'https://uploads.guim.co.uk/2025/11/27/4_5_Test--1d34df3e-8c92-4090-8bb6-d79fc7fb9467-1.0.mp4',
729+
width: 576,
730+
height: 720,
728731
},
729732
],
730-
width: 576,
731-
height: 720,
733+
aspectRatio: 4 / 5,
732734
},
733735
} satisfies DCRFrontCard;
734736

@@ -741,10 +743,12 @@ export const selfHostedLoopVideo53Card = {
741743
{
742744
mimeType: 'video/mp4',
743745
src: 'https://uploads.guim.co.uk/2025/11/27/5_3_Test--26763e61-c16b-4c10-8c16-3f11882da154-1.0.mp4',
746+
width: 1200,
747+
height: 720,
744748
},
745749
],
746-
width: 1200,
747-
height: 720,
750+
751+
aspectRatio: 5 / 3,
748752
},
749753
} satisfies DCRFrontCard;
750754

@@ -757,10 +761,11 @@ export const selfHostedLoopVideo916Card = {
757761
{
758762
mimeType: 'video/mp4',
759763
src: 'https://uploads.guimcode.co.uk/2025/11/12/5x4_test--ee49513c-bf16-4321-a444-09c9a037d584-4.0.mp4',
764+
width: 406,
765+
height: 720,
760766
},
761767
],
762-
width: 406,
763-
height: 720,
768+
aspectRatio: 9 / 16,
764769
},
765770
} satisfies DCRFrontCard;
766771

@@ -773,10 +778,11 @@ export const selfHostedLoopVideo169Card = {
773778
{
774779
mimeType: 'video/mp4',
775780
src: 'https://uploads.guim.co.uk/2026/01/02/Social_media_footage_shows_person_trying_to_put_out_flames_in_Crans-Montana_bar___video--77ec00d2-7e58-4698-898c-08174f65a94b-1.0.mp4',
781+
width: 1280,
782+
height: 720,
776783
},
777784
],
778-
width: 1280,
779-
height: 720,
785+
aspectRatio: 16 / 9,
780786
},
781787
} satisfies DCRFrontCard;
782788

dotcom-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
"webpack-dev-server": "5.2.2",
169169
"webpack-hot-middleware": "2.26.1",
170170
"webpack-hot-server-middleware": "0.6.1",
171-
"webpack-manifest-plugin": "5.0.0",
171+
"webpack-manifest-plugin": "6.0.1",
172172
"webpack-merge": "6.0.1",
173173
"webpack-messages": "2.0.4",
174174
"webpack-node-externals": "3.0.0",

dotcom-rendering/playwright/tests/banner.e2e.spec.ts

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isUndefined } from '@guardian/libs';
2-
import type { BrowserContext, Request } from '@playwright/test';
3-
import { test } from '@playwright/test';
4-
import { cmpAcceptAll } from '../lib/cmp';
2+
import type { BrowserContext, Page, Request } from '@playwright/test';
3+
import { expect, test } from '@playwright/test';
4+
import { allowRejectAll, cmpAcceptAll, cmpRejectAll } from '../lib/cmp';
55
import { addCookie } from '../lib/cookies';
66
import { loadPage } from '../lib/load-page';
77

@@ -12,6 +12,10 @@ const optOutOfArticleCountConsent = async (context: BrowserContext) => {
1212
});
1313
};
1414

15+
const ARTICLE_PATH =
16+
'/Article/https://www.theguardian.com/politics/2019/nov/20/jeremy-corbyn-boris-johnson-tv-debate-watched-by-67-million-people';
17+
const RR_BANNER_URL = 'https://contributions.guardianapis.com/banner';
18+
1519
const requestBodyHasProperties = (
1620
request: Request,
1721
url: string | RegExp,
@@ -25,21 +29,32 @@ const requestBodyHasProperties = (
2529
);
2630
};
2731

32+
const getBannerRequestField = (
33+
request: Request,
34+
url: string | RegExp,
35+
field: string,
36+
): unknown => {
37+
if (!request.url().match(url)) return undefined;
38+
const postJSON = request.postDataJSON() as {
39+
targeting?: Record<string, unknown>;
40+
};
41+
return postJSON.targeting?.[field];
42+
};
43+
2844
test.describe('The banner', function () {
2945
test('makes a request to the support-dotcom-components service', async ({
3046
page,
3147
context,
3248
}) => {
3349
await optOutOfArticleCountConsent(context);
34-
const rrBannerUrl = 'https://contributions.guardianapis.com/banner';
3550

3651
const rrBannerRequestPromise = page.waitForRequest((request) =>
37-
requestBodyHasProperties(request, rrBannerUrl, ['targeting']),
52+
requestBodyHasProperties(request, RR_BANNER_URL, ['targeting']),
3853
);
3954

4055
await loadPage({
4156
page,
42-
path: `/Article/https://www.theguardian.com/politics/2019/nov/20/jeremy-corbyn-boris-johnson-tv-debate-watched-by-67-million-people`,
57+
path: ARTICLE_PATH,
4358
waitUntil: 'domcontentloaded',
4459
region: 'GB',
4560
preventSupportBanner: false,
@@ -62,7 +77,7 @@ test.describe('Sign-in gate portal', function () {
6277

6378
await loadPage({
6479
page,
65-
path: `/Article/https://www.theguardian.com/politics/2019/nov/20/jeremy-corbyn-boris-johnson-tv-debate-watched-by-67-million-people`,
80+
path: ARTICLE_PATH,
6681
waitUntil: 'domcontentloaded',
6782
region: 'GB',
6883
preventSupportBanner: false,
@@ -82,3 +97,122 @@ test.describe('Sign-in gate portal', function () {
8297
await auxiaRequestPromise;
8398
});
8499
});
100+
101+
test.describe('Banner browserId targeting', function () {
102+
const setAuxiaVariantCookie = async (context: BrowserContext) => {
103+
await addCookie(context, {
104+
name: 'gu_client_ab_tests',
105+
value: 'growth-auxia-banner:variant',
106+
});
107+
};
108+
const setBwidCookie = async (
109+
context: BrowserContext,
110+
value = 'test-browser-id',
111+
) => {
112+
await addCookie(context, {
113+
name: 'bwid',
114+
value,
115+
});
116+
};
117+
118+
const loadAndCaptureBannerRequest = async ({
119+
page,
120+
context,
121+
acceptConsent,
122+
inAuxiaVariant,
123+
}: {
124+
page: Page;
125+
context: BrowserContext;
126+
acceptConsent: boolean;
127+
inAuxiaVariant: boolean;
128+
}) => {
129+
if (!acceptConsent) {
130+
await allowRejectAll(context);
131+
}
132+
await optOutOfArticleCountConsent(context);
133+
await setBwidCookie(context);
134+
if (inAuxiaVariant) {
135+
await setAuxiaVariantCookie(context);
136+
}
137+
138+
const bannerRequestPromise = page.waitForRequest(
139+
(request) =>
140+
request.url().includes(RR_BANNER_URL) &&
141+
request.method() === 'POST',
142+
);
143+
144+
await loadPage({
145+
page,
146+
path: ARTICLE_PATH,
147+
waitUntil: 'domcontentloaded',
148+
region: 'GB',
149+
preventSupportBanner: false,
150+
});
151+
152+
if (acceptConsent) {
153+
await cmpAcceptAll(page);
154+
} else {
155+
await cmpRejectAll(page);
156+
}
157+
158+
return bannerRequestPromise;
159+
};
160+
161+
test('sends browserId when user has consented and is in the auxia variant', async ({
162+
page,
163+
context,
164+
}) => {
165+
const bannerRequest = await loadAndCaptureBannerRequest({
166+
page,
167+
context,
168+
acceptConsent: true,
169+
inAuxiaVariant: true,
170+
});
171+
172+
const browserId = getBannerRequestField(
173+
bannerRequest,
174+
RR_BANNER_URL,
175+
'browserId',
176+
);
177+
expect(browserId).toBe('test-browser-id');
178+
});
179+
180+
// Skip this test because it doesn't work in the github actions run. It does however work locally
181+
test.skip('does not send browserId when user has not consented, even if in the auxia variant', async ({
182+
page,
183+
context,
184+
}) => {
185+
const bannerRequest = await loadAndCaptureBannerRequest({
186+
page,
187+
context,
188+
acceptConsent: false,
189+
inAuxiaVariant: true,
190+
});
191+
192+
const browserId = getBannerRequestField(
193+
bannerRequest,
194+
RR_BANNER_URL,
195+
'browserId',
196+
);
197+
expect(browserId).toBeUndefined();
198+
});
199+
200+
test('does not send browserId when user is not in the auxia variant, even if consented', async ({
201+
page,
202+
context,
203+
}) => {
204+
const bannerRequest = await loadAndCaptureBannerRequest({
205+
page,
206+
context,
207+
acceptConsent: true,
208+
inAuxiaVariant: false,
209+
});
210+
211+
const browserId = getBannerRequestField(
212+
bannerRequest,
213+
RR_BANNER_URL,
214+
'browserId',
215+
);
216+
expect(browserId).toBeUndefined();
217+
});
218+
});

dotcom-rendering/scripts/islands/island-descriptions.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const getIslands = async (report) => {
7373
const components = await readdir(componentsDirectory);
7474

7575
const filenames = components.filter((component) =>
76-
component.endsWith('.importable.tsx'),
76+
component.endsWith('.island.tsx'),
7777
);
7878

7979
const files = await Promise.all(
@@ -88,10 +88,10 @@ const getIslands = async (report) => {
8888

8989
const islandsData = await Promise.all(
9090
files.map(async ({ filename, content }) => {
91-
const name = filename.replace('.importable.tsx', '');
91+
const name = filename.replace('.island.tsx', '');
9292
const { gzipSize = 0, parsedSize = 0 } =
9393
report.find(({ label }) =>
94-
label.startsWith(name + '-importable.'),
94+
label.startsWith(name + '-island.'),
9595
) ?? {};
9696

9797
const matched = content.match(getRegExForIsland(name));

dotcom-rendering/src/client/bootCmp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ const initialiseCmp = async () => {
3838
};
3939
/**
4040
* Hydrating this island is so critical that it should not be imported
41-
* as a separate chunk. @see {PrivacySettingsLink.importable.tsx}
41+
* as a separate chunk. @see {PrivacySettingsLink.island.tsx}
4242
*/
4343
const eagerlyImportPrivacySettingsLinkIsland = () =>
4444
import(
45-
/* webpackMode: 'eager' */ '../components/PrivacySettingsLink.importable'
45+
/* webpackMode: 'eager' */ '../components/PrivacySettingsLink.island'
4646
);
4747

4848
/**

0 commit comments

Comments
 (0)