Skip to content

WS-2946: Sends Resonance page view events for Arabic, Korean and Marathi - #14169

Open
Isabella-Mitchell wants to merge 28 commits into
latestfrom
WS-2831-spike-reverb-resonance
Open

WS-2946: Sends Resonance page view events for Arabic, Korean and Marathi #14169
Isabella-Mitchell wants to merge 28 commits into
latestfrom
WS-2831-spike-reverb-resonance

Conversation

@Isabella-Mitchell

@Isabella-Mitchell Isabella-Mitchell commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Resolves JIRA: WS-2946

(Started as POC for https://bbc.atlassian.net/browse/WS-2831)

Summary

Adds Resonance library and sends page view events for Arabic, Korean and Marathi for canonical and .app. Resonance and Reverb will dual run until we are ready to fully transition.

We have validated we can see page view events come through to the BBC Activity Gateway (BAG)

Code changes

Components/ATIAnalytics

  • In index, extract Resonance Params from Reverb Context Provider. These params are passed through various children until they are passed to the sendBeacon function.
  • In params/buildParams, checks if useResonance is true for that service, and if so, builds the Resonance Params. Else Resonance params are null. (this file is called by the Reverb Context Provider).
  • In atiUrl/index.ts, builds the Resonance Params - aka the data model that we pass to Resonance.

Contexts

  • In RequestContext, Adds siteID to Request Context (ASAIK this is done automatically by Reverb. You can see a big list of these in the Reverb script. This is not done by Resonance - so we have to pass this value in)
  • In ReverbParamsContext, Updates Reverb Context Provider so that it also handles Resonance. (Will create a separate ticket to rename this context provider to something like AnalyticsContextProvider).

Lib

  • In analyticsUtils/sendBeacon, Initialises Resonance (which automatically sends pageViewEvent in sendBeacon function
  • In config/services, Adds optional useResonance boolean flag to serviceConfig. Enables for Arabic, Korean and Marathi

Other

  • In both jest.config.ts files, Uses jest moduleNameMapper to permit ESM (other Jest won't run) [copilot]
  • In package.json, Adds S3 distributed version of Resonance as a dependency (suitable for our open source repo)

If you're like me, and find the structure of our ATI files confusing, then this diagram might be helpful
ResonanceDiagram drawio

Testing

You can test this locally and on preview in the browser. As part of this work we checked page views events are going through to BAG (it is not yet self service)

Example of testing locally
Open dev tools => the network request tab
Go to a testing link
See the Resonance event fire (see screen shots for what this looks like)
Screenshot 2026-08-13 at 09 43 28
Screenshot 2026-08-13 at 09 43 35

You should see it on any arabic, korean and marathi page, e.g.

  1. http://localhost:7081/arabic?renderer_env=test
    http://localhost:7081/korean?renderer_env=test
    http://localhost:7081/marathi?renderer_env=test

You should not see it on any other service. E.g.
http://localhost:7081/mundo?renderer_env=test

Useful Links

@victranfield victranfield self-assigned this Jul 1, 2026
Comment thread src/app/components/ATIAnalytics/canonical/index.tsx Outdated
@Isabella-Mitchell Isabella-Mitchell changed the title WS-2831: Spike Reverb to Resonance WS-2831: Adds Resonance page view events for Arabic, Korean and Marathi Aug 13, 2026
@Isabella-Mitchell Isabella-Mitchell changed the title WS-2831: Adds Resonance page view events for Arabic, Korean and Marathi WS-2831: Sends Resonance page view events for Arabic, Korean and Marathi Aug 13, 2026
@Isabella-Mitchell Isabella-Mitchell changed the title WS-2831: Sends Resonance page view events for Arabic, Korean and Marathi WS-2946: Sends Resonance page view events for Arabic, Korean and Marathi Aug 13, 2026
ResonanceProperties,
PageviewProperties,
BaseProperties,
} from '@bbc/resonance';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do people feel about importing types from a package? We do this for certain dependencies like React, Storybook and Playwright - maybe this falls in a similar category? The alternative would be manually typing them here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a bbc package, I feel like there's even more reason to import them rather than rewrite them ourselves.

name: platform === 'app' ? `${appName}-app` : appName,
},
destination: statsDestination,
hashedUserId: hashedId ?? undefined,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resonance types permits hashedUserId to be a string or undefined. However reverb permits the equivalent value hashedId to be string, undefined or null. Currently, we default this to null. I'll make a note on a follow up ticket: https://bbc.atlassian.net/browse/WS-2949

@Isabella-Mitchell
Isabella-Mitchell marked this pull request as ready for review August 13, 2026 08:53
@Isabella-Mitchell
Isabella-Mitchell requested a review from a team as a code owner August 13, 2026 08:53
Copilot AI balanced review requested due to automatic review settings August 13, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Resonance page-view analytics alongside Reverb for Arabic, Korean, and Marathi canonical/app pages.

Changes:

  • Adds Resonance models, initialization, and service flags.
  • Propagates site IDs and Resonance parameters through analytics contexts.
  • Adds supporting mocks, configuration, and tests.

Reviewed changes

Copilot reviewed 28 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
package.json Adds Resonance dependency.
yarn.lock Locks Resonance and UUID packages.
jest.config.js Maps Resonance to a test mock.
ws-nextjs-app/jest.config.ts Adds Resonance mapping for Next.js tests.
ws-nextjs-app/utilities/getAmpLiteCss/index.test.ts Updates environment mutation typing.
src/testHelpers/resonanceMock.ts Provides the Resonance Jest mock.
src/app/models/types/serviceConfig.ts Adds the Resonance service flag.
src/app/lib/config/services/arabic.ts Enables Resonance for Arabic.
src/app/lib/config/services/korean.ts Enables Resonance for Korean.
src/app/lib/config/services/marathi.ts Enables Resonance for Marathi.
src/app/contexts/RequestContext/index.tsx Exposes the analytics site ID.
src/app/contexts/RequestContext/index.test.tsx Verifies the site ID context value.
src/app/contexts/RequestContext/getSiteId/index.ts Resolves environment-specific site IDs.
src/app/contexts/RequestContext/getSiteId/index.test.ts Tests site ID resolution.
src/app/contexts/ReverbParamsContext/index.tsx Provides Resonance parameters.
src/app/contexts/ReverbParamsContext/index.test.tsx Tests Resonance context propagation.
src/app/components/ATIAnalytics/types.ts Adds Resonance analytics types.
src/app/components/ATIAnalytics/params/index.ts Returns combined analytics parameters.
src/app/components/ATIAnalytics/params/index.test.ts Updates Reverb parameter assertions.
src/app/components/ATIAnalytics/params/buildParams/index.ts Conditionally builds Resonance parameters.
src/app/components/ATIAnalytics/params/buildParams/index.test.ts Tests combined parameter generation.
src/app/components/ATIAnalytics/atiUrl/index.ts Builds the Resonance data model.
src/app/components/ATIAnalytics/atiUrl/index.client.test.ts Tests Resonance model fields and modes.
src/app/components/ATIAnalytics/index.tsx Passes Resonance data to canonical analytics.
src/app/components/ATIAnalytics/index.client.test.tsx Tests service and platform propagation.
src/app/components/ATIAnalytics/canonical/index.tsx Sends Resonance with canonical page views.
src/app/components/ATIAnalytics/canonical/index.test.tsx Tests forwarding Resonance configurations.
src/app/lib/analyticsUtils/sendBeacon/index.ts Initializes Resonance alongside Reverb.
src/app/lib/analyticsUtils/sendBeacon/index.test.ts Tests Resonance initialization and failures.
src/app/pages/ArticlePage/index.test.tsx Updates analytics return-shape assertions.
Suppressed comments (1)

src/app/lib/analyticsUtils/sendBeacon/index.ts:107

  • A Resonance exception currently exits this shared try before callReverb runs. That means the new integration can suppress the existing Reverb page view, contradicting the intended dual-run behavior. Log Resonance failures independently, then always attempt Reverb.
      if (resonanceBeaconConfig) {
        callResonance(resonanceBeaconConfig);
      }

      await callReverb(eventDetails);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +130 to +138
resonanceParams: useResonance
? buildPageResonanceParams({
atiData,
requestContext,
serviceContext,
isSignedIn,
hashedId,
})
: null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have decided to do this - so params are only built for canonical and .app. (Aware the ticket only says canonical. But I think that's because .amp and .lite need a different approach. .app uses the same approach as canonical - so I think it's fine to enable this now.

The downside is that we'll need to remember to remove these conditional checks. But the upshot is (maybe) marginal performance gains?

@@ -1,6 +1,9 @@
// import resonance - we can follow the pattern of the reverbURLHelper, e,g.
import { Resonance } from '@bbc/resonance';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have also done this and added a comment to our dedicated performance spike ticket: https://bbc.atlassian.net/browse/WS-3051. An initial test it looks like this forces Resonance to fire after Reverb.

destination: statsDestination,
producer: producerName,
},
} as ResonanceBeaconConfig;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the type casting here? I would have expected typescript to infer this ok from the output type defined at the top.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm no it's a bit unhappy if I remove it. The types in the shared ATIPageTrackingProps are different to what ResonanceBeaconConfig is expecting.

E.g. Reverb permits contentId to be string | null | undefined.
But Resonance only permits contentId to be string | undefined.

That as ResonanceBeaconConfig is needed to surpress a lot of TS issues. We could get rid of the output type without any issues. But I don't see the harm in it.

The solution would be typing it ourselves, rather than using the imported types (which I did put a question mark on)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're renaming things eventually to be more generic, it does seem a little strange that this was always called atiUrl when it doesn't return a URL or even a string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I. Totally. Agree. Have added to https://bbc.atlassian.net/browse/WS-3129

expect(mockSendBeacon).toHaveBeenCalledWith(mockReverbParams, undefined);
});

it('should call sendBeacon with null resonanceParams when provided as null', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to check this is the desired behaviour. Would it make more sense for this and the case above to always return either null or undefined?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree this looks a bit strange.

My logic was that when the page first loads Resonance (and Reverb) is undefined. But once the params are set, they are an object. The different with Resonance is that they can be null if it's not an applicable service or platform.

We don't currently test for when ReverbParams are 'undefined'. But they definitely can be. If I had to bin one of the tests, it would be this one.

(Note this is typed here as undefined because this is also shared by the AmpATIAnalytics component.

I've made a small tweak of the test to improve readability.

Comment on lines 99 to 109
if (onClient()) {
try {
const { eventDetails } = reverbBeaconConfig;

if (resonanceBeaconConfig) {
callResonance(resonanceBeaconConfig);
}

await callReverb(eventDetails);
} catch (error) {
logger.error(ATI_LOGGING_ERROR, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If callResonance() errors, I think callReverb() won't be reached. It might be worth splitting sendBeacon into one for resonance, one for reverb.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I have wrapped it in a try catch and added a unit test. Seems simpler.

Comment on lines +1 to +4
export const ResonanceMode = {
LIVE: 'live',
TEST: 'test',
} as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these add much value if the value is the same as the key just lowercase?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a copilot suggestion - I think the issue is if the tests want to find ResonanceMode but I don't want it to go to the import

Or in copilot speak: Yes, they add value here. This file is a mock of an external package (@bbc/resonance), so the string values are a contract — they must match what the real SDK exports and what gets sent to the analytics endpoint. The lowercase values aren't arbitrary choices; they replicate the actual API contract of @bbc/resonance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants