Skip to content

[WS-3079]: Send a Piano activation event when a user is activated into an Optimizely experiment - #14293

Merged
elvinasv merged 22 commits into
latestfrom
WS-3079-send-a-piano-activation-event-when-a-user-is-activated-into-an-optimizely-experiment
Aug 28, 2026
Merged

[WS-3079]: Send a Piano activation event when a user is activated into an Optimizely experiment#14293
elvinasv merged 22 commits into
latestfrom
WS-3079-send-a-piano-activation-event-when-a-user-is-activated-into-an-optimizely-experiment

Conversation

@LukasFrm

@LukasFrm LukasFrm commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Resolves JIRA: https://bbc.atlassian.net/browse/WS-3079

Summary

Adds a standalone Piano/Reverb "activation" beacon that fires the moment a user is bucketed into an Optimizely experiment (client- or server-side), decoupled from any page-view/click event, following the official "Activation (v1.0.1) on Web" viewability-model event spec.

Code changes

  • Added buildActivationEventModel in atiUrl to build a spec-compliant viewability beacon (event.action: 'serve', interaction_type: 'optimizely_activation', spec_id/spec_version, group/experience fields)
  • Added sendOptimizelyActivationEvent and a module-level activationContext bridge that carries the current page's ATI tracking context into the non-React Optimizely DECISION notification listener
  • withOptimizelyProvider's DECISION listener now dedupes per experiment via notifyDecision (now returns whether the decision was newly recorded) and fires the activation beacon once per unique decision
  • activateExperiment now guards against re-activating or concurrently double-activating the same experiment via a module-level Set
  • useServerSide now derives a single activeVariation value and always activates via useEffect (previously activated on some paths outside the effect)
  • Threaded appName/platform through EventTrackingContextactivationContext → the beacon builder so app_name/app_type reflect each service's real ATI config instead of being hardcoded

Testing

  1. Run yarn dev and open a page with an active Optimizely experiment
  2. Confirm the experiment only activates once, even across rerenders/navigations within the same session
  3. In DevTools Network tab, confirm a Reverb beacon fires with eventPublisher: 'viewability' and event.interaction_type: 'optimizely_activation'
  4. Run yarn jest src/app/hooks/useOptimizelyVariation src/app/contexts/EventTrackingContext src/app/legacy/containers/PageHandlers/withOptimizelyProvider src/app/lib/analyticsUtils src/app/lib/optimizelyDecisionStore.test.ts

Flow Diagram

sequenceDiagram
    participant ETC as EventTrackingContextProvider
    participant AC as activationContext (module)
    participant Hook as useServerSide / useClientSide
    participant SDK as Optimizely SDK
    participant Listener as DECISION listener (withOptimizelyProvider)
    participant Store as optimizelyDecisionStore
    participant Send as sendOptimizelyActivationEvent
    participant Beacon as sendBeacon (Piano/Reverb)

    Note over ETC: 1. Render time (synchronous, not an effect)
    ETC->>AC: setActivationContext({ trackingIsEnabled, pageIdentifier, platform, appName, producerName, statsDestination, isSignedIn, hashedId })

    Note over Hook: 2. A descendant resolves an experiment variation
    Hook->>SDK: activateExperiment() → optimizely.activate() (server-side)<br/>or useDecision() (client-side)

    SDK-->>Listener: 3. fires DECISION notification (decisionInfo)
    Listener->>Listener: resolveDecision() → { decisionKey, impressionDispatched }

    alt decisionKey && variationKey && variationKey !== 'off'
        Listener->>Store: 4. notifyDecision(decisionKey)
        Store-->>Listener: isNewDecision (true = first time this session)

        alt impressionDispatched
            alt isNewDecision
                Listener->>AC: 5. getActivationContext()
                AC-->>Listener: bridged ATI context
                Listener->>Send: sendOptimizelyActivationEvent({ experimentName, experimentVariant, ...context })
                Send->>Send: 6. guard: trackingIsEnabled, variant valid, required ATI fields present
                Send->>Beacon: 7. sendBeacon(reverbParams)
            end
            Listener->>SDK: optimizely.track('visit' / 'page-views' / 'signed-in-page-views')
        end
    end
Loading

Useful Links

@LukasFrm LukasFrm changed the title feat: Piano & Optimizely activation event sync; tests [WS-3079]: Send a Piano activation event when a user is activated into an Optimizely experiment Aug 5, 2026

@HarveyPeachey HarveyPeachey 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.

Wouldn't it be easier/better to do this in the decision event listener, which already tells us when an activation/impression occurs?

@LukasFrm
LukasFrm marked this pull request as ready for review August 18, 2026 07:08
…-a-user-is-activated-into-an-optimizely-experiment
Copilot AI lite review requested due to automatic review settings August 18, 2026 07:09

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 standalone Piano/Reverb “activation” analytics to capture when a user is activated into an Optimizely experiment (server-side and client-side), decoupled from view/click events, with dedupe to prevent duplicate firing.

Changes:

  • Introduces an activation event model (buildActivationEventModel) and a new sendOptimizelyActivationEvent utility to send the beacon.
  • Adds useOptimizelyActivationEvent hook to collect ATI context + expose a stable activation-sender callback.
  • Adds once-per-experiment activation dedupe and activation-event triggering for both useServerSide and useClientSide Optimizely flows, plus expanded test coverage.

Reviewed changes

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

Show a summary per file
File Description
src/app/lib/analyticsUtils/sendOptimizelyActivationEvent/index.ts Adds guarded sender for Optimizely activation beacons.
src/app/lib/analyticsUtils/sendOptimizelyActivationEvent/index.test.ts Unit tests for activation-beacon guard/send behavior.
src/app/lib/analyticsUtils/sendBeacon/index.ts Extends Reverb sending to support activation and additional labels.
src/app/lib/analyticsUtils/sendBeacon/index.test.ts Adds coverage for activation event wiring to userActionEvent.
src/app/lib/analyticsUtils/analytics.const.ts Adds ACTIVATION_EVENT constant.
src/app/hooks/usePWAInstallTracker/index.test.tsx Updates hook tests to include ToggleContext provider wrapper.
src/app/hooks/useOptimizelyVariation/useServerSide/index.ts Adds activation-event firing + ref-based dedupe to server-side hook.
src/app/hooks/useOptimizelyVariation/useServerSide/index.test.tsx Updates tests for new provider requirements + activation side-effects.
src/app/hooks/useOptimizelyVariation/useClientSide/index.ts Adds activation-event firing + ref-based dedupe to client-side hook.
src/app/hooks/useOptimizelyVariation/useClientSide/index.test.tsx New tests covering activation event sending + dedupe.
src/app/hooks/useOptimizelyVariation/useClientSide/index.test.ts Removes old client-side hook tests (replaced by .test.tsx).
src/app/hooks/useOptimizelyVariation/activateExperiment/index.ts Adds dedupe and activation callback hook point.
src/app/hooks/useOptimizelyVariation/activateExperiment/index.test.ts Adds tests for dedupe and activation callback invocation.
src/app/hooks/useOptimizelyActivationEvent/index.ts New hook to gather ATI context and send activation beacons.
src/app/hooks/useOptimizelyActivationEvent/index.test.tsx Tests for activation hook behavior and tracking-toggle handling.
src/app/components/ATIAnalytics/types.ts Extends Reverb event typing to support activation + extra labels.
src/app/components/ATIAnalytics/atiUrl/index.ts Adds buildActivationEventModel for Reverb activation events.

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

Comment thread src/app/lib/analyticsUtils/sendOptimizelyActivationEvent/index.ts
Comment thread src/app/hooks/useOptimizelyVariation/activateExperiment/index.ts Outdated
…tivated-into-an-optimizely-experiment' of https://github.com/bbc/simorgh into WS-3079-send-a-piano-activation-event-when-a-user-is-activated-into-an-optimizely-experiment

@elvinasv elvinasv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like we're really close! Great flow diagram 👍

Comment thread src/app/hooks/useOptimizelyVariation/useServerSide/index.ts Outdated
Comment thread src/app/lib/analyticsUtils/sendOptimizelyActivationEvent/index.ts
Comment thread src/app/components/ATIAnalytics/atiUrl/index.ts
@alex-magana

alex-magana commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Hello.
A polite heads-up that we're triaging a somewhat similar solution
in Discovery, to address an issue we've faced with the search referrer
experiment.
The main differences in the approaches, is the schema we're passing to
Reverb's userActionEvent.
Our aim is to use mimic the useCustomEventTracker hook while removing
dependencies on hooks to address React violations associated with calling
hooks from outsides of hooks & React components.
Thanks.

@LukasFrm

LukasFrm commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Hello. A polite heads-up that we're triaging a somewhat similar solution in Discovery, to address an issue we've faced with the search referrer experiment. The main differences in the approaches, is the schema we're passing to Reverb's userActionEvent. Our aim is to use mimic the useCustomEventTracker hook while removing dependencies on hooks to address React violations associated with calling hooks from outsides of hooks & React components. Thanks.

Hello,
Thank you for the heads up, let's talk offline.

@alex-magana

alex-magana commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

I've cross-referenced the schema in buildReverbEventModel, the function we use via the
useCustomEventTracker hook, against buildActivationEventModel.

The main differences are the missing attributes are grouping: 'optimizely' in the
event object and engine_type: ['experimentation'], in the experience object.

Comparing the spec v2.0.0 to v1.0.1 , grouping and engine_type are properties in v2.0.0.
Given this implementation is based on v1.0.1, we are good to proceed with the schema in
buildActivationEventModel as is.

Comment thread src/app/hooks/useOptimizelyVariation/activateExperiment/index.ts
Comment thread src/app/components/ATIAnalytics/atiUrl/index.ts Outdated
Comment thread src/app/contexts/EventTrackingContext/index.tsx Outdated
alex-magana and others added 4 commits August 25, 2026 17:34
…-a-user-is-activated-into-an-optimizely-experiment
…tivated-into-an-optimizely-experiment' of github.com:bbc/simorgh into WS-3079-send-a-piano-activation-event-when-a-user-is-activated-into-an-optimizely-experiment
…-a-user-is-activated-into-an-optimizely-experiment
Comment thread src/app/hooks/useOptimizelyVariation/useClientSide/index.test.ts
Comment thread src/app/hooks/useOptimizelyVariation/activateExperiment/index.ts
Comment thread src/app/hooks/useOptimizelyVariation/useClientSide/index.ts Outdated
Comment thread src/app/hooks/useOptimizelyVariation/useServerSide/index.test.tsx
…-a-user-is-activated-into-an-optimizely-experiment
@elvinasv
elvinasv merged commit d74b78a into latest Aug 28, 2026
18 checks passed
@elvinasv
elvinasv deleted the WS-3079-send-a-piano-activation-event-when-a-user-is-activated-into-an-optimizely-experiment branch August 28, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants