Skip to content

Commit a24b341

Browse files
authored
Merge branch 'main' into link-follow-to-notifications
2 parents ebb87c6 + 5e85aab commit a24b341

13 files changed

Lines changed: 128 additions & 9 deletions

ab-testing/config/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ type ABTest = {
5050
* See DCR Metrics component for end usage
5151
*/
5252
shouldForceMetricsCollection?: boolean;
53+
/**
54+
* Determines whether the test should be included in Ophan reporting. This function
55+
* will be evaluated on the client before the Ophan network request is made.
56+
*
57+
* For example, you could use this if you have a server-side test and would like to exclude certain
58+
* pageviews from reporting based on client-side information, such as users with smaller screens.
59+
*
60+
* @example: shouldReportToOphan: () => window.innerWidth >= 1300
61+
*
62+
* On by default: if not provided, the test will report to Ophan as usual.
63+
*/
64+
shouldReportToOphan?: () => boolean;
5365
};
5466

5567
export type { ABTest, FastlyTestParams, AudienceSpace, AllSpace };

dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.stories.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const Fixture = {
7070
refreshInterval: 3_000,
7171
matchHeaderURL:
7272
'https://api.nextgen.guardianapps.co.uk/football/api/match-header/2026/02/08/26247/48490.json',
73+
renderingTarget: 'Web',
7374
},
7475
play: async ({ canvas, canvasElement, step }) => {
7576
const nav = canvas.getByRole('navigation');
@@ -115,6 +116,7 @@ export const Live = {
115116
footballMatch: matchDayLive,
116117
reportURL: undefined,
117118
}),
119+
renderingTarget: 'Web',
118120
},
119121
play: async ({ canvas, canvasElement, step }) => {
120122
await step(
@@ -158,6 +160,7 @@ export const Result = {
158160
...feHeaderData,
159161
footballMatch: matchResult,
160162
}),
163+
renderingTarget: 'Web',
161164
},
162165

163166
play: async ({ canvas, canvasElement, step }) => {
@@ -188,6 +191,49 @@ export const Result = {
188191
},
189192
} satisfies Story;
190193

194+
export const AppsResult = {
195+
args: {
196+
initialTab: 'report',
197+
edition: 'AU',
198+
matchHeaderURL:
199+
'https://api.nextgen.guardianapps.co.uk/football/api/match-header/2026/02/08/26247/48490.json',
200+
refreshInterval: Fixture.args.refreshInterval,
201+
getHeaderData: () =>
202+
getMockData({
203+
...feHeaderData,
204+
footballMatch: matchResult,
205+
}),
206+
renderingTarget: 'Apps',
207+
},
208+
209+
play: async ({ canvas, canvasElement, step }) => {
210+
await step(
211+
'Placeholder shown whilst header data is being fetched',
212+
async () => {
213+
void expect(
214+
canvasElement.querySelector('[data-name="placeholder"]'),
215+
).toBeInTheDocument();
216+
},
217+
);
218+
219+
await step('Fetch match header data and render UI', async () => {
220+
// Wait for 'Premier League' to appear which signals match header
221+
// data has been fetched and the UI rendered on the client
222+
await canvas.findByText('Premier League');
223+
224+
const nav = canvas.getByRole('navigation');
225+
const matchInfoLink = within(nav).getByRole('link', {
226+
name: 'Match info',
227+
});
228+
229+
void expect(matchInfoLink).toHaveAttribute(
230+
'href',
231+
expect.stringContaining('/football/match/1'),
232+
);
233+
});
234+
},
235+
} satisfies Story;
236+
191237
const getMockData = (data: FEFootballMatchHeader) =>
192238
new Promise((resolve) => {
193239
setTimeout(() => {

dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../../lib/edition';
2424
import { palette } from '../../palette';
2525
import type { ColourName } from '../../paletteDeclarations';
26+
import type { RenderingTarget } from '../../types/renderingTarget';
2627
import { BigNumber } from '../BigNumber';
2728
import { FootballCrest } from '../FootballCrest';
2829
import { Placeholder } from '../Placeholder';
@@ -35,6 +36,7 @@ export type FootballMatchHeaderProps = {
3536
initialData?: HeaderData;
3637
edition: EditionId;
3738
matchHeaderURL: string;
39+
renderingTarget: RenderingTarget;
3840
};
3941

4042
type Props = FootballMatchHeaderProps & {
@@ -45,7 +47,7 @@ type Props = FootballMatchHeaderProps & {
4547
export const FootballMatchHeader = (props: Props) => {
4648
const { data } = useSWR<HeaderData, string>(
4749
props.matchHeaderURL,
48-
fetcher(props.initialTab, props.getHeaderData),
50+
fetcher(props.initialTab, props.renderingTarget, props.getHeaderData),
4951
swrOptions(props.refreshInterval),
5052
);
5153

@@ -111,10 +113,14 @@ const swrOptions = (refreshInterval: number): SWRConfiguration<HeaderData> => ({
111113
});
112114

113115
const fetcher =
114-
(selected: Props['initialTab'], getHeaderData: Props['getHeaderData']) =>
116+
(
117+
selected: Props['initialTab'],
118+
renderingTarget: RenderingTarget,
119+
getHeaderData: Props['getHeaderData'],
120+
) =>
115121
(url: string): Promise<HeaderData> =>
116122
getHeaderData(url)
117-
.then(parseHeaderData(selected))
123+
.then(parseHeaderData(selected, renderingTarget))
118124
.then((result) => {
119125
if (!result.ok) {
120126
log('dotcom', result.error);

dotcom-rendering/src/components/FootballMatchHeader/headerData.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../../frontend/feFootballMatchHeader';
1111
import { safeParseURL } from '../../lib/parse';
1212
import { error, fromValibot, ok, type Result } from '../../lib/result';
13+
import type { RenderingTarget } from '../../types/renderingTarget';
1314
import type { Tabs } from './Tabs';
1415

1516
export type HeaderData = {
@@ -19,7 +20,10 @@ export type HeaderData = {
1920
};
2021

2122
export const parse =
22-
(selected: HeaderData['tabs']['selected']) =>
23+
(
24+
selected: HeaderData['tabs']['selected'],
25+
renderingTarget: RenderingTarget,
26+
) =>
2327
(json: unknown): Result<string, HeaderData> => {
2428
const feData = fromValibot(
2529
safeParse(feFootballMatchHeaderSchema, json),
@@ -39,6 +43,7 @@ export const parse =
3943
selected,
4044
feData.value,
4145
parsedMatch.value.kind,
46+
renderingTarget,
4247
);
4348

4449
if (!maybeTabs.ok) {
@@ -58,18 +63,37 @@ type MatchURLError = {
5863
kind: 'live' | 'report' | 'info';
5964
};
6065

66+
const getInfoUrl = (
67+
feData: FEFootballMatchHeader,
68+
renderingTarget: RenderingTarget,
69+
): Result<MatchURLError, URL> => {
70+
const parsedInfoURL = safeParseURL(feData.infoURL);
71+
72+
if (!parsedInfoURL.ok) {
73+
return error({ kind: 'info' });
74+
}
75+
76+
if (renderingTarget === 'Apps') {
77+
const path = `football/match/${feData.footballMatch.id}`;
78+
return ok(new URL(path, parsedInfoURL.value.origin));
79+
}
80+
81+
return ok(parsedInfoURL.value);
82+
};
83+
6184
const createTabs = (
6285
selected: HeaderData['tabs']['selected'],
6386
feData: FEFootballMatchHeader,
6487
matchKind: FootballMatch['kind'],
88+
renderingTarget: RenderingTarget,
6589
): Result<MatchURLError, HeaderData['tabs']> => {
6690
const reportURL =
6791
feData.reportURL !== undefined
6892
? safeParseURL(feData.reportURL)
6993
: undefined;
7094
const liveURL =
7195
feData.liveURL !== undefined ? safeParseURL(feData.liveURL) : undefined;
72-
const infoURL = safeParseURL(feData.infoURL);
96+
const infoURL = getInfoUrl(feData, renderingTarget);
7397

7498
if (reportURL !== undefined && !reportURL.ok) {
7599
return error({ kind: 'report' });

dotcom-rendering/src/components/FootballMatchHeaderWrapper.importable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const FootballMatchHeaderWrapper = (props: Props) => (
2020
matchHeaderURL={props.matchHeaderURL}
2121
getHeaderData={getHeaderData}
2222
refreshInterval={16_000}
23+
renderingTarget={props.renderingTarget}
2324
/>
2425
);
2526

dotcom-rendering/src/components/FootballMatchInfoPage.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ export const FootballMatchInfoPage = {
3030
matchHeaderUrl: new URL(
3131
'https://api.nextgen.guardianapps.co.uk/football/api/match-header/2026/02/08/26247/48490.json',
3232
),
33+
renderingTarget: 'Web',
3334
},
3435
} satisfies Story;

dotcom-rendering/src/components/FootballMatchInfoPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type FootballTableSummary } from '../footballTables';
77
import { grid } from '../grid';
88
import { type EditionId } from '../lib/edition';
99
import { palette } from '../palette';
10+
import type { RenderingTarget } from '../types/renderingTarget';
1011
import { FootballMatchInfo } from './FootballMatchInfo';
1112
import { Island } from './Island';
1213

@@ -16,13 +17,15 @@ export const FootballMatchInfoPage = ({
1617
competitionName,
1718
edition,
1819
matchHeaderUrl,
20+
renderingTarget,
1921
table,
2022
}: {
2123
matchStats: FootballMatchStats;
2224
matchInfo: FootballMatch;
2325
competitionName: string;
2426
edition: EditionId;
2527
matchHeaderUrl: URL;
28+
renderingTarget: RenderingTarget;
2629
table?: FootballTableSummary;
2730
}) => {
2831
return (
@@ -43,6 +46,7 @@ export const FootballMatchInfoPage = ({
4346
}}
4447
edition={edition}
4548
matchHeaderURL={matchHeaderUrl.href}
49+
renderingTarget={renderingTarget}
4650
/>
4751
</Island>
4852
<div css={bodyGridStyles}>

dotcom-rendering/src/components/InteractiveLayoutAtom.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const InteractiveLayoutAtom = ({
1818
elementJs,
1919
elementCss,
2020
}: InteractiveLayoutAtomType) => (
21-
<figure
21+
<div
2222
className="interactive interactive-atom"
2323
css={containerStyles}
2424
data-atom-id={id}
@@ -42,5 +42,5 @@ export const InteractiveLayoutAtom = ({
4242
dangerouslySetInnerHTML={{ __html: elementJs }}
4343
/>
4444
)}
45-
</figure>
45+
</div>
4646
);

dotcom-rendering/src/experiments/lib/beta-ab-tests.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { activeABtests } from '@guardian/ab-testing-config';
12
import { isUndefined } from '@guardian/libs';
23
import { getABTestParticipations } from '../../client/abTesting';
34

@@ -81,9 +82,18 @@ export class BetaABTests implements BetaABTestAPI {
8182
});
8283
}
8384

84-
private buildOphanPayload(errorReporter: ErrorReporter) {
85+
private shouldReportToOphan(testId: string): boolean {
86+
const activeTest = activeABtests.find(({ name }) => name === testId);
87+
return activeTest?.shouldReportToOphan
88+
? activeTest.shouldReportToOphan()
89+
: true;
90+
}
91+
92+
private buildOphanPayload(errorReporter: ErrorReporter): OphanABPayload {
8593
try {
86-
const testAndVariantIds = Object.entries(this.participations);
94+
const testAndVariantIds = Object.entries(
95+
this.participations,
96+
).filter(([testId]) => this.shouldReportToOphan(testId));
8797

8898
return testAndVariantIds.reduce<OphanABPayload>(
8999
(eventLog, [testId, variantId]) => {

dotcom-rendering/src/layouts/LiveLayout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => {
395395
initialTab="live"
396396
edition={article.editionId}
397397
matchHeaderURL={footballMatchHeaderUrl}
398+
renderingTarget={renderingTarget}
398399
/>
399400
</Island>
400401
)

0 commit comments

Comments
 (0)