Report the detected player to GA4 - #32
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_detectedevent. OnedetectPlayer()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:player_vendoranthias,brightsign,unknownplayer_platformlinuxplayer_modelXT1144player_categorysignage,browser,botplayer_engineqtwebengineplayer_engine_version83(numeric)player_below_floortrueplayer_confidencehighplayer_staletrueThree judgement calls worth review:
"unknown", not omitted — an absent param reads as "(not set)" in GA4, which is indistinguishable from a dimension nobody registered.engine_versionstays numeric so GA4 will actually aggregate it.player_staleis sent despite being derivable from vendor + engine. "Stale" is a judgement this app makes (see the reasoning instale-player.js) rather than something a report author should re-derive, and it's what makes notice impressions countable against upgrades.sourcesis deliberately not sent: page JS can only read the UA and referrer (theX-Requested-Withheader 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
gtagso nothing hit the live property:player_staleagreed with whether the notice actually mountedgtagpath (dev, or a blocked tag in the field) is a silent no-op with the clock still renderingBefore 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_versionas a metric, the rest as dimensions.🤖 Generated with Claude Code