Skip to content

feat(frontend): measure whats-new video engagement in posthog - #989

Merged
spashii merged 1 commit into
mainfrom
feat/whats-new-video-analytics
Aug 10, 2026
Merged

feat(frontend): measure whats-new video engagement in posthog#989
spashii merged 1 commit into
mainfrom
feat/whats-new-video-analytics

Conversation

@spashii

@spashii spashii commented Aug 10, 2026

Copy link
Copy Markdown
Member

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):

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.

Events for the release video modal: opened (auto/manual trigger), started,
progress milestones, completed, and a close summary with watch seconds and
percent. Playback state comes from the embed's own postMessage stream
(enablejsapi=1 plus a listening handshake), so no YouTube script loads, the
CSP stays untouched, and the frame stays on the nocookie origin. Milestones
fire as crossed so a closed tab still leaves a record; pagehide flushes the
summary. Login recency is a PostHog query-side join, not a client guess.
@spashii
spashii enabled auto-merge August 10, 2026 18:38
@spashii
spashii merged commit aed8bfe into main Aug 10, 2026
11 checks passed
@spashii
spashii deleted the feat/whats-new-video-analytics branch August 10, 2026 18:39
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