ReadThat 7: Observability
A vendor-neutral KMP telemetry contract, fixed timer boundaries, percentile SLOs, and the hierarchy that connects them: which performance metrics are levers for DAU and engagement, and which ones matter most in international markets.
Part 7 of the ReadThat case study.
📲 Try it live: Download the ReadThat APK (8.8 MB, Android 8+). Open the file on your phone and allow “install unknown apps” if prompted.
The founding rule of the measurement contract: a number without its boundary and segment defined is not the same metric. Everything below fixes boundaries first, targets second. Full contract: docs/PERFORMANCE_OBSERVABILITY.md.
The pipeline
Android features / iOS adapter / browser adapter | vvendor-neutral PerformanceEvent (KMP contract, :core:observability) | L1 bounded queue -> L2 durable Room outbox |POST /v1/telemetry/performance (max 50 events / 64 KiB) |Worker: strict allowlist + per-IP limit |Analytics Engine distributions ├── Workers Logs (accepted-batch audit) └── Workers Traces (sampled backend dependency latency):core:observability compiles for Android, iOS, and browser JS, and deliberately exposes a small OpenTelemetry-shaped contract instead of importing the still-experimental OTel KMP SDK; moving to OTLP later touches the exporter, not the feature modules. PerformanceTelemetry.kt
The Worker enforces the schema as a hard allowlist: every metric name and dimension is enumerated; anything else is rejected at ingest:
const metricName = z.enum([ "home_tti", "feed_initial_fetch", "feed-load-success", "feed-load-fail", "comments_tti", "comments_initial_fetch", "comments_full_fetch", "media_feed_tti", "mutation_local_commit", "mutation_server_ack", "interaction_to_next_frame", "screen_frame_summary", "network_request", "video_time_to_first_frame", "video_rebuffer", "sdui_dropped_cell", ...]);Privacy is structural, not policy: no titles, bodies, URLs, or tokens pass the schema; user and content IDs are HMAC-peppered into irreversible pseudonyms server-side; network events record a route template, never a URL. Product analytics (sessions, impressions with a 600 ms dwell gate, playback time) is a separate dataset with the same posture (docs/PRODUCT_ANALYTICS.md).
Timer boundaries and key SLOs
Boundaries follow Reddit’s published vocabulary so numbers are comparable to their posts. Selected budgets (p50/p90/p99, evaluated per platform × start type × cache tier × network before any rollup):
| Metric | Boundary | p50 | p90 |
|---|---|---|---|
| Home TTI (warm, Room) | app init → first non-placeholder feed unit | 100 ms | 250 ms |
| Home TTI (cold, authed) | same, cold process | 600 ms | 1,200 ms |
| Comments TTI (prefetched) | feed tap → first real comment frame | 300 ms | 1,000 ms |
| MediaFeed TTI | media tap → exact seeded/Room item | 100 ms | 250 ms |
| Optimistic local commit | intent → visible L1/Room state | 8 ms | 16 ms |
| Server ACK (vote/text post) | same mutation → authoritative response | 300 ms | 800 ms |
| Video first frame | prepare/switch → first rendered frame | 500 ms | 1,000 ms |
| Touch → next frame | pointer action → next Compose frame | 50 ms | 100 ms |
Frame health is measured per surface via JankStats (screen_frame_summary after 300 frames: target jank < 1%, zero frozen frames). The local-commit / server-ACK split is the most diagnostic pair in the table: a slow local commit is a client storage bug; a slow ACK with a fast commit means the UX held while network/edge/database needs work.
Which metrics are levers for DAU and engagement
Perf metrics aren’t goals; they’re levers on top-line metrics. The causal chain this contract is built around:
latency/reliability metric → behavior metric → top lineComments TTI → comments viewed/posted → session length, retentionfeed-load failure rate → posts viewed → DAUvideo first-frame + rebuffer→ watch time → time spenttouch→frame, jank → scroll depth → posts viewedReddit’s published sensitivities make the chain concrete: their Instant Comment Loading work moved p90 Comments TTI −60% and measured +4% comments viewed; their instrumentation post ties a 0.15% feed-load error rate to roughly 5% fewer posts viewed. Error rates punch far above their percentage; a failed load ends a session in a way a slow load doesn’t. That’s why the release gate blocks on failure ≥ 0.10% but only on a >10% p90 TTI regression.
The levers, ranked by expected top-line torque:
- Feed-load failure rate: the cheapest DAU lever in the system.
- Comments TTI: directly gated conversion into the highest-value engagement act.
- Cold Home TTI: the first-session/notification-open experience; retention lever.
- Video first frame + rebuffer ratio: time-spent lever; rebuffer > 1% correlates with abandons.
- Jank/frozen frames: a slow diffuse lever, but frozen frames and ANRs are also a Play-Store-visibility lever (bad-behavior threshold 0.47%).
Diagnostic metrics (payload bytes, edge latency via Server-Timing, cache-tier splits, sdui_dropped_cell) exist to explain lever movement, not to be goaled.
What changes for international markets
The same contract, different weightings; this is the measurement half of part 8:
- Segment or lie. Mid-range Android with 3–4 GB RAM dominates growth markets. A global p90 hides that the p90 in market is the p50 experience there. Every SLO panel splits by device class, network type, and country before rollup.
- Different levers dominate. On congested or metered networks: payload size (
feed_query_response_size), rebuffer ratio, and offline correctness (outbox drain success, queued-mutation age) become primary. The local-commit metric matters more than ACK; it’s the metric of “the app worked on the bus.” - Device capability mix moves frame health and cold TTI from tier-3 to tier-1: decode time for images, jank on low-RAM devices under memory pressure (
trimMemorybehavior is instrumented for a reason). - Carrier/network reality: HTTP/3 adoption per origin per country is tracked because QUIC is throttled or broken on some carriers; a silent h3→h2 collapse is a halt-and-inspect trigger in the runbook.
- Cultural/product differences show up in product analytics, not perf: dwell-gated impressions, comments-per-session, and media-vs-text mix differ by market and should re-weight which surfaces get the optimization budget.
Release gates
Rollout blocks on: >10% p90 Home/Comments TTI regression, feed-load failure ≥ 0.10%, any repeatable frozen frame, a lost optimistic rollback, or telemetry schema rejection from a production build. Percentiles publish with sample counts; p99 doesn’t alert until the population is meaningful. And a device launch stays in the gate even when all suites are green: an emulator run caught a JankStats initialization-order defect no unit test could.
📷 Screenshot placeholder: Analytics Engine query results for comments_tti split by cache_tier.
← Part 6: Backend architecture · Next: Part 8: International strategy →