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
| Table | Data | Purpose |
|---|---|---|
patient_treatment_plans | sessions_completed, sessions_skipped, status | Plan-level progress |
patient_session_completions | started_at, completed_at, pain levels, difficulty | Session-level outcomes |
patient_exercise_logs | prescribed vs actual (sets/reps), video watch %, pose accuracy | Exercise-level performance |
forms (instances) | Post-session questionnaire responses | Detailed patient-reported outcomes |
Key Queries
Patient progress over time:
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:
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:
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:
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):
{
"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:
- Computes per-rep and per-session aggregates from the accumulated landmark buffer.
- Finalizes the S3 replay blob at
s3://restartix-telemetry/{org_id}/{session_id}.bin.gz. - Publishes an
events.Busevent with the aggregates. - 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
| Data | Storage | Retention |
|---|---|---|
| Patient session completions | Postgres | 7 years (clinical) |
| Patient exercise logs (with pose / video aggregates) | Postgres | 7 years (clinical) |
| Post-session form responses | Postgres | 7 years (clinical) |
pose_session_metrics, pose_rep_metrics (per-rep aggregates) | Postgres, partitioned monthly | 7 years (clinical) |
media_session_metrics, media_buffering_events | Postgres, partitioned monthly | 2 years |
| Pose replay blobs | S3 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_logcovers 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).
Biometric consent
- Pose tracking requires explicit
biometricper-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.