Analytics & Crash Reporting
Status
Accepted (2026-06-15). Client crash/error reporting ships as a Sentry SDK tunneled
through our own server — the SDK posts to our server (CF-fronted, authed where a session
exists, IP/instance rate-limited, size-capped, PII-scrubbed), which forwards to a
destination delivered per-deployment via the InstanceInfo handshake (like web_origin).
T.REX SaaS forwards to Sentry Cloud (crash-only, scrubbed); white-label forwards to a
first-party Loki/Grafana sink (or the customer’s own Sentry, or off). Launch-gating —
required before the 70k production cutover. Implementation is phased; see the work item
client-crash-telemetry. The resolutions to this doc’s prior open questions are captured
under Decision below; the comparison that led here is preserved in the sections
that follow.
Context
The legacy Flutter app shipped a heavy telemetry stack (confirmed in tmp/range_day/pubspec.yaml): Firebase analytics, crashlytics, messaging, performance, core, plus Mixpanel. The rebuild deliberately drops these — they were a large share of the legacy iOS app’s ~75 MB, and they put third-party tracking SDKs (Google, Mixpanel) inside a privacy-sensitive firearms-training app. For our audience, not shipping those SDKs is arguably a feature, not a regression. See Legacy App Analysis.
This doc separates “data we already have, just unsurfaced” from “capabilities genuinely gone.”
Clarifies older wording: Legacy App Analysis says Firebase Analytics is “replaced by the observability stack.” More precisely: domain KPIs come from the app DB, server observability (Loki/Grafana/traces) covers ops, and client crash/error reporting uses the first-party
TelemetryServiceRPC (see Observability) — not the ops-observability pipeline. This doc is the current reference for that split.
What those tools did → where we stand
| Capability | Legacy | Us today |
|---|---|---|
| Push delivery | firebase_messaging (FCM) | ✅ Replaced by our push service (APNs + platform push; Android still rides FCM as transport, without the analytics SDKs) |
| Domain/product metrics (signups, scores, drills run, event attendance, challenges, retention, feature usage) | Firebase/Mixpanel events | ✅ In our Postgres DB, with timestamps — more accurate than client event-tracking ever was. Unsurfaced (no dashboard yet). |
| Client behavioral analytics (screen flows, taps, funnels, “viewed but didn’t act,” session length) | Mixpanel + Firebase Analytics | ❌ Gap — the server only sees API calls, not pure-client interactions |
| Crash reporting | firebase_crashlytics | ❌ Gap — our observability is server-side; it never sees SwiftUI/Compose/WASM client crashes |
| Client performance (app-start, render, ANR, client-perceived latency) | firebase_performance | ❌ Gap — we measure server latency only |
| Acquisition / attribution, device & geo cohorts | Firebase | ❌ minor gap — server can infer some (UA, IP→geo) but not install source/UTM |
Key reframe
The product KPIs that matter for a training app — DAU/MAU, scores submitted, drills completed, retention, feature adoption — live in our own DB, exactly, not as noisy client-side proxies. So that’s “data we have, no dashboard yet,” not lost data. Our observability stack (Loki / Grafana / tracing) covers server health; product KPIs come from SQL on the app DB. This is already anticipated by the Post-MVP Admin analytics dashboard roadmap item.
Genuine losses (the part needing a decision)
- Client crash reporting — the biggest real gap. Nothing today captures a native/WASM client crash.
- Pure client-side funnels — e.g. “opened the QR scanner but never completed a scan.” Only client instrumentation sees this.
- Client perf + acquisition/attribution — lower priority.
Options (all keep data in-house — no Google/Mixpanel SDKs)
Client crash + error/perf reporting (gaps #1, #3)
- Baseline — first-party
TelemetryServiceRPC (already planned). Observability already specifies a ConnectRPCTelemetryService(ReportError/ReportEvent) plus native crash capture (PLCrashReporter on iOS, an uncaught-exception handler on Android, a panic hook for WASM). Reports flow into our own infra — no third-party SDK. This is the production default and the minimal viable crash reporting. - Sentry — chosen (see Decision). More out of the box (symbolication, crash grouping, release health, client perf, ready-made dashboards) for less build effort, at the cost of a dependency. Reached via the server-side authed tunnel (not a client-embedded DSN); Sentry Cloud for the T.REX SaaS (crash-only, PII-scrubbed, no session replay, token/header redaction, privacy-policy coverage), self-hosted rejected on ops cost, white-label → first-party Loki sink (or own Sentry, or off). Per Observability, the client reporting interface stays the same whether the server forwards to our Loki sink or Sentry — so this is not a one-way door.
Behavioral funnels (gap #2) — optional, not planned
- self-hosted PostHog — events, funnels, cohorts, retention. Only if product genuinely needs drop-off analysis the DB can’t answer. Documented as a possible later option, not a roadmap commitment.
Domain KPIs (we already have the data)
- Admin analytics dashboard on Postgres (existing Post-MVP item) — highest-value, lowest-effort, no new third-party tooling.
Recommendation (per architect review)
- Admin analytics dashboard on our DB — domain KPIs, zero new deps.
- Before broad production cutover (launch-gating): client crash/error reporting must be live — the tunneled Sentry SDK with per-deployment destination via the handshake (T.REX → Cloud, white-label → Loki); see Decision and client-crash-telemetry. Flying blind on native/PWA crashes during the legacy-replacement launch is not an acceptable trade.
- Deferred indefinitely: behavioral funnels / self-hosted PostHog — not a roadmap commitment (DB-derived KPIs suffice).
Implementation note: the
TelemetryService+ per-platform crash capture (PLCrashReporter/iOS, uncaught-exception handler/Android, panic hook/WASM) is direction only here. When scheduled, it becomes a dedicated mobile/client-telemetry work item with explicit acceptance criteria, not an inline task.
Decision
Resolved 2026-06-15 (architect review) — the three questions this doc opened with:
Q1 — minimal first-party TelemetryService vs Sentry? → Sentry, reached server-side
via an authed tunnel — not a client-embedded DSN. Clients run the Sentry SDK but post to
our own server (CF-fronted; authed where a session exists; IP/instance rate-limited;
size-capped; PII-scrubbed). The real DSN/destination is held server-side and delivered
per-deployment through the InstanceInfo handshake (same mechanism as web_origin).
Rationale: a client-embedded DSN is public by design, so forgery can’t be prevented;
the authed tunnel is what bounds abuse and — with Cloud — also caps billable events. This
sharpens the “baseline = minimal first-party TelemetryService” framing below: the
first-party path doesn’t disappear, it survives as the white-label Loki sink, with
Sentry/Cloud as the SaaS forward target.
Q2 — when is Sentry Cloud acceptable? → Cloud is for the T.REX SaaS only. Self-hosted Sentry is rejected on hardware/ops cost (4 vCPU / 16 GB + swap minimum, painful upgrades, no SLA). Cloud is permitted crash-only and only with: PII scrubbing, token/header redaction, no session replay, and privacy-policy coverage. White-label never uses Cloud (privacy / air-gap) — it forwards to a first-party Loki/Grafana sink, the customer’s own Sentry, or off.
Q3 — behavioral funnels / PostHog for the MVP? → No. No behavioral funnels for MVP; DB-derived KPIs (the Post-MVP Admin analytics dashboard item) suffice. PostHog is deferred indefinitely — not a roadmap commitment.
Implementation is phased and tracked in client-crash-telemetry.
Related
- Legacy App Analysis
- Observability
- Roadmap: Admin analytics dashboard (Post-MVP) — see Roadmap — Pro & New Features