Android
Native Android SDK, Kotlin-first with full Java interop: JVM crashes, NDK native crashes, ANRs, release-health sessions, breadcrumbs and logs.
Requirementslink
- minSdk 24 (Android 7.0), compileSdk 35, Kotlin 2.0+, AGP 8.x, Java/Kotlin toolchain 17
- Transitive runtime deps:
androidx.core-ktx,kotlinx-coroutines-android,kotlinx-serialization-json. OkHttp 4.12 is compileOnly, only needed for the network-breadcrumb interceptor INTERNETandACCESS_NETWORK_STATEpermissions merge in from the SDK manifest; consumer R8/ProGuard rules ship in the AAR, so your build needs no extra rules
1 - Installlink
dependencies {
implementation("cloud.newinstance:bugwatch:0.1.2")
}2 - Initialise (Application.onCreate, not an Activity)link
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
BugWatch.initialize(application = this, options = BugWatchOptions(
projectId = BuildConfig.BUGWATCH_PROJECT_ID,
appSecret = BuildConfig.BUGWATCH_APP_SECRET,
environment = if (BuildConfig.DEBUG) "development" else "production",
release = BuildConfig.VERSION_NAME,
))
}
}initialize is idempotent: a second call returns the existing instance without reconfiguring. Inject credentials via buildConfigField from local.properties/CI variables, never hard-coded.
3 - Configurationlink
| Option | Default | What it does |
|---|---|---|
enabled | true | Master switch |
endpoint | https://api.newinstance.cloud | Override for dev or self-hosted backends |
environment / release | production / - | Labels; release must match symbol uploads |
sampleRate | 1.0 | Sampling for logs and handled events. Crashes, ANRs and sessions always bypass it |
autoSessionTracking | true | Release-health sessions |
enableAnrTracking / anrThresholdMs | true / 5000 | ANR watchdog; the threshold is floored to 1000 ms |
enableAutoBreadcrumbs | true | Activity and app lifecycle breadcrumbs |
enableNetworkBreadcrumbs + allow/deny host lists | true | OkHttp crumbs. Deny wins, matching is case-insensitive, *. prefix wildcards work, empty allow-list means all hosts |
sensitiveFields | built-in list | Extra keys to redact on device |
batchSize / flushIntervalMs | 50 / 5000 | Upload batching |
maxQueueSize | 1000 | Offline queue cap (see storage below) |
requestTimeoutMs | 15000 | Per-request timeout |
retry | 3 attempts, 200 ms to 5 s | Exponential backoff; a batch is dropped after the last attempt |
debug | false | Emits diagnostics, but only through a handler you install (see troubleshooting) |
4 - Capture APIlink
BugWatch.captureException(e)
BugWatch.captureMessage("Sync finished", level = Severity.INFO)
BugWatch.setUser(BugWatchUser(id = "u_123", email = "ada@example.com"))
BugWatch.setUser(null)
BugWatch.setTag("tenant", "acme")
BugWatch.setContext("payment.provider", "paystack")
BugWatch.addBreadcrumb(Breadcrumb(category = "ui", message = "Tapped checkout"))
BugWatch.setRelease("2.4.2")
BugWatch.okHttpInterceptor()
BugWatch.crashedLastRun()
BugWatch.processPendingCrash()
BugWatch.close()- Capture methods return a
bw_e_…event id; beforeinitializethe static forwarders returnnull. Severityis an enum with wire values TRACE 10, DEBUG 20, INFO 30, WARN 40, ERROR 50, FATAL 60.setContext(key, value)takes String values;addBreadcrumbtakes aBreadcrumbobject.flush()is a suspend function;flushBlocking()(no arguments) is the Java and crash-handler variant.- From Java, the static forwarders live on the companion:
BugWatch.Companion.captureMessage(...). captureWrapperException(type, value, frames, level, platform, rawStacktrace)exists for React Native and Flutter hosts; itsplatformargument overridesandroidso the backend picks the right symbolication artifact.
Automatic capturelink
- JVM crashes: default uncaught-exception handler, chaining any handler you had installed.
- NDK native crashes: a C++ signal handler (
libbugwatch-native.so, all four ABIs) writes a crash sidecar that ships on the next launch viaprocessPendingCrash(). Frames carryinstruction_addrplus the module GNU build-id for server-side symbolication. The alternate signal stack is 128 KiB (clamped to at least 2xSIGSTKSZ) so stack-overflow SIGSEGV is still captured. If the native library fails to load, JVM capture, ANR detection, sessions and delivery are unaffected. - ANRs: a watchdog reports main-thread stalls past the threshold.
- Sessions: the previous run is finalised as
crashedorexitedon the next launch, afterprocessPendingCrash()runs. - After
close()the native signal handler stays armed for the process lifetime; only the sidecar is cleared.
Breadcrumbslink
A ring buffer of the most recent 100 crumbs rides on every event. Auto categories: ui.lifecycle, app.lifecycle, network; type defaults to default. Network crumbs record method, host, path, status_code, duration_ms with query strings stripped, and the interceptor always lets the host call proceed even if its own bookkeeping throws.
Delivery and storagelink
- Offline queue: append-only NDJSON at
filesDir/cloud.newinstance.bugwatch/pending-events.ndjson, corruption tolerant. - Eviction: entries older than 7 days are dropped in addition to the
maxQueueSizecap. - Flush triggers: after every capture, on the
flushIntervalMstimer, and when connectivity returns. - The install id persists in the
cloud.newinstance.bugwatchSharedPreferences file.
5 - Symbolication (CI)link
npx @newinstance/bugwatch-cli artifacts upload app/build/outputs/mapping/release/mapping.txt \
--release "$VERSION_NAME" --platform android --type r8 --token "$BUGWATCH_CI_KEY"
(cd app/build/intermediates/merged_native_libs/release && zip -r native-symbols.zip .)
npx @newinstance/bugwatch-cli symbols upload app/build/intermediates/merged_native_libs/release/native-symbols.zip \
--platform android --release "$VERSION_NAME" --token "$BUGWATCH_CI_KEY"The two commands are strict about input: artifacts upload is for the text mapping.txt (matched on release + platform, so the string must equal options.release exactly); symbols upload is for binary .so symbols (matched by build-id) and rejects a mapping file. For Android, zip the .so tree first as shown: the CLI uploads a pre-made .zip as-is, while a bare .so file or a directory of .so files is rejected (directory scanning is dSYM-only). The key needs the symbols:upload scope. Raw-body mapping upload was retired; the CLI presign flow is the supported path.
What is never capturedlink
Platform limits, not SDK limits. No crash reporter on this platform captures them, so plan around them rather than discover them during an incident.
| Not captured | Why |
|---|---|
| Out-of-memory kills, jetsam, watchdog terminations | The OS sends SIGKILL, which no handler can intercept. The prior session finalises as exited, so crash-free rate reads slightly optimistic. There is no abnormal session status |
| The user force-quitting the app | Indistinguishable from a clean exit |
| Anything before the SDK starts | Handlers are armed by the initialiser. Start the SDK first in your launch path |
| A crash inside the crash handler itself | The signal is already being handled and the process dies |
| A stack overflow on a thread other than the one that started the SDK | sigaltstack is registered per-thread |
Delivery needs a relaunchlink
Nothing is uploaded from inside a crash handler: the process is dying and networking is not safe there. The handler writes a small artifact to disk synchronously, then the next launch turns it into a fatal event and uploads it. There is no background job that uploads after the process is gone, so a user who crashes and never returns is never counted. Queued events are delayed, never lost.
Device context on every eventlink
model, manufacturer, brand, family, osName, osVersion, sdkInt, locale, timezone, simulator, appVersion, appBuild, packageName. Alongside it every event carries an installId (stable per install), a sessionId, the release, the environment, your tags, contexts, user and the breadcrumb ring.
Production checklistlink
- Initialise in
Application.onCreate, never in an Activity. - Call
setUser(null)on sign-out. - Verify
libbugwatch-native.sois present for every ABI you ship. - Upload the R8 mapping and native symbols in the same CI job that builds the release.
Confirm the connectionlink
The SDK performs a handshake with BugWatch when it starts and exposes the result, so you never have to guess whether events are leaving the device:
BugWatch.getInstance().setConnectionListener { state ->
when (state) {
is ConnectionState.Connected -> Log.i("BugWatch", "reachable, clock skew ${state.clockSkewMs} ms")
is ConnectionState.Rejected -> Log.w("BugWatch", "refused: ${state.reason}. ${state.hint}")
is ConnectionState.Unreachable -> Log.w("BugWatch", "offline: ${state.message}")
ConnectionState.Unknown -> Unit
}
}
BugWatch.getInstance().testConnection { state -> /* same states, on demand */ }
val now = BugWatch.getInstance().connectionStateA Rejected state carries the server's exact reason and a fix hint (signature_invalid, project_unavailable, mobile_ingest_disabled, token_expired, clock_ahead), and the SDK also writes it to Logcat at WARN even with debug = false. Every delivery attempt updates the same state, so a build that suddenly starts failing shows up without any extra calls. The dashboard mirrors this: the project's setup page shows a Mobile SDK connection panel with the last accepted and last rejected handshake per platform, including the SDK version and device that made it. testConnection() ingests nothing and is not billed.
Troubleshootinglink
- No diagnostics even with
debug = true: install a handler first:BugWatchDiagnosticLog.setHandler { line -> Log.d("BugWatch", line) }. Then grep fornative crash handler armedvsnative crash handler unavailableat startup. - No events at all: call
testConnection()or readconnectionState.Rejected(signature_invalid)means the app secret does not match the project;token_expiredorclock_aheadmeans the device clock is off (tokens reject more than 60 s of skew);project_unavailablemeans the projectId is wrong or the project is inactive. The same reason appears on the dashboard's Mobile SDK connection panel. crashedLastRun()still false right after a crash: the flag flips on the launch after the crash report ships, so it reads true from the second launch onward.- Unreadable stacks: the mapping was not uploaded for this exact release string, or was uploaded after the build changed.