Skip to main content

Android

scout-android is a native Kotlin SDK that ships zero-config OpenTelemetry RUM for Android. One Scout.initialize(...) call auto-captures the full Real User Monitoring event set — taps, screens, crashes, ANR, jank, startup, lifecycle — and exports it as OTLP traces, metrics, and logs to a Scout collector.

Scout.initialize(
this,
ScoutConfig(
serviceName = "my-app",
endpoint = "https://otel.example.com",
),
)

That's the only code you write. Every tap, screen view, crash, ANR, frozen frame, and startup is gathered automatically — no manual Scout.track(...) calls anywhere in your app. HTTP tracking is the one opt-in (add one OkHttp interceptor — see below).

What You Get

CapabilitySignalMechanism
Screen / navigationscreen_view (long-lived root span) + screen_load + view_sessionActivityLifecycleCallbacks (250 ms post-resume debounce); Compose via NavController.trackScoutScreens(). Root span is resurrected on the next launch if the process dies mid-screen
App startupapp_startup span with app_startup.type = cold | warmCold: Process.getStartUptimeMillis() → first resume. Warm: onStart
FBC vital (First Build Complete)app_vital span with vital.name = fbcEmitted from cold start; ready as a first-class dashboard vital
INV vital (Interaction → Next View)app_vital span with vital.name = invTap timestamp correlated with the next screen_view within 5 s
Lifecycleapp_lifecycle.changed (device.app.lifecycle.resumed / .paused)ProcessLifecycleOwner; also drives session foreground/background
Tap trackinguser_interaction span (ui.type = tap, target, name source, x/y)Window.Callback wrap intercepting ACTION_UP. Label resolved from Compose semantics (contentDescription / text / testTag) or the View tree (contentDescription → resource id → TextView text → class name)
HTTP requestshttp.request CLIENT span with method, URL, status, durationScoutOkHttpInterceptoropt-in: add it to your OkHttpClient. Injects a W3C traceparent on first-party hosts
Errors (handled)error span with error.fingerprint, breadcrumbsScout.reportError(...)
JVM crashesapp_crash span with error.type, error.stack_trace, error.fingerprint, crash.last_screen, breadcrumbsThread.setDefaultUncaughtExceptionHandler persists synchronously; replayed on next launch
Native crashesnative_crash span with signal info, registers, stack, memory map, binary images (ELF build-ids)Custom NDK signal handler (libscout_crash) + ApplicationExitInfo (API 30+). Each OS death is reported exactly once (persisted watermark); normal exits (swipe-away, Force Stop, exit()) are never reported as crashes
ANRanr span with thread dump + breadcrumbsMain-thread watchdog (scout-anr-watchdog, polls at anrThresholdMs / 5, clamped 200-1000 ms) + ApplicationExitInfo REASON_ANR post-mortem tombstone
Janklong_task (≥ longTaskThresholdMs) + frozen_frame (≥ frozenFrameThresholdMs)AndroidX JankStats per Activity window
Memory (opt-in)android.memory.usage gauge (By)scout-metrics thread polling every vitalsCollectionIntervalSeconds (60 s). Off by default (enableMemoryMetrics)
CPU (opt-in)android.cpu.usage gauge (%)Reads /proc/self/stat. Off by default (enableCpuMetrics)
Frame (opt-in)android.frame.build_time gauge (ms)Drains FrameStats averages. Off by default (enableFrameMetrics)
Device / network contextnetwork.connection.type, device attributesDynamicAttributes + DeviceResources as resource attributes (always on)
LogsOTLP logsScout.log*()
Anonymous user iduser.anonymous_id on every spanUUID minted on first launch, persisted

Prerequisites

RequirementVersion
minSdkVersion≥ 26 (ApplicationExitInfo features activate from API 30+)
compileSdkVersion35
Android Gradle Plugin≥ 8.0
Gradle≥ 7
JDK17
NDK (bundled native crash handler)27.1.12297006
CMake3.22.1

The native signal handler ships as a prebuilt libscout_crash inside the artifact for arm64-v8a, x86_64, armeabi-v7a, and x86 — no NDK setup is required in your app.

Installation

scout-android is published on Maven Central. Add mavenCentral() to your repositories and the dependency to your module:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}

// app/build.gradle.kts
dependencies {
implementation("io.base14:scout-android:0.1.7")
}

scout-android re-exports scout-core transitively (api(...)) — you do not add scout-core yourself.

Initialization

Initialize once, as early as possible — in your Application.onCreate() so the crash handler is armed and cold-start timing is anchored before the first Activity:

import android.app.Application
import io.base14.scout.android.Scout
import io.base14.scout.core.ScoutConfig

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Scout.initialize(
this,
ScoutConfig(
serviceName = "my-app",
serviceVersion = "1.0.0",
endpoint = "https://otel.example.com",
headers = mapOf("Authorization" to "Bearer …"),
),
)
}
}

initialize is idempotent — a second call is a no-op. Read-only state is available on Scout: isInitialized, sessionId, userId, anonymousId.

Screen tracking with Compose Navigation

Activity screens are tracked automatically. For Compose Navigation, attach the tracker to your NavController so route changes emit screen_view:

val navController = rememberNavController()
navController.trackScoutScreens()

You can also set the current screen manually at any time — Scout.setScreen("Checkout") switches the screen tracker to manual mode.

HTTP tracking (opt-in)

HTTP is the one signal you wire up. Add ScoutOkHttpInterceptor to your client:

val client = OkHttpClient.Builder()
.addInterceptor(ScoutOkHttpInterceptor())
.build()

It emits an http.request span per call, skips the collector endpoint and any ignoreUrlPatterns, and injects a W3C traceparent header on hosts listed in firstPartyHosts.

Setting user identity

Scout.setUser(
id = "user-123",
attributes = mapOf(
"email" to "jane@example.com",
"plan" to "pro",
),
)

// On logout:
Scout.clearUser()

user.id and every attribute ride on every span until cleared.

Setting session attributes

Scout.setSessionAttributes(mapOf("tenant" to "acme", "ab_bucket" to "B"))
Scout.clearSessionAttributes()

They attach to every subsequent span, metric, and log for the rest of the session, and persist until cleared.

Configuration

ScoutConfig is the single config object. serviceName and endpoint are the only required fields; everything else has a sensible default.

Identity

FieldTypeDefaultDescription
serviceNameString(required)Logical app identifier. Used as service.name. Must be non-blank.
endpointString(required)OTLP-HTTP collector URL. /v1/traces, /v1/metrics, /v1/logs are appended automatically. Must be non-blank.
serviceVersionString?nullMaps to service.version. Auto-detected if null.
environmentString?nullDeployment environment (e.g. production).
headersMap<String, String>{}Extra HTTP headers on every OTLP export. Use for auth.
resourceAttributesMap<String, String>{}Extra attributes merged into every signal's Resource.

Sessions

FieldTypeDefaultDescription
sessionSampleRateDouble (0-100)1.0Percent of sessions sampled — default 1%. Decided once per session; a sampled session sends everything (spans, metrics, logs), an unsampled one sends nothing.
alwaysCaptureErrorsBooleantrueError / crash / ANR-class spans bypass sessionSampleRate and are always exported.
sessionTimeoutMinutesInt30Inactivity timeout before a new session.id is minted.
maxSessionDurationMinutesInt60Hard cap on session lifetime.

Network

FieldTypeDefaultDescription
firstPartyHostsList<String>[]Hosts that receive a W3C traceparent header. Supports exact match or *.host wildcards.
ignoreUrlPatternsList<String>[]URL substrings excluded from HTTP tracking.

Thresholds

FieldTypeDefaultDescription
anrThresholdMsLong5000Main-thread block duration that fires an anr span.
longTaskThresholdMsLong100Frame duration that qualifies as a long_task.
frozenFrameThresholdMsLong700Frame duration that qualifies as a frozen_frame.

Batching & export (applies to spans, metrics, AND logs)

FieldTypeDefaultDescription
exportIntervalSecondsInt30One export cadence for spans, metrics, and logs (coerced ≥ 1).
maxExportBatchSizeInt512Max items per export batch, per signal.
maxQueueSizeInt2048Max items buffered awaiting export; overflow is dropped.
maxRetriesInt0Delivery attempts after a failed export. Default 0 = at-most-once.
metricExportIntervalSecondsInt?nullMetrics-only override of exportIntervalSeconds.
vitalsCollectionIntervalSecondsInt60How often memory/CPU/frame gauges are polled (when enabled).

Per-metric switches

The SDK ships no metrics by default — each gauge is opt-in.

FieldDefaultDescription
enableMetricstrueMaster switch for the metrics pipeline. Individual gauges still need their own switch below.
enableMemoryMetricsfalseandroid.memory.usage gauge.
enableCpuMetricsfalseandroid.cpu.usage gauge.
enableFrameMetricsfalseandroid.frame.build_time gauge.

Auto-instrumentation toggles

Every auto-instrumentation can be turned off independently. Span and log instrumentation defaults to on; metric collection defaults to off (see Per-metric switches).

ToggleDefaultWhat you lose when false
enableScreenTrackingtruescreen_view / screen_load / view_session spans.
enableTapTrackingtrueAll user_interaction spans.
enableHttpTrackingtruehttp.request spans from ScoutOkHttpInterceptor.
enableErrorTrackingtrueerror spans — including manual Scout.reportError(...) calls, which become no-ops.
enableCrashTrackingtrueJVM app_crash + NDK native_crash + ApplicationExitInfo fallback.
enableAnrTrackingtrueanr spans (watchdog + tombstone).
enableJankTrackingtruelong_task / frozen_frame spans.
enableLifecycleTrackingtrueapp_lifecycle.changed spans and the session foreground/background transitions they drive.
enableStartupTrackingtrueapp_startup spans and the FBC vital.
enableLoggingtrueScout.log*() calls become no-ops.

Offline buffer

Offline buffering is fully disabled by default — nothing is written to disk, and a batch that fails to export is dropped (strict at-most-once delivery).

FieldTypeDefaultDescription
offlineBufferEnabledBooleanfalseMaster toggle. Persist failed batches and replay them on next initialize() or connectivity change.
offlineMaxTraceItemsInt0Accepted, not yet enforced.
offlineMaxMetricItemsInt0Accepted, not yet enforced.
offlineMaxLogItemsInt0Accepted, not yet enforced.
maxOfflineStorageMbInt5Accepted, not yet enforced.

The four cap fields are part of the config surface but nothing reads them yet. When offlineBufferEnabled is on, the persisted queue is bounded by maxQueueSize and maxExportBatchSize — the same limits the in-memory path uses. Size the buffer with those.

Diagnostics

FieldTypeDefaultDescription
debugLoggingBooleanfalsePrint SDK-internal export logging to Logcat. Use it to confirm batches are leaving the device; leave it off in release builds.

Filtering — beforeSend

ScoutConfig(
// …
beforeSend = { name, attributes ->
// Mutate attributes to scrub; return false to drop the signal.
attributes.remove("user.email")
val isHealthCheck = (attributes["http.url"] as? String)
?.contains("/health") == true
!isHealthCheck
},
)

beforeSend runs synchronously on every span / metric / log before export. It sees per-signal attributes — resource attributes (service.name, os.*, device.*) are not in the payload.

Native crash setup

No app-side setup is required — the JVM handler, the NDK signal handler, and the ApplicationExitInfo reader all install automatically inside Scout.initialize.

JVM crashes

A Thread.setDefaultUncaughtExceptionHandler (chaining any previous handler) catches uncaught exceptions on any thread, persists a report synchronously (error.type, error.message, error.stack_trace, error.fingerprint, crash.last_screen, breadcrumbs) before the process dies, and replays it as an app_crash span on the next launch.

Native (NDK) crashes

A custom in-process signal handler (libscout_crash) catches SIGSEGV / SIGABRT / SIGBUS / SIGILL / SIGFPE, writes a compact report to cacheDir (signal info, registers, stack, memory map, binary images with ELF build-ids), and emits it as a native_crash span on the next launch.

ApplicationExitInfo fallback (API 30+)

On Android 11+, scout-android reads ActivityManager.getHistoricalProcessExitReasons and emits a span for any OS-recorded death newer than a persisted watermark — each death exactly once across launches. REASON_CRASH_NATIVEnative_crash, REASON_CRASHapp_crash, REASON_ANRanr (with a tombstone thread dump, capped at 128 KB). Benign exits (user_requested, user_stopped, exit_self) are filtered out. This catches deaths the OS killed before in-process handlers could run (OOM, hard watchdog).

Because crashes drain on the next launch, to test: trigger a real fault, relaunch the app, then check the collector.

Manual API

Every method is @JvmStatic and a no-op if the SDK is not initialized.

MethodPurpose
setScreen(name)Set the current screen (switches to manual mode).
setUser(id, attributes = {}) / setUserAttributes(attributes) / clearUser()User identity.
setAccount(id, name = null) / clearAccount()Account / org context.
setFeatureFlag(name, value) / clearFeatureFlags()Feature-flag values.
setSessionAttributes(attributes) / clearSessionAttributes()Session attributes.
addBreadcrumb(type, message) / setBreadcrumbs(list)Breadcrumb trail.
reportError(throwable, handled = true)Report an error from a Throwable (error span).
reportError(type, message, stackTrace)Report an error from string fields.
logDebug / logInfo / logWarning / logError(message, attributes = {})Emit a log at that level.
log(level, message, attributes = {})Emit a log at an explicit ScoutLogLevel.
logEvent(name, attributes = {})Emit a named custom event.
addTiming(name)Record a named timing marker.
startVital(name) / endVital(name, description = null)Custom vital measurement.
recordOperationStep(name, step, key = null, failureReason = null)Step in a multi-step operation.
reportHttp(method, url, statusCode, startEpochNanos, endEpochNanos)Manually emit an http.request span.
reportLongTask(durationMs)Manually emit a long_task span.
reportTap(target, targetType, x, y)Manually emit a user_interaction span.
emitGauge(name, value, unit)Emit a custom gauge metric.
recordScreenLoad(name, durationMs) / recordViewSession(name, durationMs)Timing spans.
recordSpan(name, durationMs, attributes = {})Emit an arbitrary named span.

What happens when export fails

Delivery is at-most-once by default (maxRetries = 0): a batch gets one attempt, and a failed batch is dropped rather than risking a duplicate delivery. A retried timeout whose first request the collector already ingested would store the same events twice.

A batch counts as delivered only on an HTTP 2xx. Any other status, or a transport exception, is a failure.

FailureWhat Scout does (defaults)
Any export failure, maxRetries = 0 (default)One attempt; batch dropped. No duplicates, ever.
maxRetries = n configuredUp to n + 1 attempts total, retried back-to-back with no backoff. Duplicate risk on ambiguous failures.
Failure with offlineBufferEnabled = trueBatch persisted to cacheDir and replayed on a later launch.
Queue overflow (maxQueueSize, default 2048)Oldest items dropped before they are ever exported.
Process dies mid-intervalAnything emitted since the last export is lost — there is no flush-on-background hook. Crash evidence is the exception: it is persisted synchronously at crash time and replayed on the next launch.

Set debugLogging = true to print each batch's destination and HTTP status to Logcat.

Troubleshooting

SymptomLikely cause + fix
No http.request spansHTTP is opt-in — add ScoutOkHttpInterceptor to your OkHttpClient.
native_crash / app_crash not appearing after a crashCrashes drain on the next launch. Relaunch the app, then check the collector.
Android native_crash empty on API < 30ApplicationExitInfo requires API 30+. Older devices only get whatever the in-process NDK handler caught.
Crash button gives a graceful shutdownYou're calling exit(), which no crash reporter intercepts. Trigger a real fault (uncaught exception, or a native null-deref).
Compose screens not trackedAttach NavController.trackScoutScreens(), or call Scout.setScreen(...) manually.
Tap labels are class namesThe tapped widget has no contentDescription / testTag / text to resolve a friendlier name. Add a contentDescription or Modifier.testTag(...).
Requests not getting traceparentThe host isn't in firstPartyHosts. Add it (api.example.com) or a wildcard (*.example.com).
No telemetry at allSet debugLogging = true to print export attempts and their HTTP status to Logcat, then confirm the endpoint is reachable from the device. Remember the default sessionSampleRate is 1%.

Performance considerations

  • Unified 30 s batching. Spans, metrics, and logs each flush once per exportIntervalSeconds (default 30 s).
  • No metrics unless enabled. The default configuration ships zero metric data points; the memory / CPU / frame gauges are opt-ins.
  • Zero disk usage by default. Offline buffering is off; the only disk writes are crash evidence.
  • Sampling. sessionSampleRate drops full sessions — never individual events — so session traces stay coherent.
  • Idempotent init. A second initialize is a no-op.

Security considerations

  • PII scrubbing. Use beforeSend to mutate attributes (attributes.remove("user.email")) or drop signals (return false).
  • Custom headers for auth. Pass headers = mapOf("Authorization" to "Bearer …").
  • No telemetry-to-disk PII by default. The offline buffer is off; when enabled, scrub in beforeSend before batches hit disk.

FAQ

Does scout-android require the NDK in my app?

No. The native crash handler ships prebuilt for all four ABIs inside the artifact.

Will init block app startup?

Scout.initialize does its work inline but is lightweight; call it in Application.onCreate(). Instrumentation runs on background threads.

Can I add custom spans, metrics, or logs?

Yes — recordSpan, emitGauge, and log* are the manual entry points. They go through the same sampling / export pipeline.

Does it work with Java-only apps?

Yes — every method is @JvmStatic; call Scout.initialize(this, config) from Java the same way.

What's next

References

Was this page helpful?