This document lists every event the NRC Games Hub emits to the parent page. Use it as the source of truth for hooking up Google Analytics (or any other analytics provider).

Scope: this reference applies to the NRC Games Hub only. Other Games Hub variants (e.g. Mediahuis) emit a different set of events and are documented separately.


How to listen

On web, the Games Hub fires every event — analytics and otherwise — as a CustomEvent on window named gamesHubEvent. On non-web platforms (iOS/Android wrapper webviews) most events are dispatched to the native wrapper via window.parent.postMessage(JSON.stringify(detail), '*') instead; the analytics tracking* events are the exception and always use the web CustomEvent transport regardless of platform — see the two notes below. Analytics events use a dedicated tracking* prefix on the top-level type (e.g. trackingModal, trackingGameshub) so consumers filter by type alone:

window.addEventListener('gamesHubEvent', (event) => {
  if (!event.detail.type.startsWith('tracking')) return;
  const { type, timestamp, data, metadata, timer } = event.detail;
  // forward to GA, e.g.:
  window.dataLayer.push({
    event: `gameshub_${type}`,
    type,
    timestamp,
    data,
    metadata,
    timer,
  });
});

Analytics events always use dispatchEvent, even inside the iOS/Android webview. They are emitted as CustomEvents on window for the host page to consume, never via postMessage to the native wrapper. (Other events still postMessage to the wrapper on non-web — that contract is unchanged.)

Lifecycle events are not tracking*-prefixed. boot, ready, and initialized fire under their original type. If your analytics integration needs lifecycle timing — or needs metadata.userId from the initialized event — listen for those types directly.

The boot event is dispatched directly on window from the entry script before the SDK has loaded. On non-web platforms it is also sent via window.parent.postMessage(...), so consumers listening outside the browser can receive it through the same bridge as other events.

Two parallel event sets, distinguished by type

Every event uses the same payload shape ({ type, timestamp, data, metadata, timer? }); the type value tells consumers which set an event belongs to:

  • tracking* types (trackingModal, trackingDialog, trackingAction, trackingGameshub) — analytics-only events. Carry gameTitle / variantTitle in their metadata. Always dispatched on window as a CustomEvent('gamesHubEvent'), including on iOS/Android — never via postMessage to the native wrapper. Future changes requested by analytics engineers land on these types only.
  • All other types — legacy/functional events. Every event the Games Hub has historically emitted still fires under its original name, so functional consumers — the mobile-app wrapper, custom integrations that act on showFullScreen / visitUrl, etc. — keep working unchanged. Their transport follows the platform default: CustomEvent on web, postMessage to the native wrapper on iOS/Android.

If you are wiring up analytics, filter on type.startsWith('tracking') — plus separately handle any lifecycle types you also need (see the allowlist). If you are an existing integration that consumes events for functional purposes, ignore any event whose type starts with tracking — that’s the contract that will keep evolving.

Common envelope

Every event — analytics and otherwise — uses the same shape. Analytics events differ only by their type value (tracking* prefix) and by carrying gameTitle / variantTitle in metadata:

{
  type: string;              // event name (see tables below); `tracking*` for analytics
  timestamp: number;         // Date.now() at emission
  data?: object;             // event-specific payload (see each event)
  metadata?: {               // see "Game identification" below
    userId?: string;         // populated from the `initialized` event onward
    gameTitle?: string;      // analytics events only; added while a puzzle is open, plus on tracking modal events (see "Modal attribution")
    variantTitle?: string;   // analytics events only; added while a puzzle is open, plus on tracking modal events (see "Modal attribution")
    puzzleId?: string;       // present on events bubbled from OnePlayer
    gameName?: string;       // present on events bubbled from OnePlayer
    gameType?: string;       // present on events bubbled from OnePlayer
    gameId?: string;         // present on events bubbled from OnePlayer
  };
  timer?: { ms: number };    // present on game-lifecycle events
}

See Game identification for what each metadata field means, and in particular how gameTitle differs from gameName.


Game identification

Every event emitted while a puzzle is open carries the active game’s title in its metadata, so you can attribute any event — started, completed, share, actionClicked, userCheated, etc. — to a specific game variant without joining against a separate gameData event.

In addition, the modalOpened / modalClosed events carry attribution even when emitted outside an active game: see Modal attribution for the per-modal rules (e.g. startScreen emitted from the overview page carries only gameTitle, and the archive modal updates its attribution when the user switches variant tab).

Field Added by Source
gameTitle Games Hub Hub config GameConfig.title (brand-configured, from the config API)
variantTitle Games Hub Hub config VariantConfig.title — only set when the game has more than one variant (otherwise omitted to avoid duplicating gameTitle)
gameType OnePlayer The puzzle renderer (e.g. wordle, sudoku)
gameName OnePlayer OnePlayer i18n t('<renderer>.name') — generic, variant-agnostic
puzzleId OnePlayer Unique id of the specific puzzle being played
gameId OnePlayer The OnePlayer playerId

gameTitle / variantTitle are injected centrally for every tracking* analytics event by AnalyticsService.withGameContext (apps/games-hub-nrc/src/lib/services/analyticsService.ts). The active title is kept current by the player (apps/games-hub-nrc/src/lib/webcomponents/Player.svelte) on ExternalEventEmitter.gameContext — which AnalyticsService reads — and cleared when no puzzle is open, so events fired before a puzzle opens (boot, ready, initialized, and overview-level navigation) do not carry them. They are added regardless of the externalGameTitle feature flag (that flag only gates the separate gameData event). Non-tracking* events do not carry these fields — listen for the tracking* variant if you need analytics attribution.

gameName vs gameTitle

These are not the same value and frequently differ. For analytics that should match what the user actually sees on the page, use gameTitle (+ variantTitle).

  gameName gameTitle
Set by OnePlayer Games Hub config
Source t('<renderer>.name') in OnePlayer’s i18n bundle GameConfig.title from the hub config API (editable in Studio)
Variant-aware? No — every variant of a renderer shares one name Paired with variantTitle, which distinguishes variants
Brand-specific? No — OnePlayer’s canonical name Yes — whatever the brand calls the game

They line up for some games (e.g. both report "Sudoku") but diverge whenever the brand titles a game differently, or for hub-specific variants. Two concrete cases:

  • Hub-specific variant: a 5×5 Sudoku is gameName: "Sudoku" (the renderer is sudoku), but the hub classifies it as gameType: "cinco" with its own configured gameTitle (e.g. "Cinco").
  • Same renderer, different variants: a 5-letter and a 6-letter Wordle both report gameName: "Het Woord"; only variantTitle tells them apart.

Event allowlist

Every dispatched event’s type must be in OutgoingMessage (apps/games-hub-nrc/src/lib/types/bridge.ts). Anything else is dropped before reaching the parent.

The one exception is boot, which is a special-case loader event dispatched directly by the entry script (apps/games-hub-nrc/src/lib/games-hub-entry.ts:18) before the SDK has loaded. It bypasses the bridge emitter and is therefore not part of the enum.

This is the complete list of events and the tracking* variant (if any) that ships the analytics version:

Legacy type Origin Analytics type + data.type (if umbrella) Section
boot Games Hub Lifecycle
ready Games Hub Lifecycle
initialized Games Hub Lifecycle
startScreenOpened Games Hub — (tracked via trackingModal with name: 'startScreen') Navigation & UI
modalOpened Games Hub trackingModal + data.type: 'modalOpened' — or trackingDialog + data.type: 'dialogOpened' for dialogs Navigation & UI
modalClosed Games Hub trackingModal + data.type: 'modalClosed' — or trackingDialog + data.type: 'dialogClosed' for dialogs Navigation & UI
showFullScreen Games Hub Navigation & UI
hideFullScreen Games Hub Navigation & UI
gameData Games Hub Game metadata
started OnePlayer (bubbled) trackingGameshub + data.type: 'started' Gameplay
paused OnePlayer (bubbled) trackingGameshub + data.type: 'paused' Gameplay
completed OnePlayer (bubbled) trackingGameshub + data.type: 'completed' Gameplay
share OnePlayer (bubbled) / Games Hub trackingAction + data.type: 'share' User actions
visitUrl OnePlayer (bubbled) / Games Hub User actions
actionClicked OnePlayer (bubbled) trackingAction + data.type: 'actionClicked' User actions
userCheated Games Hub trackingAction + data.type: 'userCheated' Solution reveal

Four umbrella analytics events. Every analytics event lands under one of four outer types, with data.type discriminating the specific event inside:

  • trackingModal — modal/overlay/menu lifecycle: 'modalOpened', 'modalClosed' (names: startScreen, endScreen, archive, streakInfo, freezeInfo, gameOnboarding, menu). The startScreen modal-open already covers the legacy startScreenOpened use case, so there is no separate analytics event for it.
  • trackingDialog — dialog lifecycle: 'dialogOpened', 'dialogClosed' (names: copyConfirmation, feedback, onePlayerPrompt).
  • trackingAction — user-initiated actions: 'actionClicked', 'share', 'userCheated'.
  • trackingGameshub — bubbled gameplay events from OnePlayer: 'started', 'paused', 'completed'. The “gameshub” namespace lives on the outer type name, so the inner data.type is the bare OnePlayer event name.

OnePlayer events that are NOT bubbled are listed at the bottom under Filtered OnePlayer events. If you need any of those tracked, the Games Hub code needs to be changed first. Listening on window will not see them.


Lifecycle

boot

Fired by the entry script the moment the Games Hub loader runs, before the main bundle is imported. Useful as a “page contains a Games Hub” signal.

  • Source: apps/games-hub-nrc/src/lib/games-hub-entry.ts:18
  • Payload:
    { type: 'boot', timestamp: number, metadata: {} }
    

ready

Fired right after the Games Hub bundle finishes constructing the bridge. The hub is now ready to receive initialize from the host. Sent on all platforms (it is the only event guaranteed to fire before the host has identified itself).

  • Source: apps/games-hub-nrc/src/lib/services/externalEventEmitter.ts:68
  • Payload:
    { type: 'ready', timestamp: number, metadata: {} }
    

initialized

Fired after the host has sent the initialize message and the hub has loaded its config from the API. From this point on, metadata.userId is populated on subsequent events.

  • Source: apps/games-hub-nrc/src/lib/services/bridge.ts:525
  • Payload:
    {
      type: 'initialized',
      timestamp: number,
      metadata: { userId: string }
    }
    

Gameplay

These three are bubbled from OnePlayer. They carry a timer.ms reflecting elapsed game time at the moment of emission.

started

Fires when the player makes their first move in a puzzle (transition from Ready/Paused -> Playing).

  • OnePlayer source: apps/oneplayer/src/games/event-center.ts:58
  • Payload (legacy gamesHubEvent):
    {
      type: 'started',
      timestamp: number,
      data: { ms: number },
      metadata: { userId, puzzleId, gameName, gameType, gameId },
      timer: { ms: number }
    }
    
  • Analytics variant: also fires as type: 'trackingGameshub' with data: { type: 'started', ms } and gameTitle / variantTitle added to metadata.

paused

Fires when the player pauses (manually or because an overlay opened).

  • OnePlayer source: apps/oneplayer/src/games/event-center.ts:71
  • Payload: same shape as started, with type: 'paused'. Analytics variant: trackingGameshub with data.type: 'paused'.

completed

Fires when the player solves the puzzle. Games Hub augments the payload to always include a milliseconds entry in data, even if OnePlayer didn’t add one.

  • OnePlayer source: apps/oneplayer/src/games/event-center.ts:84
  • Augmentation: apps/games-hub-nrc/src/lib/components/OnePlayer.svelte:218
  • Payload (legacy gamesHubEvent):
    {
      type: 'completed',
      timestamp: number,
      data: GameResultData[],   // always contains { unit: 'milliseconds', value: <ms> }
      metadata: { userId, puzzleId, gameName, gameType, gameId },
      timer: { ms: number }
    }
    
  • Analytics variant: also fires as type: 'trackingGameshub' with data: { type: 'completed', results: GameResultData[] }-style payload and gameTitle / variantTitle added to metadata. Where GameResultData is one of:
    { unit: 'solution'    | 'rank' | 'points' | 'attempts' | 'milliseconds',
      value: string | number }
    

startScreenOpened

Fires when the start-screen modal has finished loading and is visible.

  • Source: apps/games-hub-nrc/src/lib/components/modals/StartScreenModal/StartScreenModal.svelte:124
  • Payload:
    {
      type: 'startScreenOpened',
      timestamp: number,
      data: { variant: 'singleGame' | 'multiGame' },
      metadata: {}
    }
    

modalOpened / modalClosed

Fires when any overlay opens or closes inside the Games Hub: fullscreen modals, smaller centered modals, dialogs, or the player menu. The data.name field identifies which overlay, so they can be converted into virtual page views in analytics.

  • Sources:
    • Fullscreen modals: apps/games-hub-nrc/src/lib/components/modals/Fullscreen.svelte:16,22
    • Modals: apps/games-hub-nrc/src/lib/components/modals/Modal.svelte:14
    • Dialogs: apps/games-hub-nrc/src/lib/components/modals/Dialog.svelte:21
    • Player menu: apps/games-hub-nrc/src/lib/components/player/Menu.svelte:21
  • Payload (legacy gamesHubEvent):
    {
      type: 'modalOpened' | 'modalClosed',
      timestamp: number,
      data: { name: ModalName },
      metadata: {}
    }
    
  • Analytics variants: dialogs fire as trackingDialog + data.type: 'dialogOpened' / 'dialogClosed'; everything else fires as trackingModal + data.type: 'modalOpened' / 'modalClosed'. Both add gameTitle / variantTitle to metadata per the rules in “Modal attribution” below.

The trackingModal and trackingDialog analytics events for game-related overlays carry gameTitle (and, where it makes sense, variantTitle) in their metadata, so each open/close can be attributed to a specific game variant without joining against a separate event. The legacy modalOpened / modalClosed events do not carry these fields — listen on the tracking* variants if you need analytics attribution. Per-modal rules:

data.name gameTitle variantTitle Notes
startScreen Set from the hub config by the opener (game tile / showStartScreen host message). No variant — the screen presents multiple variants at once.
endScreen Inherited from the active player.
archive Reflects the currently selected variant tab. See “Variant switching” below.
gameOnboarding Inherited from the active player.
feedback Opened from the end screen; inherited from the active player.
streakInfo ✓ / — ✓ / — Inherits the active player’s attribution. Carries both when opened from the end screen; carries nothing when opened from the overview-page streak indicator (no active game).
freezeInfo ✓ / — ✓ / — Same as streakInfo — present when an active game’s context is set, absent otherwise.
menu Inherited from the active player.
copyConfirmation, onePlayerPrompt ✓ / — ✓ / — Inherits the active player’s attribution.

Internally this is implemented two ways and they don’t differ from the outside:

  • Player-driven attribution (most modals above): the active player keeps the ExternalEventEmitter’s gameContext in sync (apps/games-hub-nrc/src/lib/webcomponents/Player.svelte), which AnalyticsService reads when enriching analytics events. Anything that opens while a game is active picks up gameTitle / variantTitle automatically.
  • Modal-driven attribution (startScreen, archive): the opener passes an explicit ModalMeta to the modal service; AnalyticsService.emitModalOpened / emitDialogOpened uses that meta and ignores the player’s gameContext for that single event. Used for modals that can be opened with no active player (start screen) or that own their own variant selection (archive).
Variant switching (archive)

The archive modal lets the user switch between variant tabs (e.g. 5-letter vs 6-letter Wordle). Each switch emits a modalClosed for the previously selected variant followed by a modalOpened for the newly selected one (and the matching trackingModal analytics pair) — so the time spent on each variant remains analytically distinct, even though the modal stays mounted. The archive also emits the same corrective pair the first time its own data arrives, in the case where the initial modalOpened was emitted before the variant was known (e.g. the archive was opened from the start screen, which doesn’t yet know which variant the user will look at).

  • Source: apps/games-hub-nrc/src/lib/components/modals/ArchiveModal/ArchiveModal.svelte (syncMetaFromResponse)
name Component Triggered by
startScreen StartScreenModal Game tile clicked, or showStartScreen received from host
endScreen EndScreenModal Puzzle completed
archive ArchiveModal User opens the puzzle archive (from start screen, end screen or player menu)
streakInfo StreakInfoModal User opens the streak explanation popup
freezeInfo FreezeInfoModal User opens the streak-freeze explanation popup, or the freeze-onboarding notification
gameOnboarding PagedModal First-time per-game onboarding modal shown when the player loads a new game
copyConfirmation CopiedDialog “Copied to clipboard” confirmation after a share
feedback FeedbackDialog User opens the feedback form from the end screen
onePlayerPrompt OnePlayerDialog OnePlayer requests a prompt (e.g. paused-timer warning, confirm-restart, etc.)
menu Player menu drawer User taps the menu icon in the player

Coverage: gameOnboarding, archive, streakInfo, freezeInfo (all opened via modalService), copyConfirmation, feedback, onePlayerPrompt (opened via dialogService), and menu (opened via menuService) all emit the same modalOpened / modalClosed events as the fullscreen modals on the legacy channel. Filter on data.name in your analytics provider if you need to distinguish “page-like” overlays from transient ones (e.g. copyConfirmation is a short toast-style confirmation).

Analytics split: on the tracking* side, the dialogService dialogs fire under trackingDialog (data.type: 'dialogOpened' / 'dialogClosed') instead of trackingModal. The other names in this table fire under trackingModal.

Pairing with showFullScreen / hideFullScreen: Only the fullscreen variants (startScreen, endScreen) are paired with showFullScreen / hideFullScreen — the other overlays do not change the fullscreen state.

The full union of modal names is the ModalName type in apps/games-hub-nrc/src/lib/types/bridge.ts. If new overlays are added to the Games Hub later, they’ll appear here as additional name values — the Games Hub team keeps the list in sync via the type system, so any new modal will surface in your analytics automatically with a stable identifier.

showFullScreen / hideFullScreen

Companion events to modalOpened / modalClosed, intended for hosts that need to react to full-screen mode (e.g. native wrappers hiding chrome). Fires from the same Fullscreen modal lifecycle.

  • Source: apps/games-hub-nrc/src/lib/components/modals/Fullscreen.svelte:16,21
  • Payload:
    { type: 'showFullScreen' | 'hideFullScreen', timestamp: number, metadata: {} }
    

Game metadata

gameData

Fires when the active game/variant/date changes, only if the host has opted into the externalGameTitle feature flag. Used by hosts that want to render the title outside the hub.

If you only need the game/variant title for analytics, prefer the gameTitle / variantTitle fields now present on every in-game event (see Game identification) — they require no feature flag. gameData remains the dedicated event for hosts that render the title in their own chrome.

  • Source: apps/games-hub-nrc/src/lib/webcomponents/Player.svelte:358
  • Feature flag: config.features.externalGameTitle.enabled
  • Payload:
    {
      type: 'gameData',
      timestamp: number,
      data: {
        title: string;
        variant?: string;
        date: string;          // yyyy-MM-dd
        relativeDate: string;  // e.g. 'today', 'yesterday'
      },
      metadata: {}
    }
    

User actions

share

Fires when the user taps the share button. Bubbled from OnePlayer and also emitted directly by the Games Hub when the share happens at the hub level.

  • OnePlayer source: apps/oneplayer/src/games/event-center.ts:186
  • Games Hub source: apps/games-hub-nrc/src/lib/services/bridge.ts:636
  • Payload (legacy gamesHubEvent):
    {
      type: 'share',
      timestamp: number,
      data: { text: string, url?: string, title?: string },
      // bubbled-from-OnePlayer variants also include puzzleId, gameName, gameType
      metadata: { userId }
    }
    
  • Analytics variant: also fires as type: 'trackingAction' with data: { type: 'share', text, url?, title? } and gameTitle / variantTitle added to metadata.

visitUrl

Fires when an in-game link should be opened (e.g. an article link in an RSS-driven puzzle, or any external link in the hub).

On the web platform the hub already opens the URL itself; it still emits the event so analytics can record it. On native platforms the host is expected to handle navigation.

  • OnePlayer source: apps/oneplayer/src/games/event-center.ts:208
  • Games Hub source: apps/games-hub-nrc/src/lib/services/bridge.ts:610
  • Payload (legacy gamesHubEvent):
    {
      type: 'visitUrl',
      timestamp: number,
      data: { url: string, target?: '_self' | '_blank' },
      // bubbled-from-OnePlayer variants also include puzzleId, gameName, gameType
      metadata: { userId }
    }
    
  • No analytics variant. visitUrl is purely functional (hosts navigate on it); it has no tracking* counterpart.

actionClicked

Fires when the user clicks one of the tracked action buttons inside the player (the player menu and game-specific toolbars). Use this to build engagement funnels for individual in-game interactions — distinct from the lifecycle paused event, which fires for any pause (including ones induced by overlays).

The data.name field identifies which action was clicked, so they can be filtered or grouped in analytics. Debug-only actions (reload, clean) and any other unknown action IDs are deliberately not emitted.

  • OnePlayer source: apps/oneplayer/src/components/ActionButton.svelte (single click chokepoint) → apps/oneplayer/src/games/event-center.ts (emitActionClicked)
  • Allowlist type: ActionName in apps/games-hub-nrc/src/lib/types/bridge.ts
  • Payload (legacy gamesHubEvent):
    {
      type: 'actionClicked',
      timestamp: number,
      data: { name: ActionName },
      metadata: { userId, puzzleId, gameName, gameType, gameId },
      timer: { ms: number }
    }
    
  • Analytics variant: also fires as type: 'trackingAction' with data: { type: 'actionClicked', name: ActionName } and gameTitle / variantTitle added to metadata.

Action names

name Origin
restart Player menu — restart the current puzzle
quit Player menu — quit the current puzzle
back Player menu — back to the previous screen
help Player menu — open the help/onboarding screen
feedback Player menu — open the feedback dialog
fullscreen Player menu — toggle fullscreen mode
hint Game-specific (crossword, sudoku, chess, ohh1, 0hn0, tentstrees, cijferblok, …)
undo Game-specific (crossword, sudoku, chess, …)
check Crossword — check the current solution
focus Crossword — toggle focus mode (portrait only)
print Crossword, cijferblok, tentstrees, knightmove — print the puzzle
shuffle Connections, spellwise — shuffle the tiles/letters
showWords Wordfind — toggle the word list (the “Open” button)

The full union of action names is the ActionName type in apps/games-hub-nrc/src/lib/types/bridge.ts, kept in sync with OnePlayerActionName in apps/oneplayer/src/player/external-events.ts. If new tracked actions are added later, they’ll appear here as additional name values — the Games Hub team keeps the list in sync via the type system, so any new tracked action will surface in your analytics automatically with a stable identifier.


Solution reveal

userCheated

Fires when the user chooses to reveal the puzzle’s solution from the menu. The Games Hub also uses this event internally to switch to a different end screen reflecting that the puzzle was revealed rather than solved. The timer.ms reflects elapsed play time at reveal.

  • Source: apps/games-hub-nrc/src/lib/webcomponents/Player.svelte:495
  • Payload (legacy gamesHubEvent):
    {
      type: 'userCheated',
      timestamp: number,
      metadata: { userId },
      timer: { ms: number }
    }
    
  • Analytics variant: also fires as type: 'trackingAction' with data: { type: 'userCheated' } and gameTitle / variantTitle added to metadata.

Filtered OnePlayer events

The following events are emitted by OnePlayer but never reach the page because they are not in the OutgoingMessage allowlist (apps/games-hub-nrc/src/lib/types/bridge.ts:86) and/or are explicitly intercepted in OnePlayer.svelte. They are listed here so you know what data is theoretically available but would require a code change in the hub before it can be tracked.

OnePlayer event Why it’s not visible OnePlayer source (event-center.ts unless noted)
move Not in OutgoingMessage allowlist :280
progress Not in OutgoingMessage allowlist :269
closed Not in OutgoingMessage allowlist :121
aborted Not in OutgoingMessage allowlist :134
requireLogin Not in OutgoingMessage allowlist :160
requireSubscription Not in OutgoingMessage allowlist :173
adBreak Not in OutgoingMessage allowlist :147
track Not in OutgoingMessage allowlist :197
endScreenData Consumed internally by Games Hub (drives the end-screen modal) :109
timerUpdate Explicit early return in OnePlayer.svelte:299 :340
promptOpened Consumed internally (opens a hub dialog) :297
promptClosed Consumed internally (closes hub dialog) :312
hint / hintCleared Consumed internally (rendered as toast or banner) :219, :230
gameWarning / gameWarningCleared Consumed internally (rendered as toast) :241, :252
userThemePreference Not in OutgoingMessage allowlist :326
onStateChange Not in OutgoingMessage allowlist (one-player-element.ts:433)

Quick reference: event flow

┌──────────────┐        ┌────────────┐        ┌──────────────────────────┐
│  OnePlayer   │ events │ Games Hub  │ filter │  window dispatch /       │
│  (puzzle UI) │ ─────▶ │  Bridge    │ ─────▶ │  postMessage to host     │
└──────────────┘        └────────────┘        └──────────────────────────┘
                            │                              │
                            │ also emits its own           │
                            │ lifecycle / UI events        │
                            └──────────────────────────────┘

The OnePlayer.svelte listener in the hub processes every OnePlayer event, then forwards it to both bridge.externalEventEmitter.sendOnePlayerEvent(event) (legacy path — checks the OutgoingMessage allowlist and ships under the original event name) and bridge.analyticsService.sendOnePlayerEvent(event) (analytics path — checks the TRACKED_PLAYER_EVENTS mapping in AnalyticsService and ships under the mapped tracking* umbrella with the original name in data.type). Just before dispatching, AnalyticsService enriches the metadata with the active gameTitle / variantTitle via withGameContext (see Game identification).

  • Inbound listener: apps/games-hub-nrc/src/lib/components/OnePlayer.svelte
  • Outbound filters: apps/games-hub-nrc/src/lib/services/externalEventEmitter.ts (legacy), apps/games-hub-nrc/src/lib/services/analyticsService.ts (analytics)
  • Game-title injection: apps/games-hub-nrc/src/lib/services/analyticsService.ts