FiftyStrong · PostHog Dev (project 563462)

The user's lifecycle, and the events that watch it

Latest run 19 Aug 2026, 01:15–01:28 HKT Read back from PostHog 19 Aug, 02:1x HKT Branch analytics-posthog @ 58b250d

The latest run, laid out in the lifecycle

Every mulberry chip is a real event record read back out of PostHog from the 19 Aug verification run — tap one to see the payload as stored. Green chips are catalogue events verified on earlier runs but not exercised in this one.

This run — tap for the stored payload Earlier run — tap for its latest stored payload Wired, never fired yet Not wired (deliberate)
Landingfiftystrong.app · static HTML

This run entered at /quiz directly, so no landing view — the event is verified from earlier drives.

Catalogue

Quiz/quiz · anonymous id BKB3TKUT_r8jhgEB

Three sittings in one browser: the drive reloaded the page twice, and each reload before the results correctly fired quiz_abandoned for the sitting it ended — an abandon next to a fresh quiz_started is a reload boundary, not a logging error. These records predate the 19 Aug view/submit split: choice rode the next screen's view (null on interstitials). Since the split, arrival fires quiz_screen_viewed and the answer fires quiz_screen_submitted with the choice.

Paywall/quiz screen s5-5

Scrolled to the bottom — all four depth thresholds and nine named sections recorded — then left without checking out, which is exactly what paywall_abandoned says.

Catalogue — not in this run

checkout_failedneeds a failure
PurchaseStripe webhook · server, posthog-node

This run signed in as Maggie (already a subscriber) rather than paying again — purchase_completed is once-per-person by design. Verified on her original purchase.

Catalogue

The app/today · person 3bcb3f24… (Maggie)

The moment of the weld: $identify carries the anonymous quiz id in $anon_distinct_id, joining this run's quiz sitting to Maggie's person.

Workout player/session/morning-wake-up · attempt 5f8156c6…

Her second attempt at this activity (attempt_no: 2). She skipped the first movement 20 seconds in, then quit during a cooldown — the abandonment rode sendBeacon out of a dying page and still landed.

Catalogue — not in this run

ChurnStripe webhook · server

Wired 19 Aug (commit 817a074); fires on the first real cancellation or full refund with the churn autopsy: week number, activities completed, days since the last activity.

subscription_cancellednever fired subscription_refundednever fired faq_openednot wired retest_completednot wired

Evidence ledger

All-time counts in the Dev project as of 19 Aug 2026, 02:1x HKT. Every browser and server event that has ever been designed is on this list — nothing verified is omitted, nothing unverified is dressed up.

EventTriggerKey payloadStatusCount
Funnel — static pages
landing_viewedMarketing page loads, once per browserin_app_browserverified5
quiz_startedHer first answer tap, once per pageload — not merely loading /quizin_app_browserverified8
quiz_screen_viewedArriving on any screen, deduped per sittingscreen_id · previous_screen · seconds_on_previousverified94
quiz_screen_submittedHer answer became final — she moved on with it picked. Re-fires only if a back-navigation changes it. Split from the view 19 Augscreen_id · act · choice · seconds_on_screenwire-verified, ingesting3 sent
quiz_abandonedPage hidden/unloaded before resultslast_screen_id · answered_count · seconds_on_screenverified2
quiz_completedResults screen reachedemphasis · secondary_emphasis · mode · answered_countverified3
Paywall
paywall_viewedPaywall screen reachedemphasis · mode · plan_defaultverified3
paywall_scrolledCrossing 25/50/75/100% depth, once eachdepth_pctverified4
paywall_section_viewedA named section scrolls into viewsectionverified11
plan_selectedShe taps a different planplan · price_usd · from_defaultverified3
checkout_started“Start my plan”, before the Stripe redirectplan · price_usdverified3
checkout_failedNo session URL, or network failurereason · plannever fired0
paywall_abandonedLeaving the paywall without checkoutseconds_on_paywall · max_scroll · plans_viewed · sections_readverified2
Purchase — server-side
purchase_completedFulfilment, exactly once per personplan · price_usd · had_quizverified1
$create_aliasEvery fulfilment — the identity weldquiz id ↔ person idverified
$identifyEvery fulfilment + app pagesname · plan · stripe ids · $anon_distinct_idverified
The app
app_opened/today loads for a signed-in woman — once per page load, not per re-renderprogramme_week · days_since_purchase · has_trained_beforeverified5
activity_startedThe player mountsactivity_id · attempt_id · attempt_no · resumed · weekverified2 +5*
move_completedA movement finished — same values as the DB rowactivity_id · attempt_id · move_slug · ordinalverified3
move_skippedNext tapped mid-movementmove_slug · ordinal · seconds_in_moveverified2
activity_completedThe closing stepduration_s · moves_done · moves_repeatedverified1 +1*
activity_abandonedExit tapped or tab closed — sendBeaconat_step_kind · pct_complete · seconds_in · last_move_slugverified1 +3*
checkin_submitted“How did that feel?” after completionexertion · activity_id · weekverified2
media_autoplay_blockedA rejected play() the player swallowedwhere · in_app_browserverified2
Churn — server-side
subscription_cancelledcustomer.subscription.deleted webhookweek_number · activities_completed_total · days_since_last_activitywired 19 Aug, never fired0
subscription_refundedA full refundsame as cancelledwired 19 Aug, never fired0
faq_openednot wired0
retest_completednot wired0

* Counts marked +N* are verified rows under the old session_* names, from before the 18 Aug rename to activity_*.

Identity weld

Verified again in this run: the anonymous quiz sitting and Maggie's signed-in training resolve to one person.

Anonymous quiz idBKB3TKUT_r8jhgEB — all 33 screen views, the completion, and the paywall hang off it.
$identify at /todayCarries $anon_distinct_id: BKB3TKUT_r8jhgEB — tap it in the flow above to see the stored record.
One person3bcb3f24…, named “Maggie” from her quiz answer, not her card. Quiz funnel + training in one timeline.
Ordering matters: alias first, identify second. The alias merge resolves conflicting person properties in ways that can clobber an identify sent a moment earlier — that is exactly how the repaired name reverted to <redacted> once.

Five silent failures

Every one of these returned success and produced no data. They are why this page verifies by reading events back out of PostHog instead of trusting the code.

  1. The scrubber ate posthog-js's own token. The PII denylist redacted properties.token — the project API key. PostHog answers Ok to an invalid token and discards the event. Three days of blank funnel.
  2. Abandonment died on unload. A queued fetch does not survive the page going away. activity_abandoned now rides sendBeacon — the record in the flow above is the proof it survives.
  3. The bot filter measured nothing. posthog-js silently drops every capture from a “likely bot”, so headless QA drives verified a pipeline that recorded zero events. Filter disabled on dev hosts only.
  4. The scrubber ate name. Persons displayed as <redacted>. Now an explicit exception; everything else stays denied.
  5. choiceOn() threw inside the capture arguments. It reached for a DOM helper scoped to a different closure; the ReferenceError killed every quiz_screen_viewed except the first screen — and quiz_completed and paywall_viewed behind it. Found while building this page; fixed in commit 5173338 and verified by this run's 33 screen records.

Method

Every mulberry record in the flow is the row PostHog returned for the 19 Aug run — HogQL queries against the Dev project selecting each event's full custom payload, plus $device_type and the identity fields. SDK metadata ($browser, $screen, session recording internals) is omitted from the payload views for legibility; nothing custom is. Verified in the ledger means returned by a read-back query — never inferred from source. Times are HKT.

Canonical event reference: docs/analytics-events.md in the repo — trigger, full payload, and call site for every event, kept in the same commits as the code it describes.