Analytics events for the NRC Games Hub
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
The Games Hub fires every public event as a CustomEvent on window named gamesHubEvent. The actual payload sits on event.detail.
window.addEventListener('gamesHubEvent', (event) => {
const { type, timestamp, data, metadata, timer } = event.detail;
// forward to GA, e.g.:
window.dataLayer.push({ event: `gameshub_${type}`, ...event.detail });
});
On non-web platforms (iOS/Android wrapper apps, where
config.platform !== 'web') events are also sent viawindow.parent.postMessage(JSON.stringify(detail), '*'). For browser-based analytics you only need thegamesHubEventlistener.
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.
Common envelope
Every event has the shape:
{
type: string; // event name (see tables below)
timestamp: number; // Date.now() at emission
data?: object; // event-specific payload (see each event)
metadata?: { // varies by event; commonly { userId } or {}
userId?: string;
};
timer?: { ms: number }; // present on game-lifecycle events
}
Event allowlist
Once the SDK is initialized, the bridge emitter filters every outgoing event against the OutgoingMessage enum (apps/games-hub-nrc/src/lib/types/bridge.ts:86). Anything not in that enum 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 OutgoingMessage.
This is the complete list of events you can listen for:
type |
Origin | Section |
|---|---|---|
boot |
Games Hub | Lifecycle |
ready |
Games Hub | Lifecycle |
initialized |
Games Hub | Lifecycle |
startScreenOpened |
Games Hub | Navigation & UI |
modalOpened |
Games Hub | Navigation & UI |
modalClosed |
Games Hub | Navigation & UI |
showFullScreen |
Games Hub | Navigation & UI |
hideFullScreen |
Games Hub | Navigation & UI |
gameData |
Games Hub | Game metadata |
started |
OnePlayer (bubbled) | Gameplay |
paused |
OnePlayer (bubbled) | Gameplay |
completed |
OnePlayer (bubbled) | Gameplay |
share |
OnePlayer (bubbled) | User actions |
visitUrl |
OnePlayer (bubbled) / Games Hub | User actions |
actionClicked |
OnePlayer (bubbled) | User actions |
userCheated |
Games Hub | Solution reveal |
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
windowwill 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:43 - 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:
{ type: 'started', timestamp: number, data: { ms: number }, metadata: { userId, puzzleId, gameName, gameType, ... }, timer: { ms: number } }
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, withtype: '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:
{ type: 'completed', timestamp: number, data: GameResultData[], // always contains { unit: 'milliseconds', value: <ms> } metadata: { userId, puzzleId, gameName, gameType, ... }, timer: { ms: number } }Where
GameResultDatais one of:{ unit: 'solution' | 'rank' | 'points' | 'attempts' | 'milliseconds', value: string | number }
Navigation & UI
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
- Fullscreen modals:
- Payload:
{ type: 'modalOpened' | 'modalClosed', timestamp: number, data: { name: ModalName }, metadata: {} }
Modal names
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 viamodalService),copyConfirmation,feedback,onePlayerPrompt(opened viadialogService), andmenu(opened viamenuService) all emit the samemodalOpened/modalClosedevents as the fullscreen modals. Filter ondata.namein your analytics provider if you need to distinguish “page-like” overlays from transient ones (e.g.copyConfirmationis a short toast-style confirmation).Pairing with
showFullScreen/hideFullScreen: Only the fullscreen variants (startScreen,endScreen) are paired withshowFullScreen/hideFullScreen— the other overlays do not change the fullscreen state.
The full union of modal names is the
ModalNametype inapps/games-hub-nrc/src/lib/types/bridge.ts. If new overlays are added to the Games Hub later, they’ll appear here as additionalnamevalues — 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.
- Source:
apps/games-hub-nrc/src/lib/webcomponents/Player.svelte:343 - 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:631 - Payload:
{ type: 'share', timestamp: number, data: { text: string }, metadata: { userId: string } }
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:606 - Payload:
{ type: 'visitUrl', timestamp: number, data: { url: string, target?: '_self' | '_blank' }, metadata: { userId: string } }
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:
ActionNameinapps/games-hub-nrc/src/lib/types/bridge.ts - Payload:
{ type: 'actionClicked', timestamp: number, data: { name: ActionName }, metadata: { userId, puzzleId, gameName, gameType, ... }, timer: { ms: number } }
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 |
The full union of action names is the
ActionNametype inapps/games-hub-nrc/src/lib/types/bridge.ts, kept in sync withOnePlayerActionNameinapps/oneplayer/src/player/external-events.ts. If new tracked actions are added later, they’ll appear here as additionalnamevalues — 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:479 - Payload:
{ type: 'userCheated', timestamp: number, metadata: { userId: string }, timer: { ms: number } }
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 calls bridge.externalEventEmitter.sendOnePlayerEvent(event) which checks messageIsAllowed(event.name) against OutgoingMessage — this is the gate that drops events not in the allowlist.
- Inbound listener:
apps/games-hub-nrc/src/lib/components/OnePlayer.svelte:218 - Outbound filter:
apps/games-hub-nrc/src/lib/services/externalEventEmitter.ts:190,228