Skip to content

Report the detected player to GA4 - #32

Merged
vpetersson merged 1 commit into
masterfrom
feat/track-player-in-ga4
Jul 16, 2026
Merged

Report the detected player to GA4#32
vpetersson merged 1 commit into
masterfrom
feat/track-player-in-ga4

Conversation

@vpetersson

Copy link
Copy Markdown
Contributor

The stale-player notice already profiles every viewer to decide whether to warn it, but that judgement was made and thrown away on each load — so the population we're warning, and whether it ever shrinks, was unmeasurable.

This sends the same profile to GA4 as a player_detected event. One detectPlayer() call now feeds both consumers, so the notice and the telemetry can never disagree about what a screen is running.

Client-side for the same reason the notice is: the SSR page cache is keyed by asset version + country + timezone with no user-agent component, so a server-side profile would describe whichever player missed the cache and then be attributed to every hit behind it.

The params

All prefixed player_, since the GA property is shared with the other Screenly-Labs apps:

Param Example
player_vendor anthias, brightsign, unknown
player_platform linux
player_model XT1144
player_category signage, browser, bot
player_engine qtwebengine
player_engine_version 83 (numeric)
player_below_floor true
player_confidence high
player_stale true

Three judgement calls worth review:

  • Nulls are sent as "unknown", not omitted — an absent param reads as "(not set)" in GA4, which is indistinguishable from a dimension nobody registered.
  • engine_version stays numeric so GA4 will actually aggregate it.
  • player_stale is sent despite being derivable from vendor + engine. "Stale" is a judgement this app makes (see the reasoning in stale-player.js) rather than something a report author should re-derive, and it's what makes notice impressions countable against upgrades.

sources is deliberately not sent: page JS can only read the UA and referrer (the X-Requested-With header isn't exposed), so it's near-constant here and would spend a custom dimension on nothing.

Verification

Unit tests cover the param mapping against the same real player UA fixtures the stale-notice suite pins. Beyond that, I drove the built bundle in a browser across four spoofed player UAs — old Anthias, current Anthias, Screenly, BrightSign — with a stubbed gtag so nothing hit the live property:

  • exactly one event per load, params correct per player
  • player_stale agreed with whether the notice actually mounted
  • no-gtag path (dev, or a blocked tag in the field) is a silent no-op with the clock still rendering

Before this reports anything

The params stay invisible in reports until they're registered as custom definitions in the GA4 UI (Admin → Custom definitions) — player_engine_version as a metric, the rest as dimensions.

🤖 Generated with Claude Code

The stale-player notice already profiles every viewer to decide whether to
warn it, but that judgement was made and thrown away on each load — so the
population we are warning, and whether it ever shrinks, was unmeasurable.

Send the same profile as a `player_detected` event. One detectPlayer() call
now feeds both consumers, so the notice and the telemetry can never disagree
about what a screen is running.

Client-side for the same reason the notice is: the SSR page cache is keyed by
asset version + country + timezone with no user-agent component, so a server
-side profile would describe whichever player missed the cache and then be
attributed to every hit behind it.

Params are prefixed `player_` because the GA property is shared with the other
Screenly-Labs apps. Three calls worth naming:

* Nulls are sent as "unknown" rather than omitted — an absent param reads as
  "(not set)" in GA4, indistinguishable from a dimension nobody registered.
* engine_version stays numeric so GA4 will aggregate it.
* player_stale is sent despite being derivable from vendor + engine: "stale"
  is a judgement this app makes (see stale-player.js) rather than something a
  report author should re-derive, and it is what makes notice impressions
  countable against upgrades.

`sources` is not sent — page JS can only read the UA and referrer, so it is
near-constant here and would spend a custom dimension on nothing.

Verified against the built bundle in a browser across four spoofed player UAs:
one event per load, player_stale agreeing with whether the notice mounted, and
a silent no-op when gtag is absent (dev, or a blocked tag in the field).

Note: the params stay invisible in reports until they are registered as custom
definitions in the GA4 UI — engine_version as a metric, the rest dimensions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vpetersson
vpetersson merged commit 99f04ad into master Jul 16, 2026
1 check passed
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.

1 participant