feat(frontend): add posthog analytics for whats-new banner - #986
feat(frontend): add posthog analytics for whats-new banner#986dembrane-sam-bot wants to merge 1 commit into
Conversation
|
Superseded by #989. Same goal, different mechanics, for two reasons this branch could not measure what it set out to: the IFrame API script it injects is served from www.youtube.com, which script-src (vercel.json) does not allow, so the player never initialises in production and every watch metric silently reads zero while the events still fire; and the localStorage last_login_time self-heals to now on first render, which fabricates seconds_since_login for every already-logged-in user, exactly the cohort the 2026-08 video targets. #989 listens to the embed's own postMessage stream instead (enablejsapi=1, no script, no CSP change, nocookie origin kept) and answers login recency with a PostHog query against the existing user_logged_in history; it also sends progress milestones as they are crossed, so a closed tab keeps the record. |
Pull request was closed
### What is this change? PostHog analytics for the "What's new" release video modal, answering: did they watch, how long, in which language, and how the showing relates to login recency. **Events** | event | when | notable properties | |---|---|---| | `whats_new_modal_opened` | the modal shows | `trigger` (`auto`/`manual`), `seconds_since_page_load` | | `whats_new_video_started` | first play | `seconds_after_open` | | `whats_new_video_progress` | furthest position crosses 25/50/75/95% | `milestone_percent` | | `whats_new_video_completed` | the player reports ended | `video_watched_seconds`, `video_duration_seconds` | | `whats_new_modal_closed` | dismiss or pagehide | `reason`, `video_watched`, `video_watched_seconds`, `video_duration_seconds`, `video_percent_watched`, `video_max_percent`, `modal_open_seconds` | Every event carries `language` (the UI language), `version` (the release entry) and `trigger` (the automatic showing vs the sidebar's "What's new"). **How playback is observed** The embed runs on www.youtube-nocookie.com, the only YouTube origin `frame-src` allows, and `script-src` allows no YouTube origin at all. So the official IFrame API loader (a script served from www.youtube.com) is not an option: the CSP would block it in production and every metric would silently read zero. Instead the iframe URL gains `enablejsapi=1` plus `origin`, and the modal speaks the player's own postMessage protocol directly: a `listening` handshake, then `infoDelivery` messages carrying `currentTime`, `duration` and `playerState`. No script loads, no CSP change, and the nocookie privacy posture stays as it was. Watch time is the sum of small forward `currentTime` steps: a seek does not count as watching, double speed counts the content covered, rewatching counts again but the percentage caps at 100. `video_max_percent` is the furthest position reached. Milestones fire the moment they are crossed, so a tab closed mid-video still leaves a durable record; `pagehide` flushes the close summary for the walk-away case (posthog-js flushes its queue with sendBeacon there). **Time since login** Deliberately not stamped from the client. A client-side breadcrumb (localStorage written at login) fabricates the answer for every already-logged-in user, misses sessions restored from a refresh token, and dies with cleared site data. The honest source already exists: PostHog holds each person's event history, including `user_logged_in`. Recency is a query-side join, for example (HogQL): ```sql select opens.person_id, any(opens.opened_at) as opened_at, max(prior.timestamp) as last_login_before, dateDiff('day', max(prior.timestamp), any(opens.opened_at)) as days_since_login from ( select person_id, min(timestamp) as opened_at from events where event = 'whats_new_modal_opened' and properties.version = '2026-08' group by person_id ) as opens left join events as prior on prior.person_id = opens.person_id where prior.event = 'user_logged_in' and prior.timestamp < opens.opened_at group by opens.person_id ``` Swap the `user_logged_in` filter for any prior event with a 30 minute guard to measure time since last activity instead, since sessions persist across days and form logins are rarer than visits. **Questions this schema answers** 1. Play rate: `whats_new_modal_opened` to `whats_new_video_started` funnel. 2. Drop-off: milestone counts at 25/50/75/95, split by language. 3. Language reach: the video is English; completion by locale decides subtitles vs localised videos. 4. Voluntary interest: `trigger=manual` reopens and replays vs the automatic showing. 5. Quick dismissals: `modal_open_seconds` with `video_watched=false`. 6. Dormancy: the query above. Supersedes #986: same goal, but that branch loads the IFrame API script that `script-src` blocks (metrics would silently read zero in production), stores last-login in localStorage (the self-heal fabricates it for every existing user), and reports watch data only on close (a closed tab loses the whole record). ### Test plan - `vitest run src/components/release`: 49 tests pass, including new tracker unit tests and modal analytics tests that hand-deliver widget messages (origin and source checks, milestone dedupe, summary-once semantics). - `biome lint`, `tsc` and `vite build` are clean; lingui catalogs re-extracted (line-reference shifts only, no new strings). - The two failures in `useChunkAnchorScroll.test.tsx` reproduce on a clean main checkout; unrelated and untouched here.
What is this change?
Adds robust PostHog analytics for the 'What's new' banner (ReleaseVideoModal) to track user engagement with release update videos:
whats_new_modal_openedwith user's UI language (language), opaque version string, and seconds elapsed since their last login.whats_new_video_startedonce when they play/start watching the embedded video.whats_new_modal_closedon close/dismiss containing:video_watched_seconds: Total elapsed wall-clock play duration (perfectly handles pause/resume intervals).video_duration_seconds: Total length of the video queryable from the YouTube player API.video_percent_watched: Proportion of the video watched.video_watched: Boolean indicating if they played it at all.language: User UI language.seconds_since_login: Time elapsed since login.version: Release version.To support tracking "seconds since login", we now store the current timestamp in
localStorageaslast_login_timeon successful logins/registrations and clear it on logout. If already logged in, we self-heal by initializing it to the current timestamp on the first modal render.What led to this?
Operator request: "Can you make posthog events for the new 'What's new' banner? I need to know if / how long they watched the video / what language they are on / how long has it been since they logged in?"
Tier
Tier 1/2 (Frontend application feature code).
Confidence
High. The implementation uses the standard YouTube IFrame Player API headlessly (without loading any external scripts if already present, and gracefully degrading if unavailable) and aggregates precise, non-overlapping play intervals.