Critical Path Analytics
Critical Path instrumentation via PostHog tracks player engagement, retention, and progression across the daily puzzle feature.
Event Taxonomy
| Event | Trigger | Properties | Semantic meaning |
|---|---|---|---|
critical_path_tile_tapped | CriticalPathTile click | is_played_today: false (constant) | User tapped the Critical Path tile |
critical_path_puzzle_started | PlayView mounted with puzzle generated | node_count: int, puzzle_date: string (YYYY-MM-DD), theme_active: string | User began solving today's puzzle |
critical_path_puzzle_completed | Game won state reached | time_taken_seconds: int, move_count: int, percentile: int | null, stim_xp_earned: int, theme_active: string | User solved today's puzzle |
critical_path_puzzle_abandoned | PlayView unmounted while gameState === 'playing' | elapsed_seconds: int, nodes_tapped: int, total_nodes: int | User navigated away before completion |
critical_path_tutorial_viewed | HowToPlayOverlay shown to user | first_time: bool | User viewed the tutorial overlay |
critical_path_undo_tapped | Undo button pressed during gameplay | path_length_at_undo: int | User used the undo feature |
critical_path_theme_changed | Theme picker selection in PlayView | from_theme: string, to_theme: string | User changed puzzle theme |
critical_path_stim_streak_milestone | Streak crosses 7, 30, or 100-day threshold | streak: int | User hit a streak milestone |
critical_path_theme_unlocked | New cosmetic theme unlocked via progression | theme_name: string | User unlocked a new theme |
critical_path_share_card_generated | Share result modal rendered | percentile: int | User tapped share button (card generated) |
critical_path_share_card_shared | Native share success via navigator.share() | percentile: int | User completed native share flow |
critical_path_share_tapped | Deprecated (Phase 86a) — Use critical_path_share_card_generated instead | — | Legacy share initiation event |
Notes
is_played_todayis hardcodedfalse(PR #1078). Before PR #1078, this was a practical always-false — the tile was only interactive when!isPlayed, so the property was deterministic in practice. PR #1078 (feat/critical-path-dismiss-when-done) made this a formal guarantee: the tile is now entirely removed from the DOM whenisPlayedis true (v-if="!isPlayed"at the component root), so the event cannot fire at all when the user has already played.is_played_today: falseis now a constant, not a conditional.puzzle_dateis UTC YYYY-MM-DD string, never a timestamp. Maintains PII minimization per brand standards.theme_activeproperty (R5 Phase 86a) — Added topuzzle_startedandpuzzle_completedto track which theme the user selected for that puzzle session. Enables cohort analysis by theme preference.time_taken_secondsandmove_count(R5 Phase 86a) — Replacedelapsed_seconds/node_countonpuzzle_completedfor better semantic clarity.time_taken_secondsis the actual solve time in seconds;move_countis the number of node taps to complete.stim_xp_earnedis now live (R5 Phase 86a) — Populated from therecordCriticalPathPlaymutation response. No longernull.critical_path_theme_changedis now live (R5 Phase 86a) — Fires synchronously before state mutation inonThemeSelect. Captures bothfrom_themeandto_themestrings. Enables theme preference funnel analysis.critical_path_stim_streak_milestone(R5 Phase 86a) — Fires when streak crosses 7, 30, or 100-day thresholds. Detected by comparing pre-play streak against the milestone list afterGET_STIM_XP_STATUSrefetch.critical_path_theme_unlocked(R5 Phase 86a) — Fires once per newly unlocked cosmetic theme, detected by diffingunlockedThemespre/post refetch. Enables analysis of theme progression friction.critical_path_share_card_generatedandcritical_path_share_card_shared(R5 Phase 86a) — Split the legacycritical_path_share_tappedevent into two distinct steps: card generation (tap share CTA) and completion (user confirms native share). Only fires onnavigator.share()success, not on copy-to-clipboard or cancel. Enables share funnel analysis.critical_path_share_tappedis deprecated (R5 Phase 86a) — Replaced bycritical_path_share_card_generatedandcritical_path_share_card_shared. The old event remains inanalytics.tsas dead code and can be removed in a cleanup pass.send_instantlyflag onpuzzle_abandoned. PostHog'ssend_instantly: trueoption flushes the event before route teardown, mitigating loss on hard navigation. Verify in a real browser session (not jsdom) that the event reaches PostHog when navigating back to/dashboardmid-game.
PostHog Manual Setup Runbook
This runbook is for the PostHog admin (typically Josh or an observability owner) to create the funnel and cohort definitions. All events below are live as of Phase 86a (v1.19).
1. Create Funnel: Puzzle Started → Completed
Steps:
- Open PostHog project > Data Management > Funnels
- Create new funnel: "Critical Path Completion"
- Configure steps:
- Step 1:
Event = critical_path_puzzle_started(no property filters) - Step 2:
Event = critical_path_puzzle_completed(no property filters)
- Step 1:
- Set funnel window: 24 hours (conversion window per step)
- Save and view drop-off % between start and completion
What this measures:
- Intent-to-start: users who began a puzzle
- Completion rate: % of starters who solved today's puzzle
- Drop-off: abandonment friction in the puzzle flow (measured separately via
critical_path_puzzle_abandoned)
Optional: Extended funnel (Tile → Puzzle flow)
If you wish to measure full engagement funnel from tile discovery to completion:
- (Alternative) Configure steps:
- Step 1:
Event = critical_path_tile_tapped(no property filters) - Step 2:
Event = critical_path_puzzle_started(no property filters) - Step 3:
Event = critical_path_puzzle_completed(no property filters)
- Step 1:
What this measures:
- Tile discovery: % of users who discovered the tile
- Intent-to-start: % who tapped and began a puzzle
- Completion: % of starters who solved today's puzzle
2. Create Cohort: Critical Path 7-Day Players
Definition: Users who completed a puzzle (critical_path_puzzle_completed event) on 7 or more distinct days within a rolling 14-day window.
Steps:
Open PostHog project > Cohorts > New Cohort
Name: "Critical Path 7-day players"
Cohort type: Behavioral
Filter configuration:
- Condition 1:
critical_path_puzzle_completedevents - Count: Count of event fires — set to ≥ 7
- Time frame: Last 14 days
- Distinct: By
event.timestampgrouped by calendar date (ensure 7 distinct dates, not just 7 event fires on 1 day)
Note: PostHog's "Distinct" option ensures the count reflects unique date occurrences, not raw event count. If your PostHog UI doesn't expose this granularly, use the SQL mode:
sqlSELECT DISTINCT person_id FROM events WHERE event = 'critical_path_puzzle_completed' AND toDate(timestamp) >= today() - 14 GROUP BY person_id HAVING uniqExact(toDate(timestamp)) >= 7- Condition 1:
Save cohort — use this for cross-cohort retention analysis (CP players vs. non-players)
What this measures:
- Committed players: users with consistent 7+ day engagement
- Retention lift: compare 7-day retention between CP players and control group
- Feature impact: answers "Is Critical Path lifting 7-day goals/retention?"
Monitoring Queries
Daily Active Players
SELECT count(distinct person_id) as daily_active
FROM events
WHERE event = 'critical_path_puzzle_started'
AND toDate(timestamp) = today()Completion Rate (Tile Taps → Completed)
SELECT
count(distinct IF(event = 'critical_path_tile_tapped', person_id, null)) as tile_taps,
count(distinct IF(event = 'critical_path_puzzle_completed', person_id, null)) as completions,
round(completions / tile_taps, 2) as completion_rate
FROM events
WHERE toDate(timestamp) = today()Average Puzzle Duration
SELECT avg(JSONExtractFloat(properties, 'time_taken_seconds')) as avg_solve_time_seconds
FROM events
WHERE event = 'critical_path_puzzle_completed'
AND toDate(timestamp) >= today() - 7Undo Usage
SELECT
count(1) as undo_taps,
count(distinct person_id) as users_using_undo
FROM events
WHERE event = 'critical_path_undo_tapped'
AND toDate(timestamp) >= today() - 7Abandonment Rate
SELECT
count(distinct IF(event = 'critical_path_puzzle_started', person_id, null)) as starts,
count(distinct IF(event = 'critical_path_puzzle_abandoned', person_id, null)) as abandons,
round(abandons / starts, 2) as abandon_rate
FROM events
WHERE toDate(timestamp) >= today() - 7Implementation Details
Source code: ionic_frontend/src/play/analytics.ts
Components instrumented:
ionic_frontend/src/play/PlayView.vue— puzzle lifecycle eventsionic_frontend/src/play/HowToPlayOverlay.vue— tutorial eventionic_frontend/src/components/dashboard/CriticalPathTile.vue— tile tap event
Direct integration: All events use the standard posthog.capture(event_name, properties) pattern. No special batching or sampling.
Common gotchas
navigator.share() requires a user-gesture origin
When implementing share flows, navigator.share({ files }) (Web Share Level 2) must be called synchronously from a user gesture handler (click, tap). Do not insert await calls between the gesture and the navigator.share() call — in particular, avoid network round-trips.
Example of what fails on Safari:
async function handleShare() {
const response = await fetch('/api/something') // ❌ Network round-trip
navigator.share({ files: [blob] }) // ❌ NotAllowedError: not a user gesture
}Example of what works:
function handleShare() {
const blob = renderCardToCanvas() // ✅ Synchronous
navigator.share({ files: [blob], text, url }) // ✅ Still in the gesture context
}The share card implementation in ionic_frontend/src/play/shareResult.ts renders the PNG from local Canvas (no network), so this requirement is satisfied by design.
Related Decisions
For the R5 full-product launch scope and event definition rationale, see docs/product/completed/critical-path-full-product-prd.md.
Last updated: 2026-06-05 (PR #1078 — is_played_today hardcoded false guarantee)