Skip to content

Critical Path Analytics

Critical Path instrumentation via PostHog tracks player engagement, retention, and progression across the daily puzzle feature.

Event Taxonomy

EventTriggerPropertiesSemantic meaning
critical_path_tile_tappedCriticalPathTile clickis_played_today: false (constant)User tapped the Critical Path tile
critical_path_puzzle_startedPlayView mounted with puzzle generatednode_count: int, puzzle_date: string (YYYY-MM-DD), theme_active: stringUser began solving today's puzzle
critical_path_puzzle_completedGame won state reachedtime_taken_seconds: int, move_count: int, percentile: int | null, stim_xp_earned: int, theme_active: stringUser solved today's puzzle
critical_path_puzzle_abandonedPlayView unmounted while gameState === 'playing'elapsed_seconds: int, nodes_tapped: int, total_nodes: intUser navigated away before completion
critical_path_tutorial_viewedHowToPlayOverlay shown to userfirst_time: boolUser viewed the tutorial overlay
critical_path_undo_tappedUndo button pressed during gameplaypath_length_at_undo: intUser used the undo feature
critical_path_theme_changedTheme picker selection in PlayViewfrom_theme: string, to_theme: stringUser changed puzzle theme
critical_path_stim_streak_milestoneStreak crosses 7, 30, or 100-day thresholdstreak: intUser hit a streak milestone
critical_path_theme_unlockedNew cosmetic theme unlocked via progressiontheme_name: stringUser unlocked a new theme
critical_path_share_card_generatedShare result modal renderedpercentile: intUser tapped share button (card generated)
critical_path_share_card_sharedNative share success via navigator.share()percentile: intUser completed native share flow
critical_path_share_tappedDeprecated (Phase 86a) — Use critical_path_share_card_generated insteadLegacy share initiation event

Notes

  • is_played_today is hardcoded false (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 when isPlayed is true (v-if="!isPlayed" at the component root), so the event cannot fire at all when the user has already played. is_played_today: false is now a constant, not a conditional.
  • puzzle_date is UTC YYYY-MM-DD string, never a timestamp. Maintains PII minimization per brand standards.
  • theme_active property (R5 Phase 86a) — Added to puzzle_started and puzzle_completed to track which theme the user selected for that puzzle session. Enables cohort analysis by theme preference.
  • time_taken_seconds and move_count (R5 Phase 86a) — Replaced elapsed_seconds / node_count on puzzle_completed for better semantic clarity. time_taken_seconds is the actual solve time in seconds; move_count is the number of node taps to complete.
  • stim_xp_earned is now live (R5 Phase 86a) — Populated from the recordCriticalPathPlay mutation response. No longer null.
  • critical_path_theme_changed is now live (R5 Phase 86a) — Fires synchronously before state mutation in onThemeSelect. Captures both from_theme and to_theme strings. 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 after GET_STIM_XP_STATUS refetch.
  • critical_path_theme_unlocked (R5 Phase 86a) — Fires once per newly unlocked cosmetic theme, detected by diffing unlockedThemes pre/post refetch. Enables analysis of theme progression friction.
  • critical_path_share_card_generated and critical_path_share_card_shared (R5 Phase 86a) — Split the legacy critical_path_share_tapped event into two distinct steps: card generation (tap share CTA) and completion (user confirms native share). Only fires on navigator.share() success, not on copy-to-clipboard or cancel. Enables share funnel analysis.
  • critical_path_share_tapped is deprecated (R5 Phase 86a) — Replaced by critical_path_share_card_generated and critical_path_share_card_shared. The old event remains in analytics.ts as dead code and can be removed in a cleanup pass.
  • send_instantly flag on puzzle_abandoned. PostHog's send_instantly: true option 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 /dashboard mid-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:

  1. Open PostHog project > Data Management > Funnels
  2. Create new funnel: "Critical Path Completion"
  3. Configure steps:
    • Step 1: Event = critical_path_puzzle_started (no property filters)
    • Step 2: Event = critical_path_puzzle_completed (no property filters)
  4. Set funnel window: 24 hours (conversion window per step)
  5. 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:

  1. (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)

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:

  1. Open PostHog project > Cohorts > New Cohort

  2. Name: "Critical Path 7-day players"

  3. Cohort type: Behavioral

  4. Filter configuration:

    • Condition 1: critical_path_puzzle_completed events
    • Count: Count of event fires — set to ≥ 7
    • Time frame: Last 14 days
    • Distinct: By event.timestamp grouped 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:

    sql
    SELECT 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
  5. 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

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

sql
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

sql
SELECT avg(JSONExtractFloat(properties, 'time_taken_seconds')) as avg_solve_time_seconds
FROM events
WHERE event = 'critical_path_puzzle_completed'
  AND toDate(timestamp) >= today() - 7

Undo Usage

sql
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() - 7

Abandonment Rate

sql
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() - 7

Implementation Details

Source code: ionic_frontend/src/play/analytics.ts
Components instrumented:

  • ionic_frontend/src/play/PlayView.vue — puzzle lifecycle events
  • ionic_frontend/src/play/HowToPlayOverlay.vue — tutorial event
  • ionic_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

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:

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

typescript
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.

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)

Loading…