On this page
Android SDK
Add the dependency, call start() once, and the signals below arrive on their own. There is no event taxonomy to design first and no tracking plan to keep up to date.
Published on Maven Central · dev.drengr:analytics-android.
Before you start
| Runtime | minSdk 21 (Android 5.0) or newer, compiled against SDK 34. |
|---|---|
| Permissions | android.permission.INTERNET, declared by the SDK's own manifest and merged into yours. You do not need to add it. |
| Publishable key | Console, under Settings. It is a publishable key, safe to ship in client code: it can only append events. |
Install
implementation("dev.drengr:analytics-android:0.3.0")Start it
In Application.onCreate(). Your publishable key is safe to ship in client code: it can only write.
// build.gradle.kts: implementation("dev.drengr:analytics-android:0.3.0")val client = OkHttpClient.Builder() .addInterceptor( Drengr.start( context = applicationContext, publishableKey = "drengr_pk_YOUR_KEY", ingestUrl = "https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest", appPackage = "com.example.app", ) ) .build()What arrives on its own
You do not instrument these. They are captured from the moment start() runs. The third column names the Android API behind each one, so you can check the claim against your own app.
| Signal | What it records | Where it comes from |
|---|---|---|
| screen_view | Every screen or route the user lands on | Application.registerActivityLifecycleCallbacks, on onActivityResumed. Jetpack Navigation and Navigation-Compose can be added with Drengr.trackNavigation() |
| tap | Taps, with the element's own label when it has one | A wrapper around the activity's Window.Callback |
| net / net_fail | Outgoing requests: host, path, status, duration, size | The OkHttp Interceptor that start() returns, plus an HttpsURLConnection hook |
| crash | Uncaught exceptions with their stack | Thread.setDefaultUncaughtExceptionHandler, chaining the previous handler |
| rage_tap / dead_tap | Repeated taps that did nothing — the friction signal | The same Window.Callback wrapper as tap |
| rage_scroll / dead_scroll | Scroll thrash and scrolls that moved nothing | The same Window.Callback wrapper as tap |
| app_foreground / app_background | Lifecycle, which is what sessions are built from | Application.registerActivityLifecycleCallbacks |
| identify | Your own user id, when you choose to send one | Your own call to Drengr.identify() |
Drengr.start() returns an OkHttp Interceptor. Add it to the client your app already uses — requests made through other clients are not captured.
Screen and tap capture attach through the Application lifecycle, so no per-Activity wiring.
What never leaves the device
Emails, phone numbers, card numbers and government ids are detected and sealed on the device before the first byte is sent. Our servers hold ciphertext they cannot open. Labels and URLs are redacted and length-capped on the device too.
And what is never collected in the first place:
| Not collected | How you can tell |
|---|---|
| Screen recordings, screenshots or DOM snapshots | There is no column for pixels or markup on the events table, and no SDK reads the framebuffer. |
| Keystrokes | Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener. |
| Raw request and response bodies | What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent. |
| Authorization headers, cookies and API keys | Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names. |
| An IP address on the event | The events table has no ip or geo column, so no stored event carries one. |
| Advertising or cross-app identifiers | Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity. |
Most autocapture tools promise not to look. This is built so we cannot. Full design in Security and Privacy.
Consent and opt-out
Start paused behind a consent gate with Drengr.start(startEnabled = false), then resume once the user agrees. A persisted opt-out always wins over that argument, so an opted-out install stays paused across restarts.
| Start paused | Drengr.start(startEnabled = false) |
|---|---|
| Opt out | Drengr.optOut() |
| Opt back in | Drengr.optIn() |
| Where the choice is stored | SharedPreferences file drengr_sdk, key opt_out |
Retention, deletion and what a GDPR or CCPA request maps to are on Privacy controls.
Check it worked
Run the app and use it for a few seconds, then open the console. Overview checks for your first event every 15 seconds on its own, so you do not need to keep reloading.
Nothing showing up
The first five happen on every platform. The rest are specific to Android.
| What you see | Why | Fix |
|---|---|---|
| No events at all, and Overview never leaves its empty state | The key belongs to a different organisation, so the writes are accepted against a tenant you are not looking at. | Copy the key again from Settings in the organisation you have open in the console. |
| Events exist but your app is missing from the picker | app_package does not match the scope you are viewing. | Use your real bundle or package id, the same string on every launch. A display name will create a second app. |
| Events appear only after you close the app | Not a bug. The queue batches in memory and flushes on background, which is what makes a dropped connection free. | Background the app once, or wait for the next flush. Nothing is lost in the meantime. |
| Screens are empty but taps and network arrive | Screen capture is the one signal that can be wired separately from start(). | Check the screen hook for your platform in the table above. |
| Nothing arrives after a user opted out | Working as intended. A persisted opt-out outranks the start argument and survives restarts. | Call the opt-in method for your platform. See Privacy controls. |
| Screens and taps arrive, network does not | start() returns an interceptor that has to be added to the OkHttp client your app actually uses. | Add the returned DrengrInterceptor to every OkHttp client you build. Requests through other clients are not captured. |
| optOut() appeared to do nothing | It writes through the application context that start() captures, so it is a no-op before start() runs. | Gate with start(startEnabled = false) and call optIn() once the user agrees. |
| Compose screens all report the same name | Activity lifecycle sees one Activity; Compose destinations need the navigation hook. | Pass your NavController to Drengr.trackNavigation() to get per-destination screen names. |
Where to go next
Once events are arriving, these are the things worth doing, in order.
- Wire up consent and opt-outThe gate, the persisted opt-out, the 180-day window and the deletion call.
- Take the event inventory as JSONThe same signals, hooks and exclusions as a machine-readable contract for your pipeline or review.
- Bring in what you already havePoint an existing Amplitude or Segment pipe at us, or import your history.
- Drengr ActuatorThe separate MCP server that gives an agent eyes and hands on a real device.
Not ready to install anything?
Drop a .har of traffic you already have and see the business events your current analytics is missing. It runs in your browser; nothing is uploaded.
Scan for blind spots →