# Android

Native Android SDK, Kotlin-first with full Java interop: JVM crashes, NDK native crashes, ANRs, release-health sessions, breadcrumbs and logs.

## Requirements

- 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. Install

```kotlin
dependencies {
    implementation("cloud.newinstance:bugwatch:0.1.2")
}
```

## 2. Initialise (Application.onCreate, not an Activity)

```kotlin
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. Configuration

| Option | Default | What it does |
|--------|---------|--------------|
| `enabled` | true | Master switch |
| `endpoint` | `https://api.newinstance.cloud` | Override for dev or self-hosted backends |
| `environment` / `release` | production / none | 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 API

```kotlin
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 capture

- **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.

## Breadcrumbs

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 storage

- 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)

```bash
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 captured

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 relaunch

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 event

`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 checklist

- 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 connection

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:

```kotlin
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.

## Troubleshooting

- **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.
