Android / Kotlin
Install and configure the Last9 Android RUM SDK. Kotlin API, CDN-hosted Maven AAR, automatic instrumentation for sessions, views, OkHttp, errors, ANRs, and resource sampling.
Real User Monitoring for Android apps. Automatic instrumentation for sessions, views, network requests, errors, ANR detection, and resource metrics via OpenTelemetry.
Prerequisites
- Android minSdk 21 (Android 5.0+)
- Kotlin 1.8+ or Java 8+
- An
Applicationsubclass - A Last9 RUM client token and OTLP endpoint
Create a Client Monitoring Token
- Open Last9 → Settings → Ingestion Tokens
- Click Create Token → choose type Client
- Set the allowed origin to
android://com.yourcompany.yourapp— use your app’s exact package name - Copy the token and the OTLP endpoint URL
CDN artifacts
| Artifact | URL |
|---|---|
| Maven repo root | https://cdn.last9.io/rum-sdk/android/maven/ |
| POM | https://cdn.last9.io/rum-sdk/android/maven/io/last9/rum-android/0.7.0/rum-android-0.7.0.pom |
| AAR | https://cdn.last9.io/rum-sdk/android/maven/io/last9/rum-android/0.7.0/rum-android-0.7.0.aar |
Maven coordinates: io.last9:rum-android:0.7.0. Staging builds use the -alpha suffix.
Installation
-
Add the CDN Maven repository
dependencyResolutionManagement {repositories {google()mavenCentral()maven { url = uri("https://cdn.last9.io/rum-sdk/android/maven/") }}}dependencyResolutionManagement {repositories {google()mavenCentral()maven { url = uri("https://cdn.last9.io/rum-sdk/android/maven/") }}} -
Add the dependency
In your app’s
build.gradle.kts:dependencies {implementation("io.last9:rum-android:0.7.0")} -
Initialize the SDK
In your
Applicationsubclass:import android.app.Applicationimport io.last9.rum.L9Rumimport io.last9.rum.L9RumConfigimport io.last9.rum.L9BaggageConfigclass MyApplication : Application() {override fun onCreate() {super.onCreate()L9Rum.initialize(this,L9RumConfig(baseUrl = "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",// Use android://com.your.package.name — must match the// origin allowlist on your Last9 client token.origin = "android://com.example.myapp",clientToken = "your-client-token",serviceName = "my-android-app",serviceVersion = "1.0.0",deploymentEnvironment = "production",))}} -
Register the Application class
In
AndroidManifest.xml:<application android:name=".MyApplication" ... >
What’s captured automatically
| Signal | Details |
|---|---|
| Network requests | OkHttp interceptor — latency, status code, URL |
| Screen views | Activity lifecycle via ActivityLifecycleCallbacks |
| Sessions | 15m inactivity / 4h max, persisted across restarts |
| App launch time | Cold and warm start duration |
| Resource metrics | Memory and CPU sampled periodically |
| Errors | Unhandled exceptions and JVM crashes |
| ANR detection | Main thread blocks beyond threshold (default 5s) |
view.ttfd | Time from last HTTP response to next rendered frame — measures post-API render latency on data-driven screens |
View time-to-full-display (view.ttfd)
view.ttfd measures how long it takes for the screen to render after the last API response completes. It captures the delta between the HTTP response timestamp and the next Choreographer frame — giving you end-to-end visibility on data-driven screens where content appears only after a network call.
| Attribute | Type | Description |
|---|---|---|
view.ttfd | float | Milliseconds between HTTP response end and next rendered frame |
view.ttid | float | Milliseconds from screen open to first frame (unchanged) |
How it works: after any L9NetworkInterceptor span ends, the SDK schedules a Choreographer.FrameCallback. The callback fires on the next VSYNC, and the SDK records the delta as view.ttfd on the active View span.
- Works on both Jetpack Compose and View-based UIs with no app code changes.
- Applies to the View span that was active when the HTTP call was made.
- If no View span is active when the response arrives, the measurement is skipped.
Configuration
L9RumConfig( // --- Required ---------------------------------------------------------
// OTLP collector endpoint baseUrl = "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",
// Authentication token from Last9 clientToken = "your-client-token",
// Application identifier (maps to service.name) serviceName = "my-android-app",
// App version string (maps to service.version) serviceVersion = "1.0.0",
// Environment name deploymentEnvironment = "production",
// --- Optional ---------------------------------------------------------
// Required for client_monitoring tokens. Use android://com.your.package.name — // must match the origin allowlist configured on your Last9 client token. origin = "android://com.example.myapp",
// Specific build identifier (maps to app.build_id) appBuildId = "1.0.0-build-42",
// Unique per-install ID (NOT a hardware ID). Persists across launches. appInstallationId = null,
// Session sampling rate: 0-100 (percentage). 100 = sample everything. sampleRate = 100,
// Print debug logs to logcat debugLogs = false,
// Automatically trace HTTP requests via OkHttp interceptor networkInstrumentation = true,
// Automatically capture unhandled exceptions errorInstrumentation = true,
// Max spans per export batch maxExportBatchSize = 100,
// Export timeout in milliseconds exportTimeoutMs = 30_000L,
// Enable ANR (Application Not Responding) detection anrDetectionEnabled = true,
// ANR threshold in milliseconds anrThresholdMs = 5_000L,
// Periodically sample memory and CPU resourceMonitoringEnabled = true,
// Interval between resource samples (ms) resourceSamplingIntervalMs = 30_000L,
// Setting this to true will hide network requests (and their // DNS/TCP/TLS/TTFB phase child spans) from the Last9 dashboard's // Sessions → APIs tab. Each request would get its own traceId // instead of sharing the current view's traceId, and that tab // only fetches spans that share the View's traceId. Keep this // false unless you specifically need per-request trace isolation. isolateTracePerRequest = false,
// Custom resource attributes added to every span resourceAttributes = mapOf( "app.platform" to "android", ),
// W3C Baggage propagation on outgoing requests baggage = L9BaggageConfig( enabled = false, allowedKeys = listOf("session.id", "user.id"), maxTotalBytes = 8192, trackedUrlPatterns = emptyList(), warnAtPercentage = 80, ),
// Substring patterns — matching URLs are skipped before span creation. // Prefer ignorePatterns below for regex support and hostname/pathname targeting. excludedUrlPatterns = listOf(".jpg", ".png", ".pdf", "cdn.example.com"),
// Fine-grained network ignore rules. Matched URLs are dropped before span // creation. Strings use substring matching; L9UrlPattern.Regex uses regex // search semantics. Takes precedence over excludedUrlPatterns. ignorePatterns = L9NetworkIgnorePatterns( fullUrl = listOf( L9UrlPattern.Contains("https://cdn.example.com"), L9UrlPattern.Regex("^https://.*\\.example\\.com", flags = "i"), ), pathname = listOf( L9UrlPattern.Contains(".pdf"), L9UrlPattern.Contains(".jpg"), L9UrlPattern.Regex("^/internal/metrics"), ), hostname = listOf( L9UrlPattern.Contains("cdn.example.com"), L9UrlPattern.Regex("(^|\\.)assets\\.example\\.com$", flags = "i"), ), ),
// Trace header propagation for ignored URLs. // PRESERVE (default): keep traceparent on ignored requests so downstream // services retain trace context. // STRIP: remove traceparent from ignored requests (e.g. third-party CDNs). propagationMode = L9PropagationMode.PRESERVE,)API reference
Identify a user
L9Rum.identify("user-123", mapOf( "email" to "user@example.com", "plan" to "premium",))Clear user on sign-out
L9Rum.clearUser()Capture errors
try { // risky operation} catch (e: Exception) { L9Rum.captureError(e, mapOf("screen" to "checkout"))}Unhandled exceptions are captured automatically when errorInstrumentation = true.
Track views
Activities are tracked automatically via ActivityLifecycleCallbacks. Use the manual API for fragments or Compose destinations:
L9Rum.startView("ProductDetailsScreen")L9Rum.setViewName("Product #42")Custom events
L9Rum.addEvent("purchase_completed", mapOf( "product_id" to "12345", "amount" to 29.99,))Global span attributes
// Inject attributes into every spanL9Rum.spanAttributes(mapOf( "experiment" to "checkout_v2", "feature_flag" to "new_cart",))
// ClearL9Rum.spanAttributes(null)Network phase child spans
When OkHttp instrumentation is enabled, each parent HTTP span includes child spans for individual network phases:
| Child span | What it measures |
|---|---|
dns | DNS lookup duration |
tcp_connect | TCP connection establishment |
tls_handshake | TLS negotiation |
ttfb | Time from request sent to first response byte |
No SDK config change is required — phase spans are emitted automatically.
Reused connections skip DNS, TCP, and TLS work. For those requests the SDK emits zero-duration child spans with l9rum.network.phase.skipped=true so the waterfall shape stays consistent.
Network interceptor (manual OkHttp setup)
When networkInstrumentation = true, HTTP calls through the SDK’s default client are traced automatically including network phase child spans. For a custom OkHttpClient, use L9Rum.instrumentOkHttp(builder, context) to attach both the HTTP span interceptor and the OkHttp EventListener.Factory required for phase timings:
val client = OkHttpClient.Builder() .let { L9Rum.instrumentOkHttp(it, context) } .build()If you use SSL pinning:
val client = OkHttpClient.Builder() .certificatePinner(existingPinner) // SSL pinning unchanged .let { L9Rum.instrumentOkHttp(it, context) } .build()Session ID
val sessionId: String? = L9Rum.getSessionId()Flush pending data
L9Rum.flush()Call flush() before the app exits or in response to critical lifecycle events where losing the last batch matters.
Network ignore patterns
Skip noisy URLs before span creation by matching against full URL, pathname, or hostname. Strings use substring matching; L9UrlPattern.Regex uses regex search semantics.
import io.last9.rum.L9NetworkIgnorePatternsimport io.last9.rum.L9UrlPattern
L9Rum.initialize( this, L9RumConfig( // ...required config ignorePatterns = L9NetworkIgnorePatterns( fullUrl = listOf( L9UrlPattern.Contains("https://cdn.example.com"), L9UrlPattern.Regex("^https://.*\\.example\\.com", flags = "i"), ), pathname = listOf( L9UrlPattern.Contains(".pdf"), L9UrlPattern.Contains(".jpg"), L9UrlPattern.Regex("^/internal/metrics"), ), hostname = listOf( L9UrlPattern.Contains("cdn.example.com"), L9UrlPattern.Regex("(^|\\.)assets\\.example\\.com$", flags = "i"), ), ), // PRESERVE (default): keep traceparent on ignored requests. // STRIP: remove traceparent from ignored requests. propagationMode = L9PropagationMode.PRESERVE, ),)WebView correlation
Inject the active native session and view IDs into a WebView so Browser RUM spans share the same session.id:
// After inflating the WebView — call once per WebView instance.L9Rum.instrument(webView)- Native context is re-injected on every session or view rollover via
evaluateJavascript. - Calling
instrument(webView)beforeL9Rum.initialize(...)emits a warning and is a no-op. - Detached or destroyed WebViews are pruned automatically.
Requires the Browser RUM JS SDK on the page. The JS SDK adopts the native session ID and fires l9rum:session_rollover when the session rotates, rotating the view span accordingly.
See the WebView Session Correlation guide for React Native/Flutter setup, the static-script path, auto-load Browser RUM, and verification steps.
Security
Client monitoring tokens are write-only and origin-scoped to your app’s package name. Safe to ship in the app binary.
Next steps
Once data is flowing, explore it in Discover > Applications — performance, errors, and sessions.
Troubleshooting
Please get in touch with us on Discord or Email if you have any questions.