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 TelemetryService RPC (see Observability) — not the ops-observability pipeline. This doc is the current reference for that split.

What those tools did → where we stand

CapabilityLegacyUs today
Push deliveryfirebase_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 eventsIn 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 AnalyticsGap — the server only sees API calls, not pure-client interactions
Crash reportingfirebase_crashlyticsGap — our observability is server-side; it never sees SwiftUI/Compose/WASM client crashes
Client performance (app-start, render, ANR, client-perceived latency)firebase_performanceGap — we measure server latency only
Acquisition / attribution, device & geo cohortsFirebase❌ 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)

  1. Client crash reporting — the biggest real gap. Nothing today captures a native/WASM client crash.
  2. Pure client-side funnels — e.g. “opened the QR scanner but never completed a scan.” Only client instrumentation sees this.
  3. 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 TelemetryService RPC (already planned). Observability already specifies a ConnectRPC TelemetryService (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)

  1. Admin analytics dashboard on our DB — domain KPIs, zero new deps.
  2. 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.
  3. 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.