Implementing Session Replay (User Session Recording) for Mobile Apps
Session Replay on mobile is technically harder than on web. A browser always renders DOM that can be serialized. A mobile app renders native UI via UIKit, Jetpack Compose, or Flutter engine, and capturing this state requires either screenshots or serializing the View-hierarchy wireframe.
Poor Session Replay implementation consumes 10–20% extra CPU and 100–200KB network traffic per minute. Good implementation is invisible to users.
Two Recording Approaches
Screenshot-based (UXCam, Smartlook, some Datadog modes): takes screenshots at intervals (usually 1–5 fps), masks sensitive areas, sends to server. Captures visual state precisely, including custom Views and WebView. Downside: large data volume, complex dynamic content masking.
Wire-frame / View-tree based (Sentry, some Datadog modes): serializes View hierarchy with positions, colors, text, replays UI server-side via templates. Lower traffic and CPU, but WebView and custom graphics aren't reproduced accurately.
Implementation via Sentry Session Replay
Sentry Session Replay for mobile is available from SDK 8.x. Uses wireframe approach:
// iOS
import Sentry
SentrySDK.start { options in
options.dsn = "https://[email protected]/project"
options.experimental.sessionReplay = SentryReplayOptions(
sessionSampleRate: 0.1, // 10% random sessions
onErrorSampleRate: 1.0 // 100% sessions with crash/error
)
}
// Android
SentryAndroid.init(this) { options ->
options.dsn = "https://[email protected]/project"
options.experimental.sessionReplay.apply {
sessionSampleRate = 0.1
onErrorSampleRate = 1.0
}
}
onErrorSampleRate = 1.0 is critical. It means: record replay for all sessions with errors. SDK buffers last N seconds of replay in memory and sends them with error report. So you see exactly what the user did before the crash.
Masking Sensitive Data
By default, Sentry masks UITextField and fields with isSecureTextEntry = true. Not enough — also hide card data, PII, and OTP fields.
// iOS — mark Views for masking
class PaymentCardView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
SentrySDK.replay.maskView(self)
}
}
// Android — mask via tag or annotation
val cardNumberField = findViewById<EditText>(R.id.cardNumber)
cardNumberField.setTag(io.sentry.android.replay.Recorder.MASK_TAG, true)
For SwiftUI and Jetpack Compose:
// SwiftUI
Text(userEmail)
.sentryReplayMask()
// Compose
Text(
text = cardNumber,
modifier = Modifier.sentryReplayMask()
)
Integration with Crash Reports
The most valuable feature — linking from a crash report to session recording. In Sentry this works automatically: if an active replay buffer existed at error time, the Issue shows "Watch Session Replay" button.
This changes debugging: instead of reproducing from user description, you watch the recording and see exactly what happened.
Datadog Session Replay Setup
Datadog supports two modes: wireframe (default) and screenshot. Screenshot enabled explicitly:
import DatadogSessionReplay
SessionReplay.enable(
with: SessionReplay.Configuration(
replaySampleRate: 20, // 20% of sessions
defaultPrivacyLevel: .maskUserInput
)
)
Datadog Session Replay links to RUM View — in the UI you can open a specific View and watch replay for that screen with metrics (latency, FPS) on the timeline.
Performance and Overhead
Testing Sentry wireframe mode on production devices shows:
| Metric | Overhead |
|---|---|
| CPU (background sampling) | < 1% |
| Memory (60 sec buffer) | ~8–15 MB |
| Network traffic (on send) | ~50–150 KB/min |
| View-tree serialization | < 2ms per frame |
Screenshot-based modes are costlier: 3–8% CPU, 200–500 KB/min traffic at 2 fps.
What We Do
- Choose tool and recording mode based on security and performance requirements
- Connect SDK with
onErrorSampleRate = 1.0to record error sessions - Configure masking for all screens and elements with PII
- Verify masking via Privacy Audit in Sentry/Datadog UI
- Set up replay → crash report linking for quick diagnostics
Timeline
Basic setup with masking: 2–3 days. Full integration with privacy audit: 4–5 days. Pricing is calculated individually.







