From a3fbe5b5d43f36a846847beca68a0aab20f9e2f0 Mon Sep 17 00:00:00 2001 From: Cyborg Viktor <307551610+vpetersson-bot@users.noreply.github.com> Date: Fri, 14 Aug 2026 07:18:11 +0000 Subject: [PATCH] feat: pin the GA4 client_id to the device GA4's own client_id lives in the _ga cookie and these players largely boot with fresh storage, so it churns and does not identify a screen. Where a Screenly player sends asset metadata there is now a stable device id, and signage-kit derives a client_id from the same hash that produces player_device, so the two always agree about which screen this is. The inline tag becomes analyticsBootstrap() from the kit. It has to own the config call, because client_id is stamped onto each event as it is sent; it cannot move into main.js, because screens that never load the bundle would then report nothing at all; and it cannot be rendered into this HTML, because the page is edge-cached with no per-screen component and every screen would inherit whichever one warmed the cache. So it fetches the no-store /api/player route at runtime. Fails open: profile arrival, fetch rejection, a non-ok response and a timeout all end in exactly one config call, so an offline screen still reports with GA4's default behaviour. Also bumps signage-kit to 2026.8.8. Co-Authored-By: Claude Opus 5 (1M context) --- bun.lock | 4 ++-- package.json | 2 +- src/components/Layout.tsx | 32 +++++++++++++------------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index ddd9478..080c5a8 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "screenly-rss-reader", "dependencies": { - "@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.8.6", + "@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.8.8", "hono": "^4.12.33", "qrcode-generator": "^2.0.4", }, @@ -191,7 +191,7 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@screenly-labs/signage-kit": ["@screenly-labs/signage-kit@github:Screenly-Labs/signage-kit#7b0d8f1", { "dependencies": { "@fontsource-variable/bricolage-grotesque": "^5.3.0", "@fontsource-variable/fraunces": "^5.3.0", "@fontsource-variable/hanken-grotesk": "^5.3.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", "@fontsource-variable/newsreader": "^5.3.0", "@fontsource/space-mono": "^5.3.0" }, "peerDependencies": { "@csstools/postcss-cascade-layers": "^6", "browserslist": "^4", "esbuild": "^0.28", "lightningcss": "^1.32", "postcss": "^8" }, "optionalPeers": ["@csstools/postcss-cascade-layers", "postcss"] }, "Screenly-Labs-signage-kit-7b0d8f1", "sha512-x61h6zlMCOW0MYcuIQgNeZF3TEI02rmafrASohDN5NT/PD0YSnANfWcKbZx32ItiC3fDihpeQH5c4Iu5GiKUiw=="], + "@screenly-labs/signage-kit": ["@screenly-labs/signage-kit@github:Screenly-Labs/signage-kit#85934fa", { "dependencies": { "@fontsource-variable/bricolage-grotesque": "^5.3.0", "@fontsource-variable/fraunces": "^5.3.0", "@fontsource-variable/hanken-grotesk": "^5.3.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", "@fontsource-variable/newsreader": "^5.3.0", "@fontsource/space-mono": "^5.3.0" }, "peerDependencies": { "@csstools/postcss-cascade-layers": "^6", "browserslist": "^4", "esbuild": "^0.28", "lightningcss": "^1.32", "postcss": "^8" }, "optionalPeers": ["@csstools/postcss-cascade-layers", "postcss"] }, "Screenly-Labs-signage-kit-85934fa", "sha512-RcJ7vWlJWT/TFu5YdrAQAbqaiHnqobWRk2XwaC0kW1X+bCzJoDWT9kS/pMDxGEeNEixMZWtEG/vjBdg/x0fIOw=="], "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], diff --git a/package.json b/package.json index c837dea..847c800 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "license": "AGPL-3.0-only", "dependencies": { - "@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.8.6", + "@screenly-labs/signage-kit": "github:Screenly-Labs/signage-kit#2026.8.8", "hono": "^4.12.33", "qrcode-generator": "^2.0.4" }, diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 7c1c9d5..f5a6601 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,4 +1,6 @@ import { html, raw } from 'hono/html' +import { analyticsBootstrap } from '@screenly-labs/signage-kit/analytics-bootstrap' +import { PLAYER_PROFILE_PATH } from '@screenly-labs/signage-kit/analytics-server' import { GATE } from '@screenly-labs/signage-kit/gate' import type { Child } from 'hono/jsx' @@ -20,32 +22,24 @@ const sentryScript = (id?: string) => ? html`` : '' -// Inline a value as a JS object literal inside a ` would close the tag early, -// and U+2028/U+2029 are valid in JSON but not in JS string literals. Escaping -// them as \u… keeps the JSON valid while neutralizing both hazards, so a feed -// title can never break out of the script. -const SCRIPT_UNSAFE = /[<>\u2028\u2029]/g -const jsonForScript = (value: unknown): string => - JSON.stringify(value).replace( - SCRIPT_UNSAFE, - (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}` - ) - // The `source`/`source_title` on the config call ride along on GA4's automatic // page_view, so a visit is attributed to its feed (main.ts tags the same pair on // every later event). +// The kit bootstrap owns the config call so it can pin client_id to the Screenly device id, +// making one screen one GA4 user: GA4's own client_id lives in the _ga cookie and these players +// largely boot with fresh storage, so it churns. The feed pair is passed as configParams and +// merged under client_id, so the feed attribution above is unchanged. const gaScript = (id?: string, feedId?: string, feedTitle?: string) => id ? html` - ` + ${raw( + analyticsBootstrap({ + gaId: id, + profilePath: PLAYER_PROFILE_PATH, + configParams: { source: feedId ?? '', source_title: feedTitle ?? '' } + }) + )}` : '' const Layout = (props: LayoutProps) => html`