diff --git a/src/app/lib/logger.const.js b/src/app/lib/logger.const.js index 9df32070cc8..8aa6fd901fa 100644 --- a/src/app/lib/logger.const.js +++ b/src/app/lib/logger.const.js @@ -15,6 +15,7 @@ const logCodes = { DATA_REQUEST_RECEIVED: 'data_request_received', DATA_RESPONSE_FROM_CACHE: 'data_response_from_cache', BFF_FETCH_ERROR: 'bff_fetch_error', + SPORT_DATA_FETCH_ERROR: 'sport_data_fetch_error', IDCTA_FETCH_ERROR: 'idcta_fetch_error', // Files diff --git a/src/app/routes/utils/fetchDataFromSportData/fixture.json b/src/app/routes/utils/fetchDataFromSportData/fixture.json new file mode 100644 index 00000000000..9da8cac0f79 --- /dev/null +++ b/src/app/routes/utils/fetchDataFromSportData/fixture.json @@ -0,0 +1,138 @@ +{ + "title": "Villa gain upper hand with gritty Europa League win at Bologna", + "live": false, + "startDateTime": "2026-04-09T19:00:00.000Z", + "uasActivityData": { + "action": "heartbeat", + "resourceDomain": "bbc-live", + "resourceId": "urn:bbc:tipo:topic:ce847pmy2e7t", + "resourceType": "asset", + "environment": "live", + "apiKey": "km5ifdffcjdoc" + }, + "countingServiceDataAverage": 0, + "sportDataEvent": { + "urn": "urn:bbc:sportsdata:football:event:s-3y91hnyfjh24yxjhm77a7hy50", + "home": { + "id": "ej5er0oyngdw138yuumwqbyqt", + "fullName": "Bologna", + "shortName": "Bologna", + "urn": "urn:bbc:sportsdata:football:team:bologna", + "runningScores": { "halftime": "0", "fulltime": "1", "aggregate": "1" }, + "scoreUnconfirmed": "1", + "actions": [ + { + "playerUrn": "urn:bbc:sportsdata:football:player:s-2bmeynv0dhsc8sjfuaprkexre", + "playerName": "J. Rowe", + "actionType": "goal", + "actions": [ + { + "type": "Goal", + "typeLabel": { "value": "Goal", "accessible": "Goal" }, + "timeLabel": { "value": "90'", "accessible": "90 minutes" } + } + ] + } + ], + "score": "1" + }, + "away": { + "id": "b496gs285it6bheuikox6z9mj", + "fullName": "Aston Villa", + "shortName": "Aston Villa", + "urn": "urn:bbc:sportsdata:football:team:aston-villa", + "runningScores": { "halftime": "1", "fulltime": "3", "aggregate": "3" }, + "scoreUnconfirmed": "3", + "actions": [ + { + "playerUrn": "urn:bbc:sportsdata:football:player:s-8qys6qtdwgsycxducl062zld5", + "playerName": "E. Konsa", + "actionType": "goal", + "actions": [ + { + "type": "Goal", + "typeLabel": { "value": "Goal", "accessible": "Goal" }, + "timeLabel": { "value": "44'", "accessible": "44 minutes" } + } + ] + }, + { + "playerUrn": "urn:bbc:sportsdata:football:player:s-5m0j33eoa5c8pqlr0tdf7undh", + "playerName": "O. Watkins", + "actionType": "goal", + "actions": [ + { + "type": "Goal", + "typeLabel": { "value": "Goal", "accessible": "Goal" }, + "timeLabel": { "value": "51'", "accessible": "51 minutes" } + }, + { + "type": "Goal", + "typeLabel": { "value": "Goal", "accessible": "Goal" }, + "timeLabel": { + "value": "90'+4", + "accessible": "90 minutes plus 4" + } + } + ] + } + ], + "score": "3" + }, + "time": { + "accessibleTime": "20:00", + "displayTimeUK": "20:00", + "timeCertainty": true + }, + "date": "Thu 9 Apr 2026", + "tournament": { + "id": "4c1nfi2j1m731hcay25fcgndq", + "name": "UEFA Europa League", + "disambiguatedName": "UEFA Europa League", + "urn": "urn:bbc:sportsdata:football:tournament:europa-league", + "thingsGuid": "2afbdda7-71d4-544d-bcc6-d9ff50314b2a" + }, + "stage": { + "id": "7wxuj38kqm8bz3cmi15vu4w7o", + "name": "Quarter-finals", + "urn": "" + }, + "multiLeg": { "leg": 1, "relatedMatchId": "s-9ur6e6w5f4ahyxph7ef4rks2c" }, + "period": "ft", + "venue": { + "id": "2nrn0y55nz9ee7p9adzbb7fta", + "urn": "urn:bbc:sportsdata:football:venue:s-2nrn0y55nz9ee7p9adzbb7fta", + "name": "Stadio Renato Dall'Ara", + "shortName": "Stadio Renato Dall'Ara" + }, + "attendance": { "value": 31142 }, + "status": "PostEvent", + "periodLabel": { "value": "FT", "accessible": "Full time" }, + "winner": "away", + "tournamentDescriptionLabel": "UEFA Europa League - Quarter-finals", + "groupedActions": [ + { + "groupName": { "fullName": "Assists", "shortName": "Assists" }, + "homeTeamActions": ["J. Lucumí (90')"], + "awayTeamActions": ["Y. Tielemans (44', 90'+4)", "E. Buendía (51')"] + } + ], + "accessibleEventSummary": "Bologna 1 , Aston Villa 3 at Full time", + "sportDiscipline": "football" + }, + "staticLinksData": { + "staticLinks": [ + { + "url": "/sport/football/live/clygly9z1j9t", + "text": "Relive Thursday's Europa League and Conference League action" + } + ] + }, + "type": "oppm", + "headerImage": null, + "seoTimestamps": { + "firstPublished": "2026-04-08T03:00:55.128Z", + "lastModified": "2026-04-10T08:37:45.000Z" + }, + "lastUpdatedString": "Updated 6 days ago" +} diff --git a/src/app/routes/utils/fetchDataFromSportData/index.ts b/src/app/routes/utils/fetchDataFromSportData/index.ts new file mode 100644 index 00000000000..5b0e7bb9970 --- /dev/null +++ b/src/app/routes/utils/fetchDataFromSportData/index.ts @@ -0,0 +1,58 @@ +import Url from 'url-parse'; +import fetchPageData from '#app/routes/utils/fetchPageData'; +import getErrorStatusCode from '#app/routes/utils/fetchPageData/utils/getErrorStatusCode'; +import { SPORT_DATA_FETCH_ERROR } from '#lib/logger.const'; +import { FetchError, GetAgent } from '#models/types/fetch'; +import nodeLogger from '#lib/logger.node'; + +const logger = nodeLogger(__filename); + +interface fetchDataFromSportData { + type: string; + urn: string; + getAgent?: GetAgent; +} + +export default async ({ type, urn, getAgent }: fetchDataFromSportData) => { + const queryParameters = { + type, + urn, + }; + + const fetchUrl = Url('https://fabl.api.bbci.co.uk/module/sport-data').set( + 'query', + queryParameters, + ); + + const useCerts = true; + + const agent = useCerts && getAgent ? await getAgent() : undefined; + const timeout = useCerts ? undefined : 60000; + + try { + const fetchPageDataArgs = { + path: fetchUrl.toString(), + ...(agent && { agent }), + ...(timeout && { timeout }), + }; + + // @ts-expect-error - Ignore fetchPageData argument types + const { status, json } = await fetchPageData(fetchPageDataArgs); + + return { + status, + json, + }; + } catch (error: unknown) { + const { message, status = getErrorStatusCode() } = error as FetchError; + + logger.error(SPORT_DATA_FETCH_ERROR, { + type, + urn, + status, + message, + }); + + throw error; + } +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx b/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx index 71d8f0a4669..f030ec62440 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx @@ -18,6 +18,7 @@ import { import Stream from './Stream'; import Header from './Header'; import KeyPoints from './KeyPoints'; +import HeadToHeadV2 from './SportDataHeader/head-to-head-v2'; import styles from './styles'; import { StreamResponse } from './Post/types'; import { KeyPointsResponse } from './KeyPoints/types'; @@ -59,6 +60,7 @@ export type ComponentProps = { endDateTime?: string; metadata: { atiAnalytics: ATIData }; mediaCollections: MediaCollection[] | null; + sportEventDetails?: object | null; }; }; @@ -87,8 +89,14 @@ const LivePage = ({ pageData, assetId }: LivePageProps) => { headerImage, promoImage, mediaCollections, + sportEventDetails, } = pageData; + // console.log('&&&&&&&&&&&&&&&&&&&&&'); + // console.log('SPORT DATA'); + // console.log(JSON.stringify(sportEventDetails)); + // console.log('&&&&&&&&&&&&&&&&&&&&&'); + const { currentStreamData, hasPendingUpdate, applyPendingUpdate } = useLivePagePolling(pageData, livePagePollingEnabled && isLive); @@ -179,6 +187,11 @@ const LivePage = ({ pageData, assetId }: LivePageProps) => { imageWidth={imageWidth} mediaCollections={mediaCollections} /> + {sportEventDetails && ( +