Android RUM Integration
This guide walks through adding OpenObserve Real User Monitoring to a native Android app end to end: adding the Gradle dependencies, initializing the SDK at startup, turning on automatic instrumentation for screens, taps, and OkHttp network calls, reporting errors and crashes, and controlling privacy and data volume. Everything is Kotlin, targets the standard Android build system, and sends to the same OpenObserve instance you already use for logs, metrics, and traces.
New to mobile RUM in general? Start with the Mobile RUM Overview for the concepts — sessions, views, actions, resources, errors — that this guide assumes.
!!! warning "Alpha status"
The Android SDK is currently published as `0.1.0-alpha5`. It is ready to integrate and evaluate — pin the exact version and test upgrades, since some configuration details may change before the stable release.What you get
Once integrated, OpenObserve RUM automatically captures:
- Screens (views) — each
Activity,Fragment, or Jetpack Navigation destination, with load and render timing, via a view-tracking strategy. - User actions — taps, scrolls, and swipes, named from resource ids or accessibility labels.
- Network resources —
OkHttprequests with timing, status, and size, plus distributed tracing to your first-party backends. - Errors — handled and unhandled JVM exceptions with stack traces.
- Crashes — JVM crashes by default, plus native NDK crashes when you enable them.
- Mobile vitals — long tasks, non-fatal ANRs, and frustration signals like rage taps.
Prerequisites
- An Android app project with:
- minSdk 23 or higher (some auto-instrumentation requires API 29).
- compileSdk 36.
- Kotlin 2.0.21 and Java 11 source/target compatibility.
- An OpenObserve instance — OpenObserve Cloud or self-hosted. For local testing, one container is enough:
docker run -d --name openobserve \
-p 5080:5080 \
-e ZO_ROOT_USER_EMAIL="root@example.com" \
-e ZO_ROOT_USER_PASSWORD="Complexpass#123" \
public.ecr.aws/zinclabs/openobserve:latest- Your client token, RUM application id, and ingestion endpoint from Data → Data Sources → Real User Monitoring in the OpenObserve UI.
Step 1 — Add the Gradle dependencies
The SDK is published to Maven Central under the ai.openobserve group. Add the core and RUM artifacts to your app module's build.gradle, plus any optional artifacts for the features you want:
// app/build.gradle
dependencies {
def openobserveVersion = "0.1.0-alpha5"
// Required
implementation "ai.openobserve:o2-sdk-android-core:$openobserveVersion"
implementation "ai.openobserve:o2-sdk-android-rum:$openobserveVersion"
// Optional — add what you need
implementation "ai.openobserve:o2-sdk-android-okhttp:$openobserveVersion" // network tracking
implementation "ai.openobserve:o2-sdk-android-ndk:$openobserveVersion" // native crash reporting
implementation "ai.openobserve:o2-sdk-android-logs:$openobserveVersion" // logs
implementation "ai.openobserve:o2-sdk-android-trace:$openobserveVersion" // tracing
implementation "ai.openobserve:o2-sdk-android-session-replay:$openobserveVersion" // Session Replay
}Make sure mavenCentral() is in your repositories (in settings.gradle's dependencyResolutionManagement block on modern Gradle, or the top-level build.gradle on older setups). If you use a version catalog (gradle/libs.versions.toml), declare the version once and reference the artifacts with libs. accessors instead — pin all o2-sdk-android-* artifacts to the same version so core, RUM, and the optional modules stay in lockstep.
Confirm your module sets Java 11 compatibility:
android {
compileSdk 36
defaultConfig { minSdk 23 }
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
}Step 2 — Initialize OpenObserve RUM
Initialize the SDK once, as early as possible — in your Application.onCreate(). You build a core Configuration, call OpenObserve.initialize, then build a RumConfiguration and call Rum.enable. Keep your client token and application id out of source control (for example in BuildConfig fields fed from Gradle properties).
// OpenObserveApp.kt
import android.app.Application
import android.util.Log
import com.openobserve.android.OpenObserve
import com.openobserve.android.core.configuration.BatchSize
import com.openobserve.android.core.configuration.Configuration
import com.openobserve.android.core.configuration.UploadFrequency
import com.openobserve.android.privacy.TrackingConsent
import com.openobserve.android.rum.Rum
import com.openobserve.android.rum.RumConfiguration
import com.openobserve.android.rum.tracking.ActivityViewTrackingStrategy
class OpenObserveApp : Application() {
override fun onCreate() {
super.onCreate()
val config = Configuration.Builder(
clientToken = BuildConfig.OPENOBSERVE_CLIENT_TOKEN,
env = "production",
variant = "", // build flavor, optional
service = "com.example.app",
)
.setBatchSize(BatchSize.MEDIUM)
.setUploadFrequency(UploadFrequency.AVERAGE)
.setFirstPartyHosts(listOf("api.example.com"))
.build()
OpenObserve.setVerbosity(Log.VERBOSE) // lower or remove in production
OpenObserve.initialize(this, config, TrackingConsent.GRANTED)
val rumConfig = RumConfiguration.Builder(BuildConfig.OPENOBSERVE_RUM_APPLICATION_ID)
.useCustomEndpoint("https://your-openobserve-instance:5080")
.trackUserInteractions()
.trackLongTasks(250L)
.trackNonFatalAnrs(true)
.setSessionSampleRate(100f)
.useViewTrackingStrategy(ActivityViewTrackingStrategy(true))
.build()
Rum.enable(rumConfig)
}
}Register the Application class in your manifest so it actually runs:
<application
android:name=".OpenObserveApp"
...>That is the whole setup. OpenObserve.initialize starts the core SDK, Rum.enable turns on RUM with the automatic instrumentation you configured, and data begins flowing to your OpenObserve instance.
!!! note "The connection model"
You pass the **client token** to `Configuration.Builder`, the **RUM application id** to `RumConfiguration.Builder`, and point `useCustomEndpoint` at your OpenObserve instance's base URL — the SDK appends the RUM intake path automatically. There is no separate "organization" field; your organization is part of the ingestion endpoint and token. The built-in `useSite(...)` managed-cloud presets (US1, EU1, and so on) are still being wired up for OpenObserve Cloud in this alpha, so use the custom-endpoint approach for self-hosted and today's setups.!!! note "Consent gating"
Nothing is collected until tracking consent is `GRANTED`. If you show a consent dialog, initialize with `TrackingConsent.PENDING` and call `OpenObserve.setTrackingConsent(TrackingConsent.GRANTED)` once the user agrees. See [Security & Privacy](./security-privacy.md).Step 3 — Configuration options
Core options (Configuration.Builder)
| Option | Type | Default | Description |
|---|---|---|---|
clientToken | String | — (required) | Ingestion credential from Data → Data Sources → Real User Monitoring. |
env | String | — (required) | Environment tag, e.g. production, staging. |
variant | String | "" | Build flavor / variant, optional. |
service | String | app package id | Service/app identifier used to group data. |
setVersion(String) | String | app version | Release version — used for release health. |
setBatchSize(BatchSize) | BatchSize | MEDIUM | SMALL / MEDIUM / LARGE — how much to buffer per upload. |
setUploadFrequency(UploadFrequency) | UploadFrequency | AVERAGE | FREQUENT / AVERAGE / RARE — how often to upload. |
setFirstPartyHosts(List<String>) | List<String> | [] | Hosts to inject trace headers for. |
setCrashReportsEnabled(Boolean) | Boolean | true | Capture unhandled JVM crashes. |
setEncryption(Encryption) | Encryption | — | Encrypt buffered data at rest on disk. |
setProxy(...) | — | — | Route ingestion through a proxy. |
setUseDeveloperModeWhenDebuggable(...) | Boolean | — | More verbose behavior on debuggable builds. |
setAdditionalConfiguration(Map) | Map | {} | Advanced/internal configuration keys. |
TrackingConsent (passed to initialize) is PENDING, GRANTED, or NOT_GRANTED.
RUM options (RumConfiguration.Builder)
| Option | Type | Default | Description |
|---|---|---|---|
RumConfiguration.Builder(applicationId) | String | — (required) | RUM application id from Data → Data Sources → Real User Monitoring. |
useCustomEndpoint(url) | String | — | Your OpenObserve instance base URL. |
setSessionSampleRate(Float) | Float 0–100 | 100 | Percentage of sessions to keep. |
setTelemetrySampleRate(Float) | Float 0–100 | — | Percentage of internal SDK telemetry to keep. |
trackUserInteractions(...) | — | off | Auto-capture taps/gestures as actions. |
useViewTrackingStrategy(strategy) | ViewTrackingStrategy? | none | How screens become views (see below). |
trackLongTasks(thresholdMs) | Long | 100 | Report main-thread stalls above the threshold. |
trackNonFatalAnrs(Boolean) | Boolean | — | Capture non-fatal ANRs. |
trackBackgroundEvents(Boolean) | Boolean | false | Record events while the app is backgrounded. |
trackFrustrations(Boolean) | Boolean | true | Detect rage taps and other frustration signals. |
trackAnonymousUser(Boolean) | Boolean | — | Attach a stable anonymous id to sessions. |
collectAccessibility(Boolean) | Boolean | — | Collect device accessibility settings. |
setVitalsUpdateFrequency(...) | — | — | Mobile-vitals sampling cadence. |
setViewEventMapper / setResourceEventMapper / setActionEventMapper / setErrorEventMapper / setLongTaskEventMapper | mapper | — | Scrub or drop events before send. |
Step 4 — Track screens (views)
Pass a view-tracking strategy to useViewTrackingStrategy. The right one depends on how your app is structured (all strategies live in com.openobserve.android.rum.tracking):
import com.openobserve.android.rum.tracking.ActivityViewTrackingStrategy
import com.openobserve.android.rum.tracking.FragmentViewTrackingStrategy
import com.openobserve.android.rum.tracking.MixedViewTrackingStrategy
import com.openobserve.android.rum.tracking.NavigationViewTrackingStrategy
// One view per Activity (pass true to also track lifecycle timing):
.useViewTrackingStrategy(ActivityViewTrackingStrategy(true))
// One view per Fragment:
.useViewTrackingStrategy(FragmentViewTrackingStrategy(true))
// Both Activities and Fragments:
.useViewTrackingStrategy(MixedViewTrackingStrategy(true))
// Jetpack Navigation — track each destination of a nav graph:
.useViewTrackingStrategy(NavigationViewTrackingStrategy(R.id.nav_host_fragment, true))ActivityViewTrackingStrategy— best for Activity-based navigation.FragmentViewTrackingStrategy— for single-Activity apps that swap Fragments.MixedViewTrackingStrategy— when you use both.NavigationViewTrackingStrategy— for Jetpack Navigation; pass the nav-host id, whether to track arguments, and optionally aComponentPredicateto filter or rename destinations.
Manually
When automatic tracking does not fit — custom transitions, dialogs, or a screen you want named yourself — drive views directly through the RUM monitor:
import com.openobserve.android.rum.GlobalRumMonitor
val rum = GlobalRumMonitor.get()
rum.startView("checkout", "Checkout")
// ...user is on the screen...
rum.stopView("checkout")Step 5 — Track user actions
With trackUserInteractions() enabled, taps, scrolls, and swipes are captured automatically and named from the target view's resource id or accessibility label. To record an action explicitly:
import com.openobserve.android.rum.GlobalRumMonitor
import com.openobserve.android.rum.RumActionType
GlobalRumMonitor.get().addAction(
RumActionType.TAP,
"Add to cart",
mapOf("productId" to "sku-42"),
)RumActionType values: TAP, SCROLL, SWIPE, CLICK, BACK, CUSTOM. For actions that span time, use startAction and stopAction with the same type and name.
Step 6 — Track network requests with OkHttp
Add the o2-sdk-android-okhttp artifact and instrument your OkHttpClient with configureOpenObserveInstrumentation. This records every request as a RUM resource — URL, method, status, size, and timing — and, for your traced hosts, injects W3C trace-context headers so the resource links to the backend trace in OpenObserve.
import com.openobserve.android.okhttp.configureOpenObserveInstrumentation
import com.openobserve.android.okhttp.rum.RumNetworkInstrumentationConfiguration
import com.openobserve.android.okhttp.apm.ApmNetworkInstrumentationConfiguration
import okhttp3.OkHttpClient
val tracedHosts = listOf("api.example.com")
val client = OkHttpClient.Builder()
.configureOpenObserveInstrumentation(
rumInstrumentationConfiguration = RumNetworkInstrumentationConfiguration(),
apmInstrumentationConfiguration = ApmNetworkInstrumentationConfiguration(tracedHosts),
)
.build()The RUM instrumentation records the resource; the APM instrumentation propagates distributed tracing to the hosts you list, so a slow screen links straight to the backend trace behind it. Reuse this single instrumented client throughout your app. (Lower-level OpenObserveInterceptor and TracingInterceptor are also available if you need finer control over interceptor ordering.)
To record a resource manually — for a non-OkHttp transport, for example:
import com.openobserve.android.rum.GlobalRumMonitor
import com.openobserve.android.rum.RumResourceKind
import com.openobserve.android.rum.RumResourceMethod
val rum = GlobalRumMonitor.get()
rum.startResource("req-1", RumResourceMethod.GET, "https://api.example.com/cart", emptyMap())
// ...request completes...
rum.stopResource("req-1", 200, 1024L, RumResourceKind.NATIVE, emptyMap())Step 7 — Report errors
Unhandled JVM crashes are captured automatically (core setCrashReportsEnabled is on by default). Report handled errors yourself so you can see failures you caught and recovered from:
import com.openobserve.android.rum.GlobalRumMonitor
import com.openobserve.android.rum.RumErrorSource
try {
checkout()
} catch (e: Exception) {
GlobalRumMonitor.get().addError(
"Checkout failed",
RumErrorSource.SOURCE,
e, // Throwable — gives you the stack trace
mapOf("screen" to "checkout"),
)
}RumErrorSource distinguishes error origins (source, network, and so on). When you only have a stack-trace string rather than a Throwable, use addErrorWithStacktrace. For failed network calls, stopResourceWithError records the resource and its error together. For crash symbolication and error grouping, see Error & Crash Tracking.
Step 8 — User identity and global context
Attach the logged-in user so you can measure user-level impact (respecting consent). id is required; the rest are optional:
import com.openobserve.android.OpenObserve
// after login
OpenObserve.setUserInfo(
id = "user-123",
name = "Ada Lovelace",
email = "ada@example.com",
extraInfo = mapOf("plan" to "premium"),
)
// on logout
OpenObserve.clearUserInfo()Add global attributes that attach to every RUM event, log, and span — useful for release channel, feature flags, or A/B buckets. Set these on the RUM monitor:
import com.openobserve.android.rum.GlobalRumMonitor
val rum = GlobalRumMonitor.get()
rum.addAttribute("feature_flag.new_checkout", true)
rum.addAttribute("build.channel", "beta")You can also record feature-flag evaluations with rum.addFeatureFlagEvaluation(name, value), and attach account-level context with OpenObserve.setAccountInfo(...).
Step 9 — Logs and tracing (optional)
The same SDK can forward structured logs and distributed traces to OpenObserve. Enable each product once during initialization:
import com.openobserve.android.logs.Logs
import com.openobserve.android.logs.LogsConfiguration
import com.openobserve.android.trace.Trace
import com.openobserve.android.trace.TraceConfiguration
Logs.enable(LogsConfiguration.Builder().build())
Trace.enable(TraceConfiguration.Builder().build())Logs are correlated with the active RUM session, so you can pivot from a session to its logs in OpenObserve. Tracing pairs with the OkHttp APM instrumentation from Step 6 to give you one continuous trace from tap to backend.
Step 10 — Native (NDK) crash reporting
JVM crashes are captured out of the box. For native C/C++ crashes — the ones that never surface a Java stack trace — add the o2-sdk-android-ndk artifact and enable NDK crash reporting during initialization:
import com.openobserve.android.ndk.NdkCrashReports
// after OpenObserve.initialize(...)
NdkCrashReports.enable()This installs native signal handlers that capture crashes in your native libraries and report them to OpenObserve alongside your JVM errors and RUM sessions.
Step 11 — Deobfuscation and symbol upload
If you ship a release build with R8/ProGuard, your stack traces arrive obfuscated, and NDK crashes arrive as raw addresses. To make them readable, OpenObserve publishes an Android Gradle plugin (id io.openobserve.openobserve-sdk-android-gradle-plugin) that uploads your R8/ProGuard mapping files and NDK symbol files as part of your build, so OpenObserve can deobfuscate and symbolicate crash reports automatically.
!!! note
The plugin's build-script configuration DSL is being finalized in the current alpha. Apply the plugin id above, then follow the current setup instructions in the [SDK's GitHub repository](https://github.com/openobserve) for wiring the upload task and credentials — the exact DSL is documented there and evolving quickly. See [Error & Crash Tracking](./error-tracking.md) for how symbolicated crashes appear in OpenObserve.Step 12 — Session Replay (optional)
Add the o2-sdk-android-session-replay artifact to record privacy-first playback of user sessions. Enable it with a sample rate and privacy levels:
import com.openobserve.android.sessionreplay.SessionReplay
import com.openobserve.android.sessionreplay.SessionReplayConfiguration
import com.openobserve.android.sessionreplay.ImagePrivacy
import com.openobserve.android.sessionreplay.TextAndInputPrivacy
import com.openobserve.android.sessionreplay.TouchPrivacy
SessionReplay.enable(
SessionReplayConfiguration.Builder(sampleRate = 20f) // replay is heavier — sample lower than sessions
.setImagePrivacy(ImagePrivacy.MASK_ALL)
.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_ALL)
.setTouchPrivacy(TouchPrivacy.HIDE)
.build(),
)The defaults are privacy-preserving: images MASK_ALL, text and inputs MASK_ALL, touches HIDE. Loosen them deliberately — ImagePrivacy.MASK_LARGE_ONLY or MASK_NONE, TextAndInputPrivacy.MASK_SENSITIVE_INPUTS, TouchPrivacy.SHOW — only where you are sure no sensitive data is exposed. See Security & Privacy for the full privacy model.
Performance and overhead
The SDK is designed to stay out of your app's way:
- Off the main thread. Collection, batching, and upload happen in the background.
- Batched and buffered. Events are grouped and written to disk, then uploaded on the
UploadFrequencycadence; if the device is offline, they wait and retry rather than being lost. - You control volume.
setSessionSampleRate, a largerBatchSize, and a rarerUploadFrequencyall reduce network and battery use. - Session Replay is the heaviest feature — keep its sample rate well below your session sample rate.
- Encryption at rest. Use
setEncryption(...)to encrypt buffered data on disk if your threat model requires it.
See Best Practices for production tuning and cost control.
Verify it works
- Build and run the app on a device or emulator (a debug build is fine).
- Navigate a few screens, tap around, and trigger a network request through the instrumented OkHttp client.
- In OpenObserve, open RUM and confirm your session appears with views, actions, and resources.
- Force a test error (
GlobalRumMonitor.get().addError(...)) and confirm it shows under Error Tracking.
If nothing appears, raise the SDK log level (OpenObserve.setVerbosity(Log.VERBOSE)) and watch Logcat during startup and navigation.
Troubleshooting
- No data in OpenObserve. Confirm the
useCustomEndpointURL is reachable from the device, the client token is valid, and consent isGRANTED. Enable verbose logging and watch Logcat. - Screens not tracked. Make sure you passed a
useViewTrackingStrategy, and that it matches your navigation style (Activity vs. Fragment vs. Navigation). For custom flows, fall back to manualstartView/stopView. - Network calls missing. Confirm every request goes through the
OkHttpClientyou built withconfigureOpenObserveInstrumentation— requests on an uninstrumented client are invisible to RUM. - Stack traces are obfuscated. Apply the OpenObserve Gradle plugin and upload your R8/ProGuard mapping and NDK symbol files for the release build — see Step 11 and Error & Crash Tracking.
- Build fails on Java version. Set
sourceCompatibility/targetCompatibilityto Java 11 andjvmTarget = "11"; the SDK requires Java 11. - Too much data / cost. Lower
setSessionSampleRateand the Session Replay sample rate; see Best Practices.
What's next
- Performance Monitoring — read mobile vitals, screen timing, and network performance.
- Error & Crash Tracking — symbolication, mapping upload, and error grouping.
- Security & Privacy — consent, masking, and data scrubbing.
- Best Practices — sampling, cost, and release health.
- React Native and iOS — if you also ship cross-platform or Apple apps.
Frequently asked questions
Which Gradle dependencies do I need for Android RUM?
At minimum you add the core and RUM artifacts from the ai.openobserve Maven group: ai.openobserve
and ai.openobserve. Optional artifacts add the features you need — o2-sdk-android-okhttp for network tracking, o2-sdk-android-ndk for native crash reporting, o2-sdk-android-logs and o2-sdk-android-trace for logs and tracing, and o2-sdk-android-session-replay for Session Replay. Pin the same version (currently 0.1.0-alpha5) across all of them.What are the minimum requirements to use the Android SDK?
The SDK targets minSdk 23 (some auto-instrumentation needs API 29), compiles against compileSdk 36, and is built with Kotlin 2.0.21 and Java 11. Your app's Gradle module needs Java 11 source and target compatibility. The SDK works with both Kotlin and Java projects, though the examples in this guide are Kotlin.
Where do I get the client token and RUM application id?
Both come from Data → Data Sources → Real User Monitoring in your OpenObserve instance. The client token authenticates ingestion and the RUM application id identifies your app in RUM. You pass the client token to Configuration.Builder and the application id to RumConfiguration.Builder, then point the RUM config's custom endpoint at your OpenObserve instance base URL with useCustomEndpoint.
How does the Android SDK track screens automatically?
Pass a view-tracking strategy to RumConfiguration.Builder via useViewTrackingStrategy. ActivityViewTrackingStrategy tracks each Activity as a view, FragmentViewTrackingStrategy tracks Fragments, MixedViewTrackingStrategy tracks both, and NavigationViewTrackingStrategy tracks destinations of a Jetpack Navigation graph. You can also drive views manually with GlobalRumMonitor.get().startView and stopView.
Does the Android SDK capture native (NDK) crashes?
JVM crashes are captured by default through the core configuration. For native C/C++ crashes you add the o2-sdk-android-ndk artifact and call NdkCrashReports.enable() during initialization. To get readable stack traces you upload your ProGuard/R8 mapping file and NDK symbol files as part of your release build.
How do I make stack traces readable after R8/ProGuard obfuscation?
OpenObserve publishes an Android Gradle plugin (id io.openobserve.openobserve-sdk-android-gradle-plugin) that uploads your R8/ProGuard mapping files and NDK symbol files so obfuscated stack traces are deobfuscated in OpenObserve. The plugin's build-script DSL is being finalized in the current alpha, so check the SDK's GitHub repository for the current configuration.
How do I reduce the volume of RUM data sent to OpenObserve?
Use setSessionSampleRate on RumConfiguration.Builder to keep a percentage of sessions (0 to 100). Raise setBatchSize and lower setUploadFrequency to batch more aggressively and save battery. If you enable Session Replay, give it a lower replay sample rate than your session rate, and use the event mapper hooks to drop or redact specific events before they leave the device.
Last updated on
React Native
Add OpenObserve RUM to a React Native app — install the SDK, initialize at startup, and enable automatic instrumentation for screens, taps, and network calls.
iOS
Add OpenObserve RUM to a native Apple app in Swift — add the package, initialize at launch, and enable automatic view, action, and network instrumentation.