| name | mobile-metrics |
| description | Define and capture the core mobile RUM SLIs (startup time, jank, ANR, crash, network error rate, battery) with Firebase, Sentry, Datadog, or OTel. Use when setting up client-side performance metrics. |
Mobile Metrics and Core SLIs
Instructions
Mobile RUM is the metrics tier of observability. Agree on a small set of SLIs, capture them uniformly across platforms, and attribute them to release + device class.
1. The Core SLI Set
| SLI | Definition | Default SLO |
|---|
| Crash-free sessions | sessions without a fatal crash / total sessions | >= 99.5% |
| Crash-free users | users without a crash / DAU | >= 99.8% |
| ANR rate (Android) | sessions with an ANR / total sessions | <= 0.1% (Play cutoff 0.47%) |
| App hang rate (iOS) | sessions with a hang > 250 ms / total sessions | <= 0.5% |
| Cold start p95 | time from process start to first-draw on a qualifying cold start | <= 2.5 s Android, 1.8 s iOS |
| Warm start p95 | time from resume to first-draw | <= 800 ms |
| Frozen frame rate | frames > 700 ms / total frames | <= 0.1% |
| Slow frame rate | frames > 16 ms / total frames (60 Hz baseline) | <= 5% |
| Network error rate | 5xx + transport errors / total requests on critical endpoints | <= 1% |
| Battery delta per hour fg | mean battery % drop per foreground hour | <= 10% |
Every SLI must be sliceable by release, os_version, device_class (low/mid/high), and country.
2. Source of Truth per SLI
- Crash-free / ANR / hang: Crashlytics or Sentry Release Health. Do not reimplement; consume the vendor metric.
- Cold / warm start: Firebase Performance
_app_start trace, Sentry app.start.cold / app.start.warm transactions, Datadog RUM application_start_time. All three are auto-instrumented.
- Frozen / slow frames: Android
JankStats library, iOS MetricKit MXAppLaunchMetric / MXCPUMetric, Sentry slow_frames and frozen_frames on transactions.
- Network error rate: HTTP interceptor counter grouped by
endpoint_group (not raw URL -- cardinality).
- Battery:
BatteryManager on Android, UIDevice.batteryLevel on iOS, sampled once per foreground minute.
3. Android -- JankStats
class JankReporter(activity: Activity, private val meter: Meter) {
private val stats: JankStats = JankStats.createAndTrack(activity.window) { frame ->
meter.histogramBuilder("mobile.frame.duration")
.setUnit("ms")
.build()
.record(frame.frameDurationUiNanos / 1_000_000.0,
Attributes.of(
AttributeKey.stringKey("screen"), activity.javaClass.simpleName,
AttributeKey.booleanKey("is_jank"), frame.isJank
))
}
fun setTrackingEnabled(enabled: Boolean) { stats.isTrackingEnabled = enabled }
}
4. iOS -- MetricKit
final class MetricsReceiver: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
if let launch = p.applicationLaunchMetrics {
AppLog.event(logger: "metrics.launch", msg: "launch_metric",
attrs: ["cold_p95_ms": launch.histogrammedTimeToFirstDraw
.bucketEnumerator.allObjects.last as? NSNumber ?? 0])
}
if let hangs = p.applicationResponsivenessMetrics {
AppLog.event(logger: "metrics.hang", msg: "hang_metric",
attrs: ["hang_time_ms": hangs.histogrammedApplicationHangTime])
}
}
}
}
MetricKit delivers once per day on device; do not poll it more often.
5. Flutter -- Firebase Performance
final trace = FirebasePerformance.instance.newTrace('home_feed_load');
await trace.start();
try {
await feedRepo.load();
trace.putAttribute('cache', 'miss');
} finally {
await trace.stop();
}
Automatic traces: _app_start, _app_in_background, _app_in_foreground, plus auto HTTP traces via the firebase_performance HTTP client wrapper. Do not double-instrument.
6. React Native -- Sentry RUM
import { Performance } from '@sentry/react-native';
const t = Sentry.startTransaction({ name: 'home_feed_load', op: 'ui.load' });
try {
await feedRepo.load();
} finally {
t.finish();
}
Hook ReactNavigationInstrumentation into Sentry so every screen transition is a transaction with frame metrics.
7. Cardinality Discipline
Metrics are cheap only when labels are bounded. For every metric:
- Enumerate label values before emitting (
device_class in {low, mid, high}, not raw device model).
- Never use
user_id, session_id, request_id, or raw URLs as a label.
- Bucket
os_version to major (16, 15, 14), not 16.4.1.
- Cap
screen label values to the known screen registry; unknown screens go in screen="other".
8. Dashboards
A single "Mobile Release Health" dashboard, per release, with these tiles:
- Crash-free sessions & users (trend, 14 days).
- ANR / hang rate by OS version.
- Cold start p50/p75/p95 by device class.
- Frozen frame rate by screen.
- Network error rate by endpoint group.
- Adoption % of current release.
9. Alerts
- SLO burn-rate alert per SLI (see
metric-alerts and alerting-sla).
- Release regression alert: any core SLI worse by > 15% vs last released version on the same device class.
- Anomaly alert on sudden drop in session count (could indicate a bootstrap crash hiding the crash signal).
Checklist