Skip to content

Tracking & Analytics

Overview

Treatment plan tracking operates across two storage layers:

  • Core API (Postgres) — clinical truth: session completions, exercise logs, pain levels, form responses, server-computed pose aggregates, video engagement aggregates.
  • Telemetry API + S3 — high-volume time-series ingest path: pose-frame batches and media events from the Patient Portal during a session; per-session replay blobs at s3://restartix-telemetry/{org_id}/{session_id}.bin.gz.

ClickHouse is not in scope at launch — the workload's actual shape (clinic-scoped readers, per-rep aggregates, per-session replay blobs) fits Postgres + S3 comfortably to 50k+ peak concurrent users. CH becomes relevant only at Tier 3 and is reachable via the swap-point interfaces. See ../../telemetry/index.md for the full design and decisions.md → Why telemetry is PG + S3, not ClickHouse for the rationale.

Data Flow

┌─────────────────────────────────────────────────────┐
│              PATIENT PORTAL (browser)                │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐ │
│  │ Session     │  │ Video Events │  │ Pose      │ │
│  │ Completion  │  │ (per-stream) │  │ Landmarks │ │
│  │ (clinical)  │  │              │  │ (1s batches)│
│  └──────┬───────┘  └──────┬───────┘  └─────┬─────┘ │
└─────────┼─────────────────┼─────────────────┼───────┘
          │                 │                 │
          │ POST /v1/...    │ POST /v1/media/ │ POST /v1/pose/
          │ (Core API)      │   events        │   frames
          │                 │ (Telemetry API) │ (Telemetry API)
          ▼                 ▼                 ▼
    ┌───────────┐    ┌──────────────────────────────┐
    │  Core API │    │      Telemetry API            │
    │ Postgres  │    │ (separate Go service)         │
    │           │    │  • S3 multipart in-flight     │
    │  clinical │    │  • server-side aggregation    │
    │  truth    │    │  • S3 replay blob at session  │
    └─────┬─────┘    │    end                        │
          │          └──────────────┬────────────────┘
          │                         │ events.Bus
          │                         ▼
          │                ┌──────────────────────┐
          │                │  Core API subscriber │
          │                │  writes pose_/media_ │
          │                │  metrics + updates   │
          │                │  patient_exercise_logs│
          │                └────────┬─────────────┘
          │                         │
          ▼                         ▼
    ┌──────────────────────────────────────────────┐
    │           CORE API READS (RLS, audit)         │
    │  Patient progress, specialist dashboards,     │
    │  cohort views, replay blob signed URLs        │
    └───────────────────────────────────────────────┘

Core API Tracking (Clinical Data)

What's Stored

TableDataPurpose
patient_treatment_planssessions_completed, sessions_skipped, statusPlan-level progress
patient_session_completionsstarted_at, completed_at, pain levels, difficultySession-level outcomes
patient_exercise_logsprescribed vs actual (sets/reps), video watch %, pose accuracyExercise-level performance
forms (instances)Post-session questionnaire responsesDetailed patient-reported outcomes

Key Queries

Patient progress over time:

sql
SELECT
    psc.session_number,
    psc.completed_at,
    psc.pain_level_before,
    psc.pain_level_after,
    psc.perceived_difficulty,
    psc.exercises_completed,
    psc.exercises_total,
    psc.duration_seconds
FROM patient_session_completions psc
WHERE psc.patient_treatment_plan_id = 42
  AND psc.status = 'completed'
ORDER BY psc.completed_at;

Exercise performance comparison:

sql
SELECT
    e.name AS exercise_name,
    pel.prescribed_sets,
    pel.prescribed_reps,
    pel.actual_sets,
    pel.actual_reps,
    pel.video_watch_percentage,
    pel.pose_accuracy_score,
    pel.skipped
FROM patient_exercise_logs pel
JOIN exercises e ON e.id = pel.exercise_id
WHERE pel.session_completion_id = 789
ORDER BY pel.sort_order;

Adherence rate per week:

sql
SELECT
    EXTRACT(WEEK FROM psc.started_at) AS week_number,
    COUNT(*) FILTER (WHERE psc.status = 'completed') AS completed,
    COUNT(*) FILTER (WHERE psc.status = 'skipped') AS skipped,
    ptp.frequency_per_week AS expected
FROM patient_session_completions psc
JOIN patient_treatment_plans ptp ON ptp.id = psc.patient_treatment_plan_id
WHERE psc.patient_treatment_plan_id = 42
GROUP BY week_number, ptp.frequency_per_week
ORDER BY week_number;

Telemetry API ingest

The Patient Portal sends high-volume time-series data directly to the Telemetry API (separate Go service). Three typed endpoints, narrow scope. Auth on the hot path is a signed session token issued by Core API at exercise-session start — no Clerk JWT verification per pose-frame batch. See ../../telemetry/api.md for the full spec.

Video engagement events — POST /v1/media/events

When a patient watches an exercise video during a treatment plan session, the frontend sends the standard media event lifecycle. See ../../telemetry/media-events.md for the full event specification.

Event flow per exercise video:

Patient starts exercise video
  → session_start (TTFB, load time, connection info)
  → heartbeat every 10s (position, buffering, bitrate, quality, dropped frames)
  → buffering_start/buffering_end (per-stall detail)
  → quality_change (ABR bitrate/resolution switches)
  → milestone (25%, 50%, 75%, 95% watched)
  → session_end (final stats, completion status)

Example — session_start:

json
POST /v1/media/events
{
  "event": "session_start",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "media_id": "hashed-exercise-id",
  "media_type": "video",
  "timestamp": "2026-02-16T09:03:00Z",
  "data": {
    "total_duration_seconds": 42.0,
    "ttfb_ms": 340,
    "video_load_time_ms": 1200,
    "cdn_response_time_ms": 280,
    "connection_type": "wifi",
    "effective_bandwidth": 12.5,
    "rtt_ms": 45
  }
}

Example — heartbeat (every 10s during playback):

json
{
  "event": "heartbeat",
  "session_id": "550e8400-...",
  "timestamp": "2026-02-16T09:03:30Z",
  "data": {
    "position_seconds": 29.5,
    "watched_duration_seconds": 29.5,
    "completion_percent": 70.2,
    "buffering_count": 0,
    "buffering_duration_ms": 0,
    "current_bitrate": 2500000,
    "current_resolution": "720p",
    "dropped_frames": 2,
    "total_frames": 750
  }
}

Server-side aggregation produces (written to PG media_session_metrics + media_buffering_events, both monthly partitioned):

  • Average video watch percentage per exercise (per clinic, per cohort)
  • Video completion rates and milestone reach
  • Quality distribution and ABR switch patterns
  • Buffering frequency and duration

Consent requirement: analytics per-purpose flag must be active for the patient in this clinic.

Pose tracking — POST /v1/pose/frames

Pose tracking uses a dedicated endpoint with binary payload — JSON is forbidden on the wire because it's ~11× wasteful. The frontend buffers MediaPipe landmark frames into 1-second binary float32 batches, gzipped, and posts.

POST /v1/pose/frames
Content-Type: application/octet-stream
Authorization: Bearer <signed-session-token>

[1-byte version][4-byte frame_count][4-byte fps_hint]
[N × 33 × 4 × float32 landmarks]   ← x, y, z, visibility per landmark per frame
[N × 4 byte timestamp_ms]          ← elapsed ms since session_start
[gzip wrapper]

Full envelope in ../../telemetry/api.md.

Server-side aggregation at session_end produces (written to PG pose_session_metrics + pose_rep_metrics + updates patient_exercise_logs.pose_accuracy_score):

  • Per-rep aggregates: rep number, peak ROM, form score, exercise phase breakdown, deviation max
  • Per-session totals: rep count, average form score, total active duration
  • Per-clinic / per-exercise trends, materialized views nightly

Replay is fetched on demand from S3 via Core API (signed URL) — the full landmark stream for that session, decoded and rendered in the Clinic app's replay viewer. Not a queryable data store.

Consent requirement: biometric per-purpose flag must be active. The patient's biometric consent is captured via the form-driven Tier B medical-consent flow (form template seeded by F3.5; ledger row in consents). Withdrawal is immediate — Telemetry API rejects the next batch.

Session finalizer — POST /v1/sessions/{id}/end

When the patient completes (or abandons) a session, the Patient Portal posts to the finalizer. Telemetry API:

  1. Computes per-rep and per-session aggregates from the accumulated landmark buffer.
  2. Finalizes the S3 replay blob at s3://restartix-telemetry/{org_id}/{session_id}.bin.gz.
  3. Publishes an events.Bus event with the aggregates.
  4. Core API subscriber writes to PG and updates patient_exercise_logs.

If session_end never arrives (browser closed, device lost), a server-side timeout (10 min silence) finalizes the session as incomplete with whatever was received. Specialist sees the incomplete flag.

This finalizer replaces any "session_started / session_completed" tracking event — the clinical aggregate IS the source of truth, written by the events.Bus subscriber. There's no separate /v1/analytics/track endpoint; that surface is rejected.

Analytics Dashboards

Patient Dashboard (Patient View)

Available via GET /v1/patient-treatment-plans/{id}/progress:

┌────────────────────────────────────────────────┐
│  My Progress                                   │
│                                                │
│  Sessions: 6/12 (50%)                          │
│  ████████████░░░░░░░░░░░░                      │
│                                                │
│  Pain Trend:                                   │
│  8 ┤                                           │
│  6 ┤  ●──●                                     │
│  4 ┤      ╲──●──●                              │
│  2 ┤            ╲──●──●                        │
│  0 ┤──────────────────────                     │
│    S1  S2  S3  S4  S5  S6                      │
│                                                │
│  This Week: 2/3 sessions done                  │
│  Streak: 3 days 🔥                             │
└────────────────────────────────────────────────┘

Specialist Dashboard (Clinical View)

Available via GET /v1/patient-treatment-plans/{id}/analytics:

┌────────────────────────────────────────────────┐
│  Patient: Jane Doe                             │
│  Plan: Post-ACL Rehab Weeks 1-4               │
│                                                │
│  Adherence: 85% (6/7 expected sessions)        │
│  Avg Session Duration: 28 min                  │
│  Avg Video Watch: 88%                          │
│                                                │
│  Exercise Performance:                         │
│  ┌────────────────────┬─────┬──────┬──────┐   │
│  │ Exercise           │ Done│ Skip │Video │   │
│  ├────────────────────┼─────┼──────┼──────┤   │
│  │ Quad Set           │ 6/6 │ 0    │ 92%  │   │
│  │ Heel Slides        │ 6/6 │ 0    │ 88%  │   │
│  │ Straight Leg Raise │ 5/6 │ 1    │ 78%  │   │
│  │ Prone Hang         │ 6/6 │ 0    │ 85%  │   │
│  └────────────────────┴─────┴──────┴──────┘   │
│                                                │
│  Pain Evolution:                               │
│  Before: 7 → 6 → 6 → 5 → 5 → 4 (↓ 43%)     │
│  After:  5 → 4 → 4 → 3 → 3 → 3 (↓ 40%)     │
│                                                │
│  Weekly Adherence:                             │
│  Week 1: ███████████████ 100% (3/3)           │
│  Week 2: ██████████░░░░░  67% (2/3)           │
└────────────────────────────────────────────────┘

Organization Dashboard

Organization-level aggregates (for the stats described in needs-on-day-1.md):

  • Pain reduction: Average pain improvement across all completed plans
  • Adherence rates: Session completion rates across all active plans
  • Top exercises: Most prescribed, best/worst performance
  • Revenue per plan: Treatment plan → service plan correlation
  • Plan completion rates: How many plans reach "completed" vs "expired" or "cancelled"

Data retention

DataStorageRetention
Patient session completionsPostgres7 years (clinical)
Patient exercise logs (with pose / video aggregates)Postgres7 years (clinical)
Post-session form responsesPostgres7 years (clinical)
pose_session_metrics, pose_rep_metrics (per-rep aggregates)Postgres, partitioned monthly7 years (clinical)
media_session_metrics, media_buffering_eventsPostgres, partitioned monthly2 years
Pose replay blobsS3 lifecycled (Standard → IA at 90d → Glacier at 1y → expire)6 months default; clinically-flagged sessions retained per clinical rules

Privacy & compliance

GDPR / HIPAA

  • All clinical data and aggregates live in Postgres with RLS, audit, classification, and column encryption per P12.
  • Audit trail via Core API audit_log covers every authenticated mutation (session start/complete/skip, plan assignment, form submission). Telemetry's high-volume ingest path is not the audit channel — see ../../reference/telemetry.md.
  • Replay blobs in S3 are encrypted at rest (KMS) and access-controlled; reads only via short-lived signed URLs minted by Core API.
  • Patient principal IDs are stored plain in PG aggregates and S3 paths (no pseudonymization) because all readers are clinic-scoped — the patient's own clinic, not cross-tenant. Cross-tenant Console reads use anonymised aggregates only (processor rule).
  • Pose tracking requires explicit biometric per-purpose consent from the patient (named flag in the foundation per-purpose consent ledger, 1B.9).
  • The biometric consent is captured via a form-driven Tier B medical consent (template seeded with F3.5).
  • No pose data is captured or accepted until the flag is active.
  • Consent is revocable at any time. Telemetry API rejects the next batch with 403 when the flag is withdrawn.