diff --git a/jest.config.js b/jest.config.js index 123572e0ff5..436ae4ce9b0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,6 +10,7 @@ const unitTests = { moduleNameMapper: { ...jestDirAlias, '\\.module\\.(css|scss)$': 'identity-obj-proxy', + '^@bbc/resonance$': '/src/testHelpers/resonanceMock.ts', }, testEnvironment: 'jsdom', snapshotSerializers: ['@emotion/jest/serializer'], @@ -43,6 +44,7 @@ const clientUnitTests = { moduleNameMapper: { ...jestDirAlias, '\\.module\\.(css|scss)$': 'identity-obj-proxy', + '^@bbc/resonance$': '/src/testHelpers/resonanceMock.ts', }, testEnvironment: '@happy-dom/jest-environment', snapshotSerializers: ['@emotion/jest/serializer'], diff --git a/package.json b/package.json index f305adde950..052839abc98 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-is": "19" }, "dependencies": { + "@bbc/resonance": "https://mybbc-analytics.files.bbci.co.uk/resonance/resonance-2.7.2.tgz", "@bbc/reverb-url-helper": "https://mybbc-analytics.files.bbci.co.uk/reverb-url-helper/bbc-reverb-url-helper-2.5.0.tgz", "@bbc/web-vitals": "2.6.0", "@emotion/cache": "11.14.0", diff --git a/src/app/components/ATIAnalytics/atiUrl/index.client.test.ts b/src/app/components/ATIAnalytics/atiUrl/index.client.test.ts index d5f9019d7be..ff7881d81e2 100644 --- a/src/app/components/ATIAnalytics/atiUrl/index.client.test.ts +++ b/src/app/components/ATIAnalytics/atiUrl/index.client.test.ts @@ -1,6 +1,12 @@ +import { ResonanceMode } from '@bbc/resonance'; import { Platforms } from '#app/models/types/global'; +import * as getEnvConfigModule from '#app/lib/utilities/getEnvConfig'; import * as genericLabelHelpers from '../../../lib/analyticsUtils'; -import { buildReverbAnalyticsModel, buildReverbEventModel } from '.'; +import { + buildResonanceAnalyticsModel, + buildReverbAnalyticsModel, + buildReverbEventModel, +} from '.'; const mockAndSet = ({ name, source }, response) => { source[name] = jest.fn(); // eslint-disable-line no-param-reassign @@ -20,6 +26,76 @@ describe('atiUrl', () => { jest.clearAllMocks(); }); + describe('Resonance', () => { + describe('buildResonanceAnalyticsModel', () => { + const input = { + appName: 'news-pidgin', + contentId: 'urn:bbc:optimo:asset:c0000000001o', + contentType: 'article', + language: 'pcm', + statsDestination: 'statsDestination', + siteId: 12345, + hashedId: null, + pageIdentifier: 'pidgin.articles.c0000000001o.page', + producerName: 'PIDGIN', + platform: 'canonical' as Platforms, + }; + + it('should return the correct Resonance analytics model', () => { + const result = buildResonanceAnalyticsModel(input); + + expect(result.resonanceProperties).toEqual({ + mode: ResonanceMode.TEST, + }); + expect(result.baseProperties).toEqual({ + app: { name: 'news-pidgin' }, + destination: 'statsDestination', + hashedUserId: undefined, + pageName: 'pidgin.articles.c0000000001o.page', + producer: 'PIDGIN', + siteId: 12345, + }); + expect(result.pageviewProperties).toEqual({ + contentId: 'urn:bbc:optimo:asset:c0000000001o', + contentType: 'article', + language: 'pcm', + destination: 'statsDestination', + producer: 'PIDGIN', + }); + }); + + it('should suffix app name with "-app" when platform is app', () => { + const result = buildResonanceAnalyticsModel({ + ...input, + platform: 'app' as Platforms, + }); + + expect(result.baseProperties.app).toEqual({ name: 'news-pidgin-app' }); + }); + + it('should pass hashedId through as hashedUserId when provided', () => { + const result = buildResonanceAnalyticsModel({ + ...input, + hashedId: 'abc123hasheduser', + }); + + expect(result.baseProperties.hashedUserId).toBe('abc123hasheduser'); + }); + + it('should use LIVE mode when SIMORGH_APP_ENV is live', () => { + jest + .spyOn(getEnvConfigModule, 'getEnvConfig') + .mockReturnValue({ SIMORGH_APP_ENV: 'live' } as ReturnType< + typeof getEnvConfigModule.getEnvConfig + >); + + const result = buildResonanceAnalyticsModel(input); + + expect(result.resonanceProperties.mode).toBe(ResonanceMode.LIVE); + }); + }); + }); + describe('Reverb', () => { describe('buildReverbAnalyticsModel', () => { beforeEach(() => { diff --git a/src/app/components/ATIAnalytics/atiUrl/index.ts b/src/app/components/ATIAnalytics/atiUrl/index.ts index 604dae7a99e..e6fdbca0303 100644 --- a/src/app/components/ATIAnalytics/atiUrl/index.ts +++ b/src/app/components/ATIAnalytics/atiUrl/index.ts @@ -16,6 +16,7 @@ import { ATIEventTrackingProps, ATIPageTrackingProps, ReverbBeaconConfig, + ResonanceBeaconConfig, } from '../types'; /* @@ -23,6 +24,46 @@ import { * https://github.com/ampproject/amphtml/blob/master/spec/amp-var-substitutions.md#device-and-browser */ +const RESONANCE_MODE = { LIVE: 'live', TEST: 'test' } as const; + +export const buildResonanceAnalyticsModel = ({ + appName, + contentId, + contentType, + language, + statsDestination, + siteId, + hashedId, + pageIdentifier, + producerName, + platform, +}: ATIPageTrackingProps): ResonanceBeaconConfig => { + const env = getEnvConfig().SIMORGH_APP_ENV; + + return { + resonanceProperties: { + mode: env === 'live' ? RESONANCE_MODE.LIVE : RESONANCE_MODE.TEST, + }, + baseProperties: { + app: { + name: platform === 'app' ? `${appName}-app` : appName, + }, + destination: statsDestination, + hashedUserId: hashedId ?? undefined, + pageName: pageIdentifier, + producer: producerName, + siteId, + }, + pageviewProperties: { + contentId, + contentType, + language, + destination: statsDestination, + producer: producerName, + }, + } as ResonanceBeaconConfig; +}; + export const buildReverbAnalyticsModel = ({ appName, campaigns, diff --git a/src/app/components/ATIAnalytics/canonical/index.test.tsx b/src/app/components/ATIAnalytics/canonical/index.test.tsx index efcd37cd128..83e34c8a4e4 100644 --- a/src/app/components/ATIAnalytics/canonical/index.test.tsx +++ b/src/app/components/ATIAnalytics/canonical/index.test.tsx @@ -8,7 +8,7 @@ import { addSendStaticBeaconToWindow } from '#app/lib/analyticsUtils/staticATITr import processClientDeviceAndSendStaticBeacon from '#app/lib/analyticsUtils/staticATITracking/processClientDeviceAndSendStaticBeacon'; import * as beacon from '../../../lib/analyticsUtils/sendBeacon'; import CanonicalATIAnalytics from '.'; -import { ReverbBeaconConfig } from '../types'; +import { ResonanceBeaconConfig, ReverbBeaconConfig } from '../types'; describe('Canonical ATI Analytics', () => { afterEach(() => { @@ -53,6 +53,24 @@ describe('Canonical ATI Analytics', () => { }, } as ReverbBeaconConfig; + const mockResonanceParams = { + resonanceProperties: { mode: 'test' }, + baseProperties: { + app: { name: 'news-pidgin' }, + destination: 'statsDestination', + pageName: 'pidgin.articles.c0000000001o.page', + producer: 'PIDGIN', + siteId: 12345, + }, + pageviewProperties: { + contentId: 'urn:bbc:optimo:asset:c0000000001o', + contentType: 'article', + language: 'pcm', + destination: 'statsDestination', + producer: 'PIDGIN', + }, + } as ResonanceBeaconConfig; + const mockSendBeacon = jest.fn().mockReturnValue('beacon-return-value'); // @ts-expect-error - we need to mock these functions to ensure tests are deterministic beacon.default = mockSendBeacon; @@ -125,6 +143,54 @@ describe('Canonical ATI Analytics', () => { expect(mockSendBeacon).not.toHaveBeenCalled(); }); + it('should call sendBeacon with resonanceParams when provided', () => { + jest.spyOn(isOperaProxy, 'default').mockImplementation(() => false); + + act(() => { + render( + , + ); + }); + + expect(mockSendBeacon).toHaveBeenCalledWith( + mockReverbParams, + mockResonanceParams, + ); + }); + + it('should call sendBeacon with undefined resonanceParams when not provided', () => { + jest.spyOn(isOperaProxy, 'default').mockImplementation(() => false); + + act(() => { + render( + , + ); + }); + + expect(mockSendBeacon).toHaveBeenCalledWith(mockReverbParams, undefined); + }); + + it('should call sendBeacon with null resonanceParams when provided as null', () => { + jest.spyOn(isOperaProxy, 'default').mockImplementation(() => false); + + act(() => { + render( + , + ); + }); + + expect(mockSendBeacon).toHaveBeenCalledWith(mockReverbParams, null); + }); + it('should render a noscript image for non-JS users', () => { const { container } = render( , diff --git a/src/app/components/ATIAnalytics/canonical/index.tsx b/src/app/components/ATIAnalytics/canonical/index.tsx index f9c453ec828..3091d448595 100644 --- a/src/app/components/ATIAnalytics/canonical/index.tsx +++ b/src/app/components/ATIAnalytics/canonical/index.tsx @@ -38,7 +38,10 @@ const addScript = ({ script, parameters, nonce }: InlineScriptProps) => { return {addInlineScript({ script, parameters, nonce })}; }; -const CanonicalATIAnalytics = ({ reverbParams }: ATIAnalyticsProps) => { +const CanonicalATIAnalytics = ({ + reverbParams, + resonanceParams, +}: ATIAnalyticsProps) => { const { isLite, nonce } = use(RequestContext); usePWAInstallTracker(); @@ -48,10 +51,11 @@ const CanonicalATIAnalytics = ({ reverbParams }: ATIAnalyticsProps) => { usePWAOfflineTracking(); const [reverbBeaconConfig] = useState(reverbParams); + const [resonanceBeaconConfig] = useState(resonanceParams); useEffect(() => { - if (!isOperaProxy()) sendBeacon(reverbBeaconConfig); - }, [reverbBeaconConfig]); + if (!isOperaProxy()) sendBeacon(reverbBeaconConfig, resonanceBeaconConfig); + }, [reverbBeaconConfig, resonanceBeaconConfig]); const liteSiteReverbURL = reverbUrlHelper.getLitePageViewUrl(reverbParams); const operaMiniPageViewReverbURL = diff --git a/src/app/components/ATIAnalytics/index.client.test.tsx b/src/app/components/ATIAnalytics/index.client.test.tsx index 8f9d3a032e5..1c1464e696b 100644 --- a/src/app/components/ATIAnalytics/index.client.test.tsx +++ b/src/app/components/ATIAnalytics/index.client.test.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-template-curly-in-string */ +import { PageTypes } from '#app/models/types/global'; import { articleDataNews } from '#pages/ArticlePage/fixtureData'; import styUkrainianAssetData from '#data/ukrainian/cpsAssets/news-53561143.json'; import styUkrainianInRussianAssetData from '#data/ukrainian/cpsAssets/features-russian-53477115.json'; @@ -292,7 +293,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: true, pageData: mapAssetData, pageType: MEDIA_ASSET_PAGE, @@ -341,7 +342,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: false, pageData: pglAssetData, pageType: PHOTO_GALLERY_PAGE, @@ -396,7 +397,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: true, pageData: pglAssetData, pageType: PHOTO_GALLERY_PAGE, @@ -445,7 +446,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: false, pageData: styAssetData, pageType: STORY_PAGE, @@ -499,7 +500,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: true, pageData: styAssetData, pageType: STORY_PAGE, @@ -554,7 +555,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata, + pageMetadata: { ...pageMetadata, type: pageMetadata.type as PageTypes }, isAmp: true, pageData: styAssetData, pageType: CORRESPONDENT_STORY_PAGE, @@ -601,7 +602,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: false, pageData: styUkrainianAssetData, pageType: STORY_PAGE, @@ -660,7 +661,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: true, pageData: styUkrainianAssetData, pageType: STORY_PAGE, @@ -707,7 +708,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: false, pageData: styUkrainianInRussianAssetData, pageType: STORY_PAGE, @@ -766,7 +767,7 @@ describe('ATI Analytics Container', () => { render(, { ...defaultRenderProps, - pageMetadata: { atiAnalytics, type }, + pageMetadata: { atiAnalytics, type: type as PageTypes }, isAmp: true, pageData: styUkrainianInRussianAssetData, pageType: STORY_PAGE, @@ -922,4 +923,94 @@ describe('ATI Analytics Container', () => { ).toEqual(1); }); }); + + describe('Resonance', () => { + it('should pass resonanceParams to CanonicalATIAnalytics for services with useResonance', () => { + const mockCanonical = jest.fn().mockReturnValue('canonical-return-value'); + // @ts-expect-error - we need to mock these functions to ensure tests are deterministic + canonical.default = mockCanonical; + + const { + metadata: { atiAnalytics, type }, + } = articleDataNews; + + render(, { + ...defaultRenderProps, + pageMetadata: { atiAnalytics, type }, + isAmp: false, + pageData: articleDataNews, + pageType: ARTICLE_PAGE, + service: 'arabic', + }); + + const { resonanceParams } = mockCanonical.mock.calls[0][0]; + + expect(resonanceParams).toEqual({ + baseProperties: { + app: { name: 'news-arabic' }, + destination: 'WS_NEWS_LANGUAGES_TEST', + hashedUserId: undefined, + pageName: 'news.articles.c0000000001o.page', + producer: 'ARABIC', + siteId: 598343, + }, + pageviewProperties: { + contentId: 'urn:bbc:optimo:c0000000001o', + contentType: 'article', + destination: 'WS_NEWS_LANGUAGES_TEST', + language: 'en-gb', + producer: 'ARABIC', + }, + resonanceProperties: { + mode: 'test', + }, + }); + }); + + it('should pass null resonanceParams to CanonicalATIAnalytics for services without useResonance', () => { + const mockCanonical = jest.fn().mockReturnValue('canonical-return-value'); + // @ts-expect-error - we need to mock these functions to ensure tests are deterministic + canonical.default = mockCanonical; + + const { + metadata: { atiAnalytics, type }, + } = articleDataNews; + + render(, { + ...defaultRenderProps, + pageMetadata: { atiAnalytics, type }, + isAmp: false, + pageData: articleDataNews, + pageType: ARTICLE_PAGE, + service: 'news', + }); + + const { resonanceParams } = mockCanonical.mock.calls[0][0]; + + expect(resonanceParams).toBeNull(); + }); + + it('should not pass resonanceParams to AmpATIAnalytics', () => { + const mockAmp = jest.fn().mockReturnValue('amp-return-value'); + // @ts-expect-error - we need to mock these functions to ensure tests are deterministic + amp.default = mockAmp; + + const { + metadata: { atiAnalytics, type }, + } = articleDataNews; + + render(, { + ...defaultRenderProps, + pageMetadata: { atiAnalytics, type }, + isAmp: true, + pageData: articleDataNews, + pageType: ARTICLE_PAGE, + service: 'arabic', + }); + + const ampProps = mockAmp.mock.calls[0][0]; + + expect(ampProps).not.toHaveProperty('resonanceParams'); + }); + }); }); diff --git a/src/app/components/ATIAnalytics/index.tsx b/src/app/components/ATIAnalytics/index.tsx index 9b5557dbc32..e807ea4ac4a 100644 --- a/src/app/components/ATIAnalytics/index.tsx +++ b/src/app/components/ATIAnalytics/index.tsx @@ -6,10 +6,8 @@ import AmpATIAnalytics from './amp'; import AmpGeo from '../../legacy/components/AmpGeo'; const ATIAnalytics = () => { - const requestContext = use(RequestContext); - const { isAmp } = requestContext; - - const { reverbParams } = use(ReverbParamsContext); + const { isAmp } = use(RequestContext); + const { reverbParams, resonanceParams } = use(ReverbParamsContext); return isAmp ? ( <> @@ -17,7 +15,10 @@ const ATIAnalytics = () => { ) : ( - + ); }; diff --git a/src/app/components/ATIAnalytics/params/buildParams/index.test.ts b/src/app/components/ATIAnalytics/params/buildParams/index.test.ts index 073c08074c9..278bc985015 100644 --- a/src/app/components/ATIAnalytics/params/buildParams/index.test.ts +++ b/src/app/components/ATIAnalytics/params/buildParams/index.test.ts @@ -1,7 +1,7 @@ import { TOPIC_PAGE } from '#app/routes/utils/pageTypes'; import { RequestContextProps } from '../../../../contexts/RequestContext'; import { ServiceConfig } from '../../../../models/types/serviceConfig'; -import { buildPageATIParams } from '.'; +import { buildPageATIParams, buildAnalyticsParams } from '.'; jest .spyOn(document, 'referrer', 'get') @@ -13,6 +13,7 @@ const requestContext: RequestContextProps = { platform: 'canonical', statsDestination: 'statsDestination', id: 'validId', + siteId: 12345, }; // @ts-expect-error - only partial data required for testing purposes @@ -56,6 +57,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'pidgin', + siteId: 12345, statsDestination: 'statsDestination', timePublished: undefined, timeUpdated: undefined, @@ -122,6 +124,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'burmese', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2023-07-13T05:03:56.214Z', timeUpdated: '2023-07-13T08:35:47.388Z', @@ -211,6 +214,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'hausa', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2023-07-11T17:42:48.771Z', timeUpdated: '2023-07-11T17:42:48.771Z', @@ -258,6 +262,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'pidgin', + siteId: 12345, statsDestination: 'statsDestination', timePublished: undefined, timeUpdated: undefined, @@ -314,6 +319,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'pidgin', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2023-08-01T12:00:00Z', timeUpdated: '2023-08-01T12:15:00Z', @@ -397,6 +403,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'mundo', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2023-02-10T02:00:41.000Z', timeUpdated: '2023-02-10T02:00:41.000Z', @@ -474,6 +481,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'mundo', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2017-09-14T14:09:14.000Z', timeUpdated: '2017-09-14T14:09:14.000Z', @@ -549,6 +557,7 @@ describe('implementation of buildPageATIParams', () => { producerId: 'atiAnalyticsProducerId', producerName: 'atiAnalyticsProducerName', service: 'mundo', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2016-08-07T09:21:02.000Z', timeUpdated: '2016-08-07T09:21:02.000Z', @@ -591,6 +600,7 @@ describe('implementation of buildPageATIParams', () => { pageTitle: "Tech Tent: The new 'space race' for computer chips", producerId: '64', producerName: 'NEWS', + siteId: 12345, timePublished: '2021-03-05T13:37:50.000Z', timeUpdated: '2021-03-05T13:37:50.000Z', }; @@ -619,6 +629,7 @@ describe('implementation of buildPageATIParams', () => { producerId: '64', producerName: 'atiAnalyticsProducerName', service: 'news', + siteId: 12345, statsDestination: 'statsDestination', timePublished: '2021-03-05T13:37:50.000Z', timeUpdated: '2021-03-05T13:37:50.000Z', @@ -676,3 +687,102 @@ describe('implementation of buildPageATIParams', () => { }); }); }); + +describe('buildAnalyticsParams', () => { + const atiData = { + contentId: 'urn:bbc:tipo:topic:c95y35941vrt', + contentType: 'index-category', + pageIdentifier: 'pidgin.topics.c95y35941vrt.page', + pageTitle: 'Donald Trump', + }; + + it('should return null for resonanceParams when useResonance is not set', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext, + // @ts-expect-error - invalid type required for testing purposes + serviceContext: { ...serviceContext, useResonance: null }, + }); + + expect(resonanceParams).toBeNull(); + }); + + it('should return null for resonanceParams when useResonance is false', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext, + serviceContext: { ...serviceContext, useResonance: false }, + }); + + expect(resonanceParams).toBeNull(); + }); + + it('should return resonanceParams when useResonance is true', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext, + serviceContext: { ...serviceContext, useResonance: true }, + }); + + expect(resonanceParams).not.toBeNull(); + expect(resonanceParams).toHaveProperty('resonanceProperties'); + expect(resonanceParams).toHaveProperty('baseProperties'); + expect(resonanceParams).toHaveProperty('pageviewProperties'); + }); + + it('should return resonanceParams when useResonance is true and platform is app', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext: { ...requestContext, platform: 'app' }, + serviceContext: { ...serviceContext, useResonance: true }, + }); + + expect(resonanceParams).not.toBeNull(); + }); + + it('should return resonanceParams when useResonance is true and platform is canonical', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext: { ...requestContext, platform: 'canonical' }, + serviceContext: { ...serviceContext, useResonance: true }, + }); + + expect(resonanceParams).not.toBeNull(); + }); + + it('should return null for resonanceParams when useResonance is true but platform is amp', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext: { ...requestContext, platform: 'amp' }, + serviceContext: { ...serviceContext, useResonance: true }, + }); + + expect(resonanceParams).toBeNull(); + }); + + it('should return null for resonanceParams when useResonance is true but platform is lite', () => { + const { resonanceParams } = buildAnalyticsParams({ + atiData, + requestContext: { ...requestContext, platform: 'lite' }, + serviceContext: { ...serviceContext, useResonance: true }, + }); + + expect(resonanceParams).toBeNull(); + }); + + it('should always return reverbParams regardless of useResonance', () => { + const withResonance = buildAnalyticsParams({ + atiData, + requestContext, + serviceContext: { ...serviceContext, useResonance: true }, + }); + const withoutResonance = buildAnalyticsParams({ + atiData, + requestContext, + serviceContext: { ...serviceContext, useResonance: false }, + }); + + expect(withResonance.reverbParams).toBeDefined(); + expect(withoutResonance.reverbParams).toBeDefined(); + }); +}); diff --git a/src/app/components/ATIAnalytics/params/buildParams/index.ts b/src/app/components/ATIAnalytics/params/buildParams/index.ts index 30f12c183c4..546a433c00c 100644 --- a/src/app/components/ATIAnalytics/params/buildParams/index.ts +++ b/src/app/components/ATIAnalytics/params/buildParams/index.ts @@ -1,5 +1,8 @@ import { LIBRARY_VERSION } from '../../../../lib/analyticsUtils'; -import { buildReverbAnalyticsModel } from '../../atiUrl'; +import { + buildReverbAnalyticsModel, + buildResonanceAnalyticsModel, +} from '../../atiUrl'; import { ATIDataWithContexts } from '../../types'; export const buildPageATIParams = ({ @@ -12,7 +15,7 @@ export const buildPageATIParams = ({ isSignedIn?: boolean; hashedId?: string | null; }) => { - const { isUK, platform, statsDestination } = requestContext; + const { isUK, platform, statsDestination, siteId } = requestContext; const { atiAnalyticsAppName, atiAnalyticsProducerId, @@ -58,6 +61,7 @@ export const buildPageATIParams = ({ producerName: atiAnalyticsProducerName, service, statsDestination, + siteId, timePublished, timeUpdated, isSignedIn, @@ -68,16 +72,18 @@ export const buildPageATIParams = ({ }; }; -export const buildPageReverbParams = ({ +type BuildPageParamsArgs = ATIDataWithContexts & { + isSignedIn?: boolean; + hashedId?: string | null; +}; + +const buildPageReverbParams = ({ atiData, requestContext, serviceContext, isSignedIn, hashedId, -}: ATIDataWithContexts & { - isSignedIn?: boolean; - hashedId?: string | null; -}) => +}: BuildPageParamsArgs) => buildReverbAnalyticsModel( buildPageATIParams({ atiData, @@ -87,3 +93,53 @@ export const buildPageReverbParams = ({ hashedId, }), ); + +const buildPageResonanceParams = ({ + atiData, + requestContext, + serviceContext, + isSignedIn, + hashedId, +}: BuildPageParamsArgs) => + buildResonanceAnalyticsModel( + buildPageATIParams({ + atiData, + requestContext, + serviceContext, + isSignedIn, + hashedId, + }), + ); + +export const buildAnalyticsParams = ({ + atiData, + requestContext, + serviceContext, + isSignedIn, + hashedId, +}: BuildPageParamsArgs) => { + const { useResonance } = serviceContext; + const { platform } = requestContext; + + const sendResonanceEvents = + useResonance && (platform === 'canonical' || platform === 'app'); + + return { + reverbParams: buildPageReverbParams({ + atiData, + requestContext, + serviceContext, + isSignedIn, + hashedId, + }), + resonanceParams: sendResonanceEvents + ? buildPageResonanceParams({ + atiData, + requestContext, + serviceContext, + isSignedIn, + hashedId, + }) + : null, + }; +}; diff --git a/src/app/components/ATIAnalytics/params/index.test.ts b/src/app/components/ATIAnalytics/params/index.test.ts index d85e086c464..c5b0cd3138c 100644 --- a/src/app/components/ATIAnalytics/params/index.test.ts +++ b/src/app/components/ATIAnalytics/params/index.test.ts @@ -118,13 +118,13 @@ const cpsPGLPageAnalyticsData: ATIData = { describe('ATIAnalytics params', () => { describe('buildReverbParams', () => { it('should return the correct page view tracking params for an article page', () => { - const params = buildReverbParams({ + const { reverbParams } = buildReverbParams({ requestContext: { ...requestContext, pageType: ARTICLE_PAGE }, atiData: articlePageAnalyticsData, serviceContext, }); - expect(params).toEqual({ + expect(reverbParams).toEqual({ params: { page: { contentId: 'urn:bbc:optimo:asset:crgrx86em6yo', @@ -162,13 +162,13 @@ describe('ATIAnalytics params', () => { }); it('should return the correct page view tracking params for a home page', () => { - const params = buildReverbParams({ + const { reverbParams } = buildReverbParams({ requestContext: { ...requestContext, pageType: HOME_PAGE }, atiData: homePageAnalyticsData, serviceContext, }); - expect(params).toEqual({ + expect(reverbParams).toEqual({ params: { page: { contentId: 'urn:bbc:tipo:topic:cm7682qz7v1t', @@ -200,13 +200,13 @@ describe('ATIAnalytics params', () => { }); it('should return the correct page view tracking params for a media article page', () => { - const params = buildReverbParams({ + const { reverbParams } = buildReverbParams({ requestContext: { ...requestContext, pageType: MEDIA_ARTICLE_PAGE }, atiData: mediaArticlePageAnalyticsData, serviceContext, }); - expect(params).toEqual({ + expect(reverbParams).toEqual({ params: { page: { contentId: 'urn:bbc:optimo:asset:c4nrpd0d4nro', @@ -244,13 +244,13 @@ describe('ATIAnalytics params', () => { }); it('should return the correct page view tracking params for a MAP page', () => { - const params = buildReverbParams({ + const { reverbParams } = buildReverbParams({ requestContext: { ...requestContext, pageType: MEDIA_ASSET_PAGE }, atiData: cpsMAPPageAnalyticsData, serviceContext, }); - expect(params).toEqual({ + expect(reverbParams).toEqual({ params: { page: { contentId: 'urn:bbc:cps:4d36f80b-8711-0b4e-8da0-ef76ae8ac470', @@ -287,13 +287,13 @@ describe('ATIAnalytics params', () => { }); it('should return the correct page view tracking params for a PGL page', () => { - const params = buildReverbParams({ + const { reverbParams } = buildReverbParams({ requestContext: { ...requestContext, pageType: PHOTO_GALLERY_PAGE }, atiData: cpsPGLPageAnalyticsData, serviceContext, }); - expect(params).toEqual({ + expect(reverbParams).toEqual({ params: { page: { contentId: diff --git a/src/app/components/ATIAnalytics/params/index.ts b/src/app/components/ATIAnalytics/params/index.ts index b84fe4a8007..aafe67ce807 100644 --- a/src/app/components/ATIAnalytics/params/index.ts +++ b/src/app/components/ATIAnalytics/params/index.ts @@ -1,4 +1,4 @@ -import { buildPageReverbParams } from './buildParams'; +import { buildAnalyticsParams } from './buildParams'; import { ReverbDetailsProviders } from '../types'; export default ({ @@ -10,12 +10,11 @@ export default ({ }: ReverbDetailsProviders & { isSignedIn?: boolean; hashedId?: string | null; -}) => { - return buildPageReverbParams({ +}) => + buildAnalyticsParams({ atiData, requestContext, serviceContext, isSignedIn, hashedId, }); -}; diff --git a/src/app/components/ATIAnalytics/types.ts b/src/app/components/ATIAnalytics/types.ts index bebdbf8b4a1..21b043d2816 100644 --- a/src/app/components/ATIAnalytics/types.ts +++ b/src/app/components/ATIAnalytics/types.ts @@ -1,4 +1,9 @@ /* eslint-disable camelcase */ +import type { + ResonanceProperties, + PageviewProperties, + BaseProperties, +} from '@bbc/resonance'; import { PageTypes, Platforms, Services } from '../../models/types/global'; import { RequestContextProps } from '../../contexts/RequestContext'; import { ServiceConfig } from '../../models/types/serviceConfig'; @@ -137,6 +142,13 @@ export type ReverbEventDetails = { originalEvent?: Event; }; +// possible task - type this ourselves and not rely on imported types +export type ResonanceBeaconConfig = { + resonanceProperties: ResonanceProperties; + pageviewProperties: PageviewProperties; + baseProperties: BaseProperties; +}; + export type ReverbBeaconConfig = { params: { page: ReverbPageVars; user: ReverbUserVars }; eventDetails: ReverbEventDetails; @@ -144,6 +156,7 @@ export type ReverbBeaconConfig = { export interface ATIAnalyticsProps { reverbParams: ReverbBeaconConfig; + resonanceParams?: ResonanceBeaconConfig | null; } export interface ATIEventTrackingProps { @@ -205,6 +218,7 @@ export interface ATIPageTrackingProps { libraryVersion?: string; platform?: Platforms; statsDestination?: string; + siteId?: number; timePublished?: string | null; timeUpdated?: string | null; categoryName?: string | null; diff --git a/src/app/contexts/RequestContext/getSiteId/index.test.ts b/src/app/contexts/RequestContext/getSiteId/index.test.ts new file mode 100644 index 00000000000..7e50d8c6270 --- /dev/null +++ b/src/app/contexts/RequestContext/getSiteId/index.test.ts @@ -0,0 +1,31 @@ +import getSiteId from '.'; + +describe('getSiteId', () => { + describe('default services', () => { + it('should return the live siteId when env is live', () => { + expect(getSiteId({ env: 'live', service: 'pidgin' })).toBe(598342); + }); + + it('should return the test siteId when env is test', () => { + expect(getSiteId({ env: 'test', service: 'pidgin' })).toBe(598343); + }); + + it('should default to the test siteId when env is not provided', () => { + expect(getSiteId({ service: 'pidgin' })).toBe(598343); + }); + + it('should default to the test siteId when env is null', () => { + expect(getSiteId({ env: null, service: 'pidgin' })).toBe(598343); + }); + }); + + describe('japanese service', () => { + it('should return the japanese live siteId when env is live', () => { + expect(getSiteId({ env: 'live', service: 'japanese' })).toBe(646753); + }); + + it('should return the japanese test siteId when env is test', () => { + expect(getSiteId({ env: 'test', service: 'japanese' })).toBe(598290); + }); + }); +}); diff --git a/src/app/contexts/RequestContext/getSiteId/index.ts b/src/app/contexts/RequestContext/getSiteId/index.ts new file mode 100644 index 00000000000..f7500685883 --- /dev/null +++ b/src/app/contexts/RequestContext/getSiteId/index.ts @@ -0,0 +1,20 @@ +import { Environments, Services } from '#app/models/types/global'; + +type Props = { + env?: Environments | null; + service: Services; +}; + +const getSiteId = ({ env = 'test', service }: Props) => { + let siteId: number; + switch (service) { + case 'japanese': + siteId = env === 'live' ? 646753 : 598290; + break; + default: + siteId = env === 'live' ? 598342 : 598343; + } + return siteId; +}; + +export default getSiteId; diff --git a/src/app/contexts/RequestContext/index.test.tsx b/src/app/contexts/RequestContext/index.test.tsx index 4f17b26f049..c984b40af5e 100644 --- a/src/app/contexts/RequestContext/index.test.tsx +++ b/src/app/contexts/RequestContext/index.test.tsx @@ -81,6 +81,7 @@ const expectedOutput = { ampNonUkLink: 'ampNonUkLink', showAdsBasedOnLocation: input.showAdsBasedOnLocation, showCookieBannerBasedOnCountry: true, + siteId: 598343, service: 'service', pathname: '/current-path', serverSideExperiments: input.serverSideExperiments, diff --git a/src/app/contexts/RequestContext/index.tsx b/src/app/contexts/RequestContext/index.tsx index b40d1908c49..40419b1b7bc 100644 --- a/src/app/contexts/RequestContext/index.tsx +++ b/src/app/contexts/RequestContext/index.tsx @@ -11,6 +11,7 @@ import getStatsDestination from './getStatsDestination'; import getOriginContext from './getOriginContext'; import getEnv from './getEnv'; import getMetaUrls from './getMetaUrls'; +import getSiteId from './getSiteId'; export type RequestContextProps = { ampLink: string; @@ -36,6 +37,7 @@ export type RequestContextProps = { showAdsBasedOnLocation: boolean; showCookieBannerBasedOnCountry: boolean; statsDestination: string; + siteId: number; statusCode: number | null; timeOnServer: number | null; variant: Variants | null; @@ -120,6 +122,8 @@ export const RequestContextProvider = ({ service, }); + const siteId = getSiteId({ env, service }); + const value = useMemo( () => ({ env, @@ -134,6 +138,7 @@ export const RequestContextProvider = ({ isNextJs, platform, statsDestination, + siteId, statusCode, variant, timeOnServer, @@ -165,6 +170,7 @@ export const RequestContextProvider = ({ showAdsBasedOnLocation, showCookieBannerBasedOnCountry, statsDestination, + siteId, statusCode, timeOnServer, variant, diff --git a/src/app/contexts/ReverbParamsContext/index.test.tsx b/src/app/contexts/ReverbParamsContext/index.test.tsx index 129f4312e82..9f63bec390b 100644 --- a/src/app/contexts/ReverbParamsContext/index.test.tsx +++ b/src/app/contexts/ReverbParamsContext/index.test.tsx @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import { use } from 'react'; +import { ResonanceBeaconConfig } from '#app/components/ATIAnalytics/types'; import { render, screen, @@ -11,6 +12,7 @@ import { } from '../../routes/utils/pageTypes'; import { ReverbParamsContext, PageMetadata } from '.'; import * as useOptimizelyVariation from '../../hooks/useOptimizelyVariation'; +import * as buildAnalyticsParamsModule from '../../components/ATIAnalytics/params'; const pageMetadata = { atiAnalytics: { @@ -98,6 +100,7 @@ describe('ReverbParamsContext', () => { }, }, }, + resonanceParams: null, }); }); @@ -149,6 +152,7 @@ describe('ReverbParamsContext', () => { }, }, }, + resonanceParams: null, }); }); @@ -200,6 +204,7 @@ describe('ReverbParamsContext', () => { }, }, }, + resonanceParams: null, }); }); @@ -251,6 +256,7 @@ describe('ReverbParamsContext', () => { }, }, }, + resonanceParams: null, }); }); @@ -306,6 +312,7 @@ describe('ReverbParamsContext', () => { }, }, }, + resonanceParams: null, experimentProps: { experimentName: 'test_page_views_aa_3', experimentVariant: 'experimentVariant', @@ -313,4 +320,46 @@ describe('ReverbParamsContext', () => { }, }); }); + + it('should provide resonanceParams to child components when useResonance is enabled', () => { + const mockResonanceParams = { + resonanceProperties: { mode: 'test' }, + baseProperties: { + app: { name: 'news-pidgin' }, + destination: 'WS_NEWS_LANGUAGES_TEST', + pageName: 'news::pidgin.news.story.51745682.page', + producer: 'PIDGIN', + siteId: 598343, + }, + pageviewProperties: { + contentId: + 'urn:bbc:cps:curie:asset:53870d86-88c5-6f4d-a260-f97c68606458', + contentType: 'article', + language: 'pcm', + destination: 'WS_NEWS_LANGUAGES_TEST', + producer: 'PIDGIN', + }, + } as unknown as ResonanceBeaconConfig; + + jest.spyOn(buildAnalyticsParamsModule, 'default').mockReturnValue({ + reverbParams: { + params: { + page: { name: 'news::pidgin.news.story.51745682.page' }, + user: { isSignedIn: false }, + }, + eventDetails: { eventName: 'pageView' }, + }, + resonanceParams: mockResonanceParams, + }); + + render(, { + pageMetadata, + service: 'pidgin', + }); + + const testEl = screen.getByTestId('test-component'); + const contextValue = JSON.parse(testEl.textContent as string); + + expect(contextValue.resonanceParams).toEqual(mockResonanceParams); + }); }); diff --git a/src/app/contexts/ReverbParamsContext/index.tsx b/src/app/contexts/ReverbParamsContext/index.tsx index 641bd464f59..23596ee030f 100644 --- a/src/app/contexts/ReverbParamsContext/index.tsx +++ b/src/app/contexts/ReverbParamsContext/index.tsx @@ -9,10 +9,11 @@ import { RequestContext } from '#app/contexts/RequestContext'; import { ServiceContext } from '#app/contexts/ServiceContext'; import { AccountContext } from '#app/contexts/AccountContext'; import withOptimizelyProvider from '#app/legacy/containers/PageHandlers/withOptimizelyProvider'; -import buildReverbParams from '#app/components/ATIAnalytics/params'; +import buildAnalyticsParams from '#app/components/ATIAnalytics/params'; import { ATIData, ReverbBeaconConfig, + ResonanceBeaconConfig, } from '#app/components/ATIAnalytics/types'; import { ARTICLE_PAGE, @@ -31,6 +32,7 @@ import getEnrichedMediaArticleATIData from './getEnrichedMediaArticleATIData'; type ReverbParamsContextProps = { reverbParams: ReverbBeaconConfig; + resonanceParams: ResonanceBeaconConfig | null; experimentProps?: ComponentExperimentProps; }; @@ -86,7 +88,7 @@ const ReverbParamsContextProviderComponent = ({ pageType: requestContext?.pageType, }); - const reverbParams = buildReverbParams({ + const { reverbParams, resonanceParams } = buildAnalyticsParams({ requestContext, serviceContext, atiData: enrichedAtiData, @@ -105,11 +107,12 @@ const ReverbParamsContextProviderComponent = ({ const value = useMemo( () => ({ reverbParams, + resonanceParams, ...(enrichedAtiData?.experimentProps && { experimentProps: enrichedAtiData.experimentProps, }), }), - [reverbParams, enrichedAtiData?.experimentProps], + [reverbParams, resonanceParams, enrichedAtiData?.experimentProps], ); return ( diff --git a/src/app/lib/analyticsUtils/sendBeacon/index.test.ts b/src/app/lib/analyticsUtils/sendBeacon/index.test.ts index 8f00ca37127..b8075ab6843 100644 --- a/src/app/lib/analyticsUtils/sendBeacon/index.test.ts +++ b/src/app/lib/analyticsUtils/sendBeacon/index.test.ts @@ -1,7 +1,11 @@ /* eslint-disable global-require */ +import { Resonance } from '@bbc/resonance'; import loggerMock from '#testHelpers/loggerMock'; import { ATI_LOGGING_ERROR } from '#app/lib/logger.const'; -import { ReverbBeaconConfig } from '#app/components/ATIAnalytics/types'; +import { + ReverbBeaconConfig, + ResonanceBeaconConfig, +} from '#app/components/ATIAnalytics/types'; import { waitFor } from '#app/components/react-testing-library-with-providers'; import sendBeacon from './index'; import * as onClient from '../../utilities/onClient'; @@ -206,4 +210,71 @@ describe('sendBeacon', () => { }); }); }); + + describe('Resonance', () => { + const reverbConfig = { + params: { page: 'page', user: '1234-5678' }, + eventDetails: { eventName: 'pageView' }, + } as unknown as ReverbBeaconConfig; + + const resonanceConfig = { + resonanceProperties: { mode: 'test' }, + baseProperties: { + app: { name: 'news-pidgin' }, + destination: 'statsDestination', + pageName: 'pidgin.page', + producer: 'PIDGIN', + siteId: 598343, + }, + pageviewProperties: { + contentId: 'urn:bbc:optimo:asset:c0000000001o', + contentType: 'article', + language: 'pcm', + destination: 'statsDestination', + producer: 'PIDGIN', + }, + } as unknown as ResonanceBeaconConfig; + + it('should call Resonance.initialise with the correct params when resonanceBeaconConfig is provided', async () => { + await sendBeacon(reverbConfig, resonanceConfig); + + expect(Resonance.initialise).toHaveBeenCalledTimes(1); + expect(Resonance.initialise).toHaveBeenCalledWith( + resonanceConfig.resonanceProperties, + resonanceConfig.baseProperties, + resonanceConfig.pageviewProperties, + ); + }); + + it('should not call Resonance.initialise when resonanceBeaconConfig is null', async () => { + await sendBeacon(reverbConfig, null); + + expect(Resonance.initialise).not.toHaveBeenCalled(); + }); + + it('should send error to the logger when Resonance.initialise throws', async () => { + const error = new Error('Resonance failed'); + (Resonance.initialise as jest.Mock).mockImplementationOnce(() => { + throw error; + }); + + await sendBeacon(reverbConfig, resonanceConfig); + + expect(loggerMock.error).toHaveBeenCalledWith(ATI_LOGGING_ERROR, { + error: new Error(`Error initialising Resonance: ${error}`), + }); + }); + + it('should still call Reverb when Resonance.initialise throws', async () => { + // eslint-disable-next-line no-underscore-dangle + window.__reverb = { __reverbLoadedPromise: Promise.resolve(reverbMock) }; + (Resonance.initialise as jest.Mock).mockImplementationOnce(() => { + throw new Error('Resonance failed'); + }); + + await sendBeacon(reverbConfig, resonanceConfig); + + expect(reverbMock.viewEvent).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/app/lib/analyticsUtils/sendBeacon/index.ts b/src/app/lib/analyticsUtils/sendBeacon/index.ts index 26c43469d90..5988b71d104 100644 --- a/src/app/lib/analyticsUtils/sendBeacon/index.ts +++ b/src/app/lib/analyticsUtils/sendBeacon/index.ts @@ -1,6 +1,7 @@ import { ReverbClient } from '#app/models/types/eventTracking'; import { ReverbBeaconConfig, + ResonanceBeaconConfig, ReverbEventDetails, } from '#app/components/ATIAnalytics/types'; import onClient from '../../utilities/onClient'; @@ -77,11 +78,38 @@ const callReverb = async (eventDetails: ReverbEventDetails) => { ); }; -const sendBeacon = async (reverbBeaconConfig: ReverbBeaconConfig) => { +const callResonance = ( + Resonance: typeof import('@bbc/resonance').Resonance, + resonanceParams: ResonanceBeaconConfig, +) => { + try { + Resonance.initialise( + resonanceParams.resonanceProperties, + resonanceParams.baseProperties, + resonanceParams.pageviewProperties, + ); + } catch (error) { + throw new Error(`Error initialising Resonance: ${error}`); + } +}; + +const sendBeacon = async ( + reverbBeaconConfig: ReverbBeaconConfig, + resonanceBeaconConfig?: ResonanceBeaconConfig | null, +) => { if (onClient()) { try { const { eventDetails } = reverbBeaconConfig; + if (resonanceBeaconConfig) { + try { + const { Resonance } = await import('@bbc/resonance'); + callResonance(Resonance, resonanceBeaconConfig); + } catch (error) { + logger.error(ATI_LOGGING_ERROR, { error }); + } + } + await callReverb(eventDetails); } catch (error) { logger.error(ATI_LOGGING_ERROR, { diff --git a/src/app/lib/config/services/arabic.ts b/src/app/lib/config/services/arabic.ts index f73938bb7d7..fed46a62b40 100644 --- a/src/app/lib/config/services/arabic.ts +++ b/src/app/lib/config/services/arabic.ts @@ -530,6 +530,7 @@ export const service: DefaultServiceConfig = { url: '/arabic', }, ], + useResonance: true, }, }; diff --git a/src/app/lib/config/services/korean.ts b/src/app/lib/config/services/korean.ts index 7a707e81921..911a5496fcc 100644 --- a/src/app/lib/config/services/korean.ts +++ b/src/app/lib/config/services/korean.ts @@ -437,6 +437,7 @@ export const service: DefaultServiceConfig = { }, ], timezone: 'Asia/Seoul', + useResonance: true, }, }; diff --git a/src/app/lib/config/services/marathi.ts b/src/app/lib/config/services/marathi.ts index a5cfcb952e9..51704e3947e 100644 --- a/src/app/lib/config/services/marathi.ts +++ b/src/app/lib/config/services/marathi.ts @@ -452,6 +452,7 @@ export const service: DefaultServiceConfig = { url: '/marathi', }, ], + useResonance: true, }, }; diff --git a/src/app/models/types/serviceConfig.ts b/src/app/models/types/serviceConfig.ts index be18efe4645..20a84d0e55e 100644 --- a/src/app/models/types/serviceConfig.ts +++ b/src/app/models/types/serviceConfig.ts @@ -128,6 +128,7 @@ export type ServiceConfig = { iframeDevSrc: string; }; articleMessageBanners?: ArticleMessageBannerConfig[]; + useResonance?: boolean; }; export type ArticleMessageBannerConfig = { diff --git a/src/app/pages/ArticlePage/index.test.tsx b/src/app/pages/ArticlePage/index.test.tsx index 74490223e82..6ddb1bf3874 100644 --- a/src/app/pages/ArticlePage/index.test.tsx +++ b/src/app/pages/ArticlePage/index.test.tsx @@ -824,35 +824,38 @@ describe('Article Page', () => { expect(metadata).toEqual({ atiAnalytics, type }); expect(buildReverbParamsSpy).toHaveReturnedWith({ - eventDetails: { eventName: 'pageView' }, - params: { - env: undefined, - page: { - additionalProperties: { - app_name: 'news-pidgin', - app_type: 'responsive', - content_language: 'pcm', - product_platform: null, - referrer_url: null, - x10: null, - x11: '2018-01-01T12:01:00.000Z', - x12: '2018-01-01T14:00:00.000Z', - x13: null, - x14: null, - x16: '', - x17: null, - x18: null, - x5: null, - x8: 'simorgh', - x9: 'Article%20Headline%20for%20SEO%20in%20Pidgin%20-%20BBC%20News%20Pidgin', + resonanceParams: null, + reverbParams: { + eventDetails: { eventName: 'pageView' }, + params: { + env: undefined, + page: { + additionalProperties: { + app_name: 'news-pidgin', + app_type: 'responsive', + content_language: 'pcm', + product_platform: null, + referrer_url: null, + x10: null, + x11: '2018-01-01T12:01:00.000Z', + x12: '2018-01-01T14:00:00.000Z', + x13: null, + x14: null, + x16: '', + x17: null, + x18: null, + x5: null, + x8: 'simorgh', + x9: 'Article%20Headline%20for%20SEO%20in%20Pidgin%20-%20BBC%20News%20Pidgin', + }, + contentId: 'urn:bbc:optimo:c0000000001o', + contentType: undefined, + destination: 'WS_NEWS_LANGUAGES_TEST', + name: null, + producer: 'PIDGIN', }, - contentId: 'urn:bbc:optimo:c0000000001o', - contentType: undefined, - destination: 'WS_NEWS_LANGUAGES_TEST', - name: null, - producer: 'PIDGIN', + user: { hashedId: null, isSignedIn: false }, }, - user: { hashedId: null, isSignedIn: false }, }, }); }); @@ -897,35 +900,38 @@ describe('Article Page', () => { expect(metadata).toEqual({ atiAnalytics, type }); expect(buildReverbParamsSpy).toHaveReturnedWith({ - eventDetails: { eventName: 'pageView' }, - params: { - env: undefined, - page: { - additionalProperties: { - app_name: 'news-pidgin', - app_type: 'responsive', - content_language: 'pcm', - product_platform: null, - referrer_url: null, - x10: null, - x11: '2018-01-01T12:01:00.000Z', - x12: '2018-01-01T14:00:00.000Z', - x13: null, - x14: null, - x16: '', - x17: null, - x18: null, - x5: null, - x8: 'simorgh', - x9: 'Article%20Headline%20for%20SEO%20in%20Pidgin%20-%20BBC%20News%20Pidgin', + resonanceParams: null, + reverbParams: { + eventDetails: { eventName: 'pageView' }, + params: { + env: undefined, + page: { + additionalProperties: { + app_name: 'news-pidgin', + app_type: 'responsive', + content_language: 'pcm', + product_platform: null, + referrer_url: null, + x10: null, + x11: '2018-01-01T12:01:00.000Z', + x12: '2018-01-01T14:00:00.000Z', + x13: null, + x14: null, + x16: '', + x17: null, + x18: null, + x5: null, + x8: 'simorgh', + x9: 'Article%20Headline%20for%20SEO%20in%20Pidgin%20-%20BBC%20News%20Pidgin', + }, + contentId: 'urn:bbc:optimo:c0000000001o', + contentType: undefined, + destination: 'WS_NEWS_LANGUAGES_TEST', + name: null, + producer: 'PIDGIN', }, - contentId: 'urn:bbc:optimo:c0000000001o', - contentType: undefined, - destination: 'WS_NEWS_LANGUAGES_TEST', - name: null, - producer: 'PIDGIN', + user: { hashedId: null, isSignedIn: false }, }, - user: { hashedId: null, isSignedIn: false }, }, }); }); diff --git a/src/testHelpers/resonanceMock.ts b/src/testHelpers/resonanceMock.ts new file mode 100644 index 00000000000..3205573d88f --- /dev/null +++ b/src/testHelpers/resonanceMock.ts @@ -0,0 +1,8 @@ +export const ResonanceMode = { + LIVE: 'live', + TEST: 'test', +} as const; + +export const Resonance = { + initialise: jest.fn(), +}; diff --git a/ws-nextjs-app/jest.config.ts b/ws-nextjs-app/jest.config.ts index e97fd1688c7..b280a83dadf 100644 --- a/ws-nextjs-app/jest.config.ts +++ b/ws-nextjs-app/jest.config.ts @@ -22,9 +22,10 @@ const buildConfig = async (config: Config): Promise => { })(); }; -const moduleNameMapper = pathsToModuleNameMapper(compilerOptionsPaths, { - prefix: '/', -}); +const moduleNameMapper = { + ...pathsToModuleNameMapper(compilerOptionsPaths, { prefix: '/' }), + '^@bbc/resonance$': '/../src/testHelpers/resonanceMock.ts', +}; export default async (): Promise => { const canonicalIntegrationTests = await buildConfig({ diff --git a/ws-nextjs-app/utilities/getAmpLiteCss/index.test.ts b/ws-nextjs-app/utilities/getAmpLiteCss/index.test.ts index 964430c119b..2f0d96f61b5 100644 --- a/ws-nextjs-app/utilities/getAmpLiteCss/index.test.ts +++ b/ws-nextjs-app/utilities/getAmpLiteCss/index.test.ts @@ -24,13 +24,15 @@ describe('getAmpLiteCss utilities', () => { afterAll(() => { cwdSpy.mockRestore(); - process.env.NODE_ENV = originalNodeEnv; + (process.env as { [key: string]: string | undefined }).NODE_ENV = + originalNodeEnv; }); afterEach(() => { jest.clearAllMocks(); resetManifestCaches(); - process.env.NODE_ENV = originalNodeEnv; + (process.env as { [key: string]: string | undefined }).NODE_ENV = + originalNodeEnv; }); describe('resolveCssFilePath', () => { @@ -458,7 +460,8 @@ describe('getAmpLiteCss utilities', () => { describe('in development', () => { beforeEach(() => { - process.env.NODE_ENV = 'development'; + (process.env as { [key: string]: string | undefined }).NODE_ENV = + 'development'; }); it('returns the full dev CSS when dev-css-modules.css exists', () => { @@ -494,7 +497,8 @@ describe('getAmpLiteCss utilities', () => { describe('in production', () => { beforeEach(() => { - process.env.NODE_ENV = 'production'; + (process.env as { [key: string]: string | undefined }).NODE_ENV = + 'production'; }); it('returns combined build manifest CSS and dynamic import CSS', () => { diff --git a/yarn.lock b/yarn.lock index 6eaac2006af..d317e9403fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2213,6 +2213,15 @@ __metadata: languageName: node linkType: hard +"@bbc/resonance@https://mybbc-analytics.files.bbci.co.uk/resonance/resonance-2.7.2.tgz": + version: 2.7.2 + resolution: "@bbc/resonance@https://mybbc-analytics.files.bbci.co.uk/resonance/resonance-2.7.2.tgz" + dependencies: + uuid: "npm:14.0.0" + checksum: 10/b0bb7276ddb297c04373a3b7301f2958bf3914970309585f393bd856c27e44a04a7aff77e0310a704dcc392ad9cce14eea2f51496284e10ac462d4cd47dcf8ff + languageName: node + linkType: hard + "@bbc/reverb-url-helper@https://mybbc-analytics.files.bbci.co.uk/reverb-url-helper/bbc-reverb-url-helper-2.5.0.tgz": version: 2.5.0 resolution: "@bbc/reverb-url-helper@https://mybbc-analytics.files.bbci.co.uk/reverb-url-helper/bbc-reverb-url-helper-2.5.0.tgz" @@ -17019,6 +17028,7 @@ __metadata: "@babel/preset-env": "npm:7.29.7" "@babel/preset-react": "npm:7.29.7" "@babel/preset-typescript": "npm:7.29.7" + "@bbc/resonance": "https://mybbc-analytics.files.bbci.co.uk/resonance/resonance-2.7.2.tgz" "@bbc/reverb-url-helper": "https://mybbc-analytics.files.bbci.co.uk/reverb-url-helper/bbc-reverb-url-helper-2.5.0.tgz" "@bbc/web-vitals": "npm:2.6.0" "@cypress/webpack-preprocessor": "npm:7.1.1" @@ -18759,6 +18769,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:14.0.0": + version: 14.0.0 + resolution: "uuid@npm:14.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 10/8ee9b98f9650e25555515f7a28d3c3ae9364e72f7bb19b9e08b681bc135338beba5509b2830f6ae1cfaba4d45401da0d16d4d109b977097bc3d6ba0c5583341b + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2"