Android
io.e-volv:logs-android is the SDK for Android (minSdk 24, compileSdk 34): crash and ANR capture, session tracking and batched log shipping. It is the second artifact on the shared JVM core (io.e-volv:logs-jvm, see the Java page) — same event model, ids, redaction and batching — with a mobile transport: a disk-backed queue, lifecycle-driven flush and long-lived sessions.
Install
// app/build.gradle.kts — pin the current release where the snippet says 0.1.0
implementation("io.e-volv:logs-android:0.1.0") // brings the JVM core transitivelyProcessLifecycleOwner events arrive via manifest merging from the lifecycle-process dependency — no manifest entry of your own is needed. The module currently builds from source in the monorepo; until the artifact is on Maven Central, point Gradle at packages/logs-jvm.
Initialise
Initialise once in Application.onCreate:
import io.evolve.logs.android.EvolveAndroidOptions
import io.evolve.logs.android.EvolveLogsAndroid
class App : Application() {
override fun onCreate() {
super.onCreate()
EvolveLogsAndroid.init(
this,
EvolveAndroidOptions.builder()
.key("evk_pub_…") // public key — see "Key kinds" below
.url("https://api.e-volv.io/api/public/v1/logs")
.service("shop-android")
.environment("production")
.release(BuildConfig.VERSION_NAME) // the symbolication key
.build(),
)
val client = EvolveLogsAndroid.client()
client.info("cart opened", mapOf("cartId" to "c_1"))
try {
checkout()
} catch (e: Exception) {
client.exception(e, mapOf("cartId" to "c_1")) // exception.type/message/stack
}
}
}EvolveLogsAndroid.init(this, EvolveAndroidOptions.builder()
.key("evk_pub_…")
.url("https://api.e-volv.io/api/public/v1/logs")
.service("shop-android")
.environment("production")
.release(BuildConfig.VERSION_NAME)
.build());
EvolveLogsAndroid.client().info("cart opened", Map.of("cartId", "c_1"));Key kinds: what may ship in a binary
This SDK only accepts a public key (evk_pub_…), minted on the Observer Projects page and bound to your app’s application id. A server key (evk_…) belongs in servers only, and EvolveLogsAndroid.init refuses one with an IllegalArgumentException — the one case this SDK is loud rather than silent.
Every request carries two extra headers, and the ingest refuses a public key without them: x-evolve-app-id — your Android application id (context.packageName, overridable with appId(...)), checked against the key’s allowlist — and x-evolve-install-id, a UUID generated on first launch, persisted in a private SharedPreferences file and replaced when the user clears app data. Per-install quota keys on it, so one looping device cannot spend a project’s whole budget.
Sessions
Crash-free session rate is the number mobile teams watch, so a session is a first-class attribute: session.id (a UUID) rides on every event, one session per process foreground epoch — created at cold start, rotated each time the app returns to the foreground. app start is the cold-start marker; app foreground / app background transitions carry lifecycle.state in their attributes.
Crash capture
JVM crashes. Thread.setDefaultUncaughtExceptionHandler chains to the previously installed handler, so the app crashes exactly as before. The crash event (severity 17, exception.*,crash: true) is serialized once and written to the disk queue fsynced, on the dying thread; a dying process does no network, so delivery happens on the next launch — the queue is reloaded before anything else and ships first.
Breadcrumbs. The last 32 buffered events (default, breadcrumbCount) ship with the crash as a breadcrumbs attribute — a JSON array of {ts, severity, message}. That is the whole breadcrumb feature; the SDK adds no breadcrumbs API.
Trace join. EvolveTraceInterceptor reads traceparent off responses (falling back to x-evolve-traceparent), so a crash carries the trace of the server it was talking to and lands inside that trace.
ANR. A watchdog on the main looper (default 5 s, anrThresholdMs) reports the main thread’s stack as an error event with exception.type: "ANR".
Out of scope: native (NDK) crashes — a signal-handler write path is a different engineering problem and is not attempted here; Play’s native crash reporting or a future release can cover it. App Attest / Play Integrity attestation, session replay, vitals/RUM and metrics are out of scope too.
The buffer: disk, caps, drops
Batching is the shared contract — flush at 200 events, every 2 seconds or at 512 KB; 429 honours Retry-After (else backoff 500 ms → 10 s, 3 attempts); 413 halves the batch; past twice the batch size the oldest events drop, counted on client.dropped(). Mobile differs only in where the buffer lives: every event is appended to a disk queue (filesDir/evolve-logs/queue.bin) before it enters memory, with a byte cap (default 1 MB, queueMaxBytes) and drop-oldest. Flush is lifecycle-driven: ON_STOP (background) flushes. There is no exit hook; anything undelivered waits on disk.
Traces and spans
Same model as the core: client.span("db.query", attrs) installs the span as ambient context on the calling thread; the end is recorded with span.name and durationMs. client.traceparent() returns the W3C header, and client.runWithTraceparent(header) { … } continues an inbound trace. For your app’s own OkHttp clients:
val http = OkHttpClient.Builder()
.addInterceptor(EvolveTraceInterceptor()) // traceparent out, response trace remembered
.build()Injection never overwrites a traceparent the caller set.
Symbolication: upload your R8 mapping
Crashes arrive obfuscated unless the ingest has the release’s mapping.txt. Upload it in the release pipeline — a server key is required, not the public one from the app:
npx @evolve/logs-cli upload-artifacts \ --key evk_… --url https://api.e-volv.io \ --release 1.2.3 --platform android --type proguard \ app/build/outputs/mapping/release/mapping.txt
--release must match the release(...) you initialized the SDK with. Uploading backfills open, not-yet-symbolicated occurrences for the release.
Status
Written but not yet compile-verified: this module was authored on a machine with no Android SDK, so nothing here is compile- or run-verified. The code mirrors the compiled-and-green core module’s patterns line for line, and the single build step pending a machine with an SDK is the include line include("core", "server", "android") in settings.gradle.kts — it is deliberately not committed because applying com.android.library fails at configuration time without an SDK. The conformance run is likewise pending emulator CI; do not treat this SDK as conformant until that run is green.
Package reference: packages/logs-jvm/android. Back to SDK overview.