Postlip Metrics for Android
Native Android SDK of Postlip Metrics. It speaks the Segment Tracking API Spec (see docs/api/tracking-api.md): screens, product events, identity and consent, plus automatic sessions, app lifecycle events, install attribution from Google Play, and crash and ANR capture. Kotlin with a Java-friendly facade, minSdk 21, one dependency (the Google Play Install Referrer library). It sends no device id and reads no IP address.
This page is generated at build time from packages/sdk-android/README.md in the repository.
Install
// build.gradle.kts of the app
dependencies {
implementation("com.postlip:metrics-android:0.1.0")
}The library declares only INTERNET. Two optional additions in the app:
Quick start
class App : Application() {
override fun onCreate() {
super.onCreate()
PostlipMetrics.load(this, "pk_live_…", PostlipMetricsOptions(trackScreens = true))
}
}
PostlipMetrics.screen("ProductDetail", mapOf("sku" to "KUPA-1"))
PostlipMetrics.identify("usr_7c1e", mapOf("email" to "deneme@example.com", "plan" to "pro"))
val id = PostlipMetrics.track("purchase_completed", mapOf("order_id" to "HP9f2c", "value" to 240, "currency" to "TRY"), "purchase:HP9f2c")
PostlipMetrics.consent(analytics = true, marketing = false)Java:
PostlipMetrics.load(this, "pk_live_…", PostlipMetricsOptions.builder().trackScreens(true).build());
String id = PostlipMetrics.track("purchase_completed", props, "purchase:HP9f2c");
PostlipMetrics.consent(true, null);Call load() in Application.onCreate: the cold start, the first activity and early crashes are then seen. example/ is a runnable app (Activities built in code, a local collector at 10.0.2.2:7110).
API
All calls are non-blocking and thread-safe and never throw into the app. Calls made before load() or before the remote config arrived (at most 3 s) are buffered, at most 200 (the oldest dropped), and sent with their call time as timestamp.
Properties and traits take JSON values: strings, numbers, booleans, null, maps, lists and arrays. Dates and calendars become ISO 8601 strings, NaN and Infinity become null, other objects their toString(); org.json objects keep their structure. A message over 32 KB is dropped and logged.
Options
PostlipMetricsOptions (Kotlin named arguments, or PostlipMetricsOptions.builder() from Java). Options override what the panel configured.
Of the remote config's autocapture switches, pageviews: false turns automatic screen views off (even with trackScreens), pageleave: false turns screen leave off, errors: false turns crash and ANR capture off. A test source (test: true) marks every message context.test.
Consent
analyticscontrols persistence (ids, user id, session, the offline queue, lifecycle markers, pending crash reports),$first_open/$app_updated, crash and ANR capture and screen leave ($pageleave).marketingcontrols the advertising id and the click ids in the install referrer.- The decision itself is stored and wins over the mode's default at the next start. Without analytics consent it is the only thing stored.
- Granting analytics keeps the same anonymous id (earlier events join), stores the in-memory state and queue, then runs the first-open / update checks and installs crash capture. Revoking it deletes everything stored except the decision; the in-memory ids and queue stay for the rest of the process. Revoking marketing stops the advertising id at once.
- Every message carries
context.consent.
Automatic capture
Sessions follow the foreground: a new session starts on the first message, after reset(), and when the app comes to the foreground after more than sessionTimeoutMinutes in the background (or since the last message sent in the background, e.g. from a push handler). While the app is in the foreground a session never expires. The first message of a session carries context.session.start: true. With trackScreens, a new session at foreground sends the current screen again. Every message carries context.app, device (type, manufacturer, model), os, screen, locale, timezone, userAgent, session and consent.
Install attribution
$first_open carries context.install:
{ "installer": "google_play",
"referrer": "utm_source=google&utm_medium=cpc&utm_campaign=spring&gclid=Cj0KCQ",
"referrerClickAt": "2026-11-10T09:10:00.000Z", "installBeginAt": "2026-11-10T09:11:00.000Z" }referreris the Google Play Install Referrer string as returned. Without marketing consent at the moment the event is queued, its click-id parameters (gclid,gbraid,wbraid,dclid,fbclid,msclkid,ttclid,li_fat_id,yclid) are removed; theutm_*stay.installerisgoogle_playfor installs from the Play Store, else the installer's package name; left out when unknown.- The referrer is read once per install, only with analytics consent, and never blocks
load().
Crashes and ANRs
With analytics consent and captureCrashes, the SDK installs a default uncaught exception handler that writes the crash to a file synchronously and then hands it to the previously installed handler (the crash is never swallowed; other crash reporters keep working). The next launch sends it as $crash with the crashed process's own timestamp, anonymous id, user id and session. With captureAnr, a watchdog notices the main thread being blocked for 5 s or more while the app is in the foreground (not while a debugger is attached) and queues a $crash with kind: "anr" at once.
{ "path": "/Checkout", "kind": "crash", "fatal": true, "type": "java.lang.IllegalStateException",
"message": "Payment state [redacted]", "stack": "at com.example.Checkout.pay(Checkout.kt:42)\n…",
"stack_hash": "<32 hex>", "thread": "main", "app_version": "1.4.0", "app_build": "140" }pathis the current screen's path, else/.stackholds the top frames first and oneCaused by, at most 4000 characters.- Before anything is written,
messageandstackare redacted on the device: e-mail addresses and runs of 7+ digits become[redacted], URL query strings are removed, absolute user paths become~/. The server scrubs again. stack_hashis SHA-256 (first 32 hex characters) of the exception type and each frame's class and method, without line numbers or the message, so the same crash groups across launches and builds.
Privacy
- No
device.id: no ANDROID_ID and no other hardware id. The advertising id is sent only with marketing consent and when the user has not limited ad tracking. - The SDK reads no IP address. The server sees the request's IP address and user agent and stores neither with the event (Tracking API §5, §9).
- The user agent is
PostlipMetrics/0.1.0 (Linux; Android <version>; Mobile)(Mobileon phones only). It never carries the app's name, package or the device model. - Storage lives in
noBackupFilesDir/postlip-metrics/, which is never restored to another device. Without analytics consent only the consent decision is stored. - Traits are never stored by the SDK; crash reports are redacted before they touch the disk.
Transport
Messages are queued and sent as POST {host}/v1/batch with Authorization: Basic base64("<writeKey>:"), a JSON body { batch, sentAt } (sentAt set at each attempt, so the server can correct the device clock) and at most flushAt (≤ 20) messages and 512 000 bytes per request, one request at a time. A send happens when the queue reaches flushAt, after flushInterval, when the app goes to the background, on flush() and once after load(). With analytics consent the queue is stored (one file per message, written atomically off the main thread) and survives restarts and offline periods; it is bounded by maxQueueSize and messages older than 30 days are dropped.
The backoff resets on success and when the app returns to the foreground. Timeouts: connect 10 s, read 20 s.