Setting Up Real User Monitoring (RUM) for Mobile Apps
Synthetic monitoring runs scripts in controlled environments. RUM captures data on what actually happens with real users on real devices in real networks. The difference is crucial: synthetic testing won't show that Xiaomi Redmi Note 9 users experience list slowdown due to RecyclerView overdraw, or that LTE users in a region face network timeouts at the 8-second mark.
What RUM Tracks
Standard mobile RUM events include:
- View — screen transition (open, close, duration)
- Action — tap, swipe, scroll, long press
- Resource — HTTP request: URL, method, status, size, latency
- Error — handled exception, unhandled exception, ANR, crash
- Long Task — operation on main thread > 100ms (Android) / main runloop > 16ms (iOS)
Of all events, the most diagnostically valuable is Long Task correlated with View. If a 250ms Long Task occurs when opening the checkout screen and reproduces in 15% of users — that's a concrete performance bug, not "something sometimes lags."
Choosing a Tool
| Tool | Highlights | Sampling |
|---|---|---|
| Datadog RUM | Complete mobile+server stack, W3C tracing | Up to 100%, flexible |
| Sentry | Free tier, good release comparisons | 100% by default |
| Firebase Performance | Free, Google ecosystem only | Automatic |
| New Relic Mobile | Powerful NRQL, enterprise | Up to 100% |
For most products, Firebase Performance is a good start (free, zero-config for networking). But once you need server trace correlation or custom business attributes, move to Datadog or Sentry.
Setup with Datadog RUM
import DatadogRUM
RUM.enable(with: RUM.Configuration(
applicationID: "your-rum-app-id",
sessionSampleRate: 80, // 80% of sessions
telemetrySampleRate: 20,
trackBackgroundEvents: false // don't track background events
))
The sessionSampleRate parameter is the first thing to tune for your traffic. 100% sampling at a million DAU is expensive. Standard practice: 100% for new releases the first 48 hours (catch regressions), then drop to 20–30%.
Manual Screen Instrumentation (SwiftUI)
struct ProductListView: View {
var body: some View {
List(products) { product in
ProductRow(product: product)
}
.trackRUMView(name: "ProductList")
}
}
Monitoring Specific Network Requests
// With URLSession + Datadog
let delegate = DDURLSessionDelegate()
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
Datadog automatically injects x-datadog-trace-id into each request through this session — no additional interceptors needed.
Common Issue: View Duration Distortion
A frequent RUM setup bug is Views closing too early. For example, in UIKit without explicit stopView calls, the agent closes the View on viewWillDisappear, but if the controller shows a bottom sheet on top of itself — the View closes and reopens, creating duplicate records. Solution:
// iOS — explicit View lifecycle management
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
RUM.monitor?.startView(viewController: self, name: "ProductDetail")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
RUM.monitor?.stopView(viewController: self)
}
}
Session Sampling and GDPR
With RUM enabled, get user consent before recording sessions. On iOS, use trackingConsent:
Datadog.set(trackingConsent: userConsentedToAnalytics ? .granted : .notGranted)
For Sentry, similarly configure SentryOptions.enableAutoSessionTracking and SentrySDK.setUser() only after consent.
What We Do
- Choose a tool fitting your stack and budget (Firebase / Sentry / Datadog / New Relic)
- Connect the SDK with appropriate
sessionSampleRatefor your traffic - Instrument View transitions (UIKit, SwiftUI, Jetpack Compose, Flutter)
- Configure HTTP interceptors for network resource tracking
- Set up consent logic for GDPR
- Build initial dashboard: p75/p95 View load time, Error Rate, Long Task Rate
Timeline
Basic RUM setup: 1–2 days. Custom attributes and dashboards: another 1 day. Pricing is calculated individually.







