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
  • INTERNET and ACCESS_NETWORK_STATE permissions 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

OptionDefaultWhat it does
enabledtrueMaster switch
endpointhttps://api.newinstance.cloudOverride for dev or self-hosted backends
environment / releaseproduction / -Labels; release must match symbol uploads
sampleRate1.0Sampling for logs and handled events. Crashes, ANRs and sessions always bypass it
autoSessionTrackingtrueRelease-health sessions
enableAnrTracking / anrThresholdMstrue / 5000ANR watchdog; the threshold is floored to 1000 ms
enableAutoBreadcrumbstrueActivity and app lifecycle breadcrumbs
enableNetworkBreadcrumbs + allow/deny host liststrueOkHttp crumbs. Deny wins, matching is case-insensitive, *. prefix wildcards work, empty allow-list means all hosts
sensitiveFieldsbuilt-in listExtra keys to redact on device
batchSize / flushIntervalMs50 / 5000Upload batching
maxQueueSize1000Offline queue cap (see storage below)
requestTimeoutMs15000Per-request timeout
retry3 attempts, 200 ms to 5 sExponential backoff; a batch is dropped after the last attempt
debugfalseEmits 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; before initialize the static forwarders return null.
  • Severity is an enum with wire values TRACE 10, DEBUG 20, INFO 30, WARN 40, ERROR 50, FATAL 60.
  • setContext(key, value) takes String values; addBreadcrumb takes a Breadcrumb object.
  • 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; its platform argument overrides android so 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 via processPendingCrash(). Frames carry instruction_addr plus the module GNU build-id for server-side symbolication. The alternate signal stack is 128 KiB (clamped to at least 2x SIGSTKSZ) 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 crashed or exited on the next launch, after processPendingCrash() runs.
  • After close() the native signal handler stays armed for the process lifetime; only the sidecar is cleared.

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 maxQueueSize cap.
  • Flush triggers: after every capture, on the flushIntervalMs timer, and when connectivity returns.
  • The install id persists in the cloud.newinstance.bugwatch SharedPreferences 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 capturedWhy
Out-of-memory kills, jetsam, watchdog terminationsThe 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 appIndistinguishable from a clean exit
Anything before the SDK startsHandlers are armed by the initialiser. Start the SDK first in your launch path
A crash inside the crash handler itselfThe signal is already being handled and the process dies
A stack overflow on a thread other than the one that started the SDKsigaltstack 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.so is 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().connectionState

A 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 for native crash handler armed vs native crash handler unavailable at startup.
  • No events at all: call testConnection() or read connectionState. Rejected(signature_invalid) means the app secret does not match the project; token_expired or clock_ahead means the device clock is off (tokens reject more than 60 s of skew); project_unavailable means 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.