Android RUM
Updated Oct 23, 2026 · today
Send Android app performance and errors to Epok with epok-rum-otel 0.2.1, built on OpenTelemetry Android 1.7.0. The wrapper is experimental: validated on emulators, with no real-device run yet. A prior sample measured approximately +320 KB download; your increase depends on dependencies and shrinking.
Install
Requires Android 8.0 (API 26) or later. Add the JitPack repository and consume the published dependency through Gradle so its transitive dependencies and exclusions are included. Keep the packaging exclusion below. The library was built with JDK 17, AGP 8.7.3, Gradle 8.9 and Kotlin 2.1.21.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}
// app/build.gradle.kts
android {
compileSdk = 35
defaultConfig { minSdk = 26 }
packaging.resources.excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
dependencies {
implementation("com.github.epok-cloud.epok-android:epok-rum-otel:0.2.1")
// Optional independent masked wireframe replay:
// implementation("com.github.epok-cloud.epok-android:epok-replay:0.2.1")
}Initialize and connect consent
Use an ingest-only key from the Connect gallery. Any key in an app is public; never embed a read-capable or symbols-upload key. Call start once; later configurations are ignored. Do not initialize a second OpenTelemetry Android agent in the same process. Secondary processes do not collect.
import android.app.Application
import dev.epok.rum.otel.EpokRum
import dev.epok.rum.otel.EpokRumConfig
class App : Application() {
override fun onCreate() {
super.onCreate()
// Main thread, main process, before the first Activity:
EpokRum.start(this, EpokRumConfig(
endpoint = "https://ingest.getepok.dev", // base URL, no /v1/traces suffix
ingestKey = "YOUR_API_KEY", // restricted ingest-only key
serviceName = BuildConfig.APPLICATION_ID, // must equal the mapping upload app_id
// serviceVersion defaults to the installed package versionName
))
}
}
// Register App as android:name=".App" in your manifest's <application>.Consent defaults to denied and is persisted. A denied launch creates no SDK, exporter or telemetry buffer. A saved grant initializes asynchronously on the next launch. Connect the following callbacks to separate explicit user choices.
// Separate callbacks for explicit choices in your consent UI, after start():
fun onRumAccepted() { EpokRum.grantConsent() }
fun onRumWithdrawn() { EpokRum.withdrawConsent() }
val state = EpokRum.consentState()Withdrawal blocks new exports, cancels collector calls and asynchronously shuts down collection and deletes local buffers. It cannot recall data already sent. After an initialization attempt and withdrawal, granting again requires a process restart (GRANTED_PENDING_RESTART); initialization failures can remain FAILED. Consent persistence performs filesystem I/O on the calling thread.
The SDK sends gzip OTLP/HTTP protobuf to https://ingest.getepok.dev/v1/traces and https://ingest.getepok.dev/v1/logs, authenticated with X-API-Key. Its collector client is separate from your app client and does not inherit its pinning, DNS, proxy or authenticator settings.
Capture and backend traces
- App start: eligible cold starts when consent was already granted at launch and start runs early. Mid-process grants cannot backfill starts; warm starts are not captured.
- Screens: Activity and AndroidX Fragment lifecycle, plus sessions and network changes.
- Network: spans for OkHttp clients configured with both adapters below.
- Java/Kotlin uncaught crashes (usually delivered on a later granted launch), foreground ANRs (detection can take about ten seconds), and jank from slow/frozen drawing.
import okhttp3.OkHttpClient
// Install both adapters once on each client whose destinations may receive trace context.
val client = OkHttpClient.Builder()
.addInterceptor(EpokRum.connectionInterceptor())
.addNetworkInterceptor(EpokRum.networkInterceptor())
.build()Instrumented requests propagate W3C trace context to the backend. Instrument your backend service to continue that trace. Propagation applies to every destination, including redirects; there is no host allowlist. Attach adapters only to appropriate clients. They pass through until initialization finishes. App requests continue while denied; an in-flight request may retain context injected before withdrawal.
Before export, URL queries, fragments and credentials are scrubbed from url.full and http.url; url.query, carrier attributes and installation IDs are removed. Exception stacks are capped at 16 KiB. Filtering happens at export: app-private disk buffers may contain original values until purged. URL paths, exception messages, log bodies and screen names are not generally redacted. Request/response bodies and headers are not captured by default.
Readable release crashes: R8 mappings
Keep the exact mapping.txt from each minified release. Follow the R8 mapping upload and stack-resolution guide to upload it from CI to /api/v1/tenants/{tenant_id}/mobile/mappings with a separate symbols:write key. Never ship that key in the app. Mappings let compatible exception records resolve to readable stacks on read; this wrapper does not provide automatic release-stack symbolication.
The samples set serviceName to BuildConfig.APPLICATION_ID, which is the same value the guide uploads as app_id (variant.applicationId). If you set another serviceName, upload that exact string as app_id. Match version_name to serviceVersion (the installed versionName by default). The wrapper does not automatically add build-code metadata. For version-only lookup, omit version_code and build_id from the upload identity and keep serviceVersion unique for every release build. Do not copy the guide's versionCode identity unchanged for this setup.
Replay and limits
RUM and mobile session replay can be used together or independently. Replay uses epok-replay:0.2.1 with its own grant() and withdraw() calls; connect both libraries to your consent policy. Replay starts denied on each launch, while RUM persists consent. Installing RUM does not enable replay or metrics.
Native crashes, WebView traffic, HttpURLConnection and Compose navigation are unsupported. Frame instrumentation measures drawing, not every input or layout delay. Cold-start fallback heuristics can omit slow starts or misclassify background starts; asynchronous initialization can miss the first Activity Created span. Collector outages and full-buffer pressure have not been validated. Delivery and initialization or withdrawal timing are not guaranteed. See the versioned SDK guide for dependency and lifecycle details.
Check the integration
- On a fresh install with consent denied, confirm there are no RUM requests to ingest.getepok.dev.
- Grant consent and wait for consentState() to report GRANTED. Navigate screens and make an instrumented OkHttp request; find the service in Traces and check its backend continuation.
- Check exported URLs contain no query string. Relaunch with saved consent to check cold starts.
- In a test build, trigger a Java/Kotlin crash and relaunch with consent granted to check delivery. Use the exact release mapping when checking an obfuscated stack.
- Withdraw consent and confirm new exports stop. Regrant and restart the process to resume capture.