Skip to Content
StreamsAndroid SDK

Android SDK

The Zeotap Android SDK (io.github.zeotap:android-sdk) captures events from your Android app and sends them to the ingest API. It is the Android counterpart of the Browser SDK: the same event model, the same consent model, the same on-device identity hashing and the same wire format — proven identical by a shared conformance suite generated from the browser SDK — plus the things a mobile app needs that a web page does not: a durable offline queue, flush on background, device/app context and app-lifecycle events.

Kotlin, minSdk 23, zero UI, three small runtime dependencies (Kotlin stdlib, coroutines, kotlinx-serialization-json).

Install

// settings.gradle.kts dependencyResolutionManagement { repositories { mavenCentral() } } // app/build.gradle.kts dependencies { implementation("io.github.zeotap:android-sdk:0.1.0") }

Optional: add com.google.android.gms:play-services-ads-identifier to your app if you enable collectAdvertisingId — the SDK never bundles it and degrades gracefully when it is absent.

Initialise

Call Zeotap.load once, as early as possible (typically in Application.onCreate). Later calls return the existing instance.

class MyApp : Application() { override fun onCreate() { super.onCreate() val config = Configuration.Builder("https://events.zeotap.com") .consent(ConsentOptions.Builder().require(true).build()) .trackApplicationLifecycleEvents(true) .build() Zeotap.load(this, "YOUR_WRITE_KEY", config) } }

Replace YOUR_WRITE_KEY with the key from Streams > Event Sources in your workspace. apiHost is required (there is no “same origin” on a device); point it at the ingest host or at a first-party proxy on your own domain.

Configuration

Option names and units match the browser SDK’s LoadOptions, so a React Native app passes one options object to both platforms.

OptionTypeDefaultDescription
apiHoststring—Ingest base URL (required)
namespacestring"zeotap"Storage-key prefix ({namespace}_anonymous_id, …). Shared with the browser SDK so a WebView and the native layer can agree on identity
flushAtint10Send after this many queued events
flushIntervalMslong5000Send every N milliseconds
debugbooleanfalseVerbose logcat output (tag ZeotapSDK)
nativeAppIdstring—Snowflake Native App install id
consentConsentOptionsattach-onlyOpt-in gating, defaults, TCF — see below
integration(name, version)——Identity of a wrapping integration → context.library.integration (the React Native bridge sets this)
trackApplicationLifecycleEventsbooleanfalseEmit Application Installed / Updated / Opened / Backgrounded
collectAdvertisingIdbooleanfalseCollect the Google Advertising ID (honours Limit Ad Tracking; requires the Play Services artifact in the host app, or supply the id yourself with setAdvertisingId — see below)
maxQueueSizeint500Durable queue capacity; the oldest event is dropped beyond it
flushOnBackgroundbooleantrueSend queued events when the app moves to the background
logger / storage—logcat / SharedPreferencesInject your own log sink or (e.g. encrypted) key-value store

Events

Every method is safe to call from any thread; work is posted to the SDK’s own thread in call order. Optional Callbacks fire on that thread with null on success or a Throwable describing why the call was dropped.

Zeotap.track("Order Completed", mapOf("order_id" to "o-1", "total" to 99.5, "currency" to "USD")) Zeotap.screen("Product", category = "Shop", properties = mapOf("sku" to "A-1")) Zeotap.identify("user-42", mapOf("plan" to "pro")) Zeotap.group("acme", mapOf("plan" to "enterprise")) Zeotap.alias("user-42") // previousId defaults to the anonymous id Zeotap.flush() Zeotap.reset() // logout: new anonymous id, identities/consent/queue cleared

There is no page() on Android — screen() is the mobile primitive. Property maps accept strings, numbers, booleans, null, nested maps and lists; whole doubles serialise as integers (1.0 → 1), as in JavaScript.

User identities

Zeotap.setUserIdentities(mapOf( "email" to "Shopper@Example.com", // → sha256/sha1 × lowercase/uppercase "cellno" to "91 9999999999", // → cellno {sha256, sha1} + cellno_e164 (cc+number) "fpuid" to "crm-123", // first-party ids pass through unhashed )) Zeotap.setUserIdentities(mapOf("email_sha256_lowercase" to "…"), hashed = true) // pre-hashed input Zeotap.unsetUserIdentities(listOf("email"))

Raw email / phone / login values are normalised and hashed on the device and never leave it. The hashed map persists and rides on every later event as context.identities, exactly like the browser SDK. Setting identities is gated by the identify consent signal; unsetting never is.

ConsentOptions mirrors the browser SDK — see Consent management for the model. With the defaults, events flow immediately and consent is merely attached to payloads. Gating activates with require(true) (opt-in) or default(ConsentDefault.DENIED) (opt-out-by-default).

ConsentOptions.Builder() .require(true) // buffer up to maxBufferedEvents until consent resolves .cmpTimeoutMs(10_000) // then fall back to `default` .cmp(CmpMode.AUTO) // read a CMP's IAB TCF-for-Apps preferences (default when require=true) .purposesForTracking(listOf(1, 3, 4)) .build() Zeotap.setConsent(ConsentInput.Parsed(analytics = true, advertising = false)) Zeotap.setConsent(ConsentInput.Tcf("CQ…")) Zeotap.setConsent(ConsentInput.UsPrivacy("1YNN")) val state = Zeotap.getConsentState() // resolved, signals, source, gdprApplies

Advertising id

With collectAdvertisingId = true the SDK resolves the Google Advertising ID itself and honours Limit Ad Tracking — nothing else to do, provided play-services-ads-identifier is in your app.

If your app already has the id (its own lookup, or a mediation SDK), or deliberately ships without that dependency, supply it instead:

Zeotap.setAdvertisingId(gaid, limitAdTracking = false) Zeotap.setAdvertisingId(null, limitAdTracking = true) // withdraw it

A value you supply always wins, and the SDK’s own lookup is then not attempted at all — it is a blocking inter-process call. limitAdTracking = true withholds the id and reports adTrackingEnabled: false, which is the point of reporting it.

collectAdvertisingId still gates everything: it is your single switch for whether an advertising id may be collected, so one call does not override it. When it is false the SDK logs a warning rather than silently ignoring you.

TCF v2 on Android means the IAB TCF for Apps  keys your CMP SDK writes to the app’s default SharedPreferences (IABTCF_CmpSdkID, IABTCF_gdprApplies, IABTCF_TCString, IABTCF_PurposeConsents, IABTCF_VendorConsents). With cmp = AUTO the SDK detects a CMP within cmpTimeoutMs, translates purposes and the Zeotap vendor bit (GVL 1469, legacy 301) with the same rules as the browser SDK, attaches the TC string to every event, and re-resolves when the CMP updates the preferences. Resolved decisions persist across launches (persist, expiryDays).

Context

Every event carries, in addition to the browser fields:

BlockFields
context.appname, version, build, namespace
context.devicemanufacturer, model, type: "android", advertisingId / adTrackingEnabled (only with collectAdvertisingId)
context.osname: "Android", version
context.networkwifi, cellular, carrier
context.screenwidth, height, density
context.locale, context.timezone, context.userAgentBCP-47 tag, IANA zone id, the system HTTP agent

Privacy defaults: no ANDROID_ID, no Wi-Fi IP, no device name, no READ_PHONE_STATE permission.

Lifecycle events

With trackApplicationLifecycleEvents(true) the SDK tracks Application Installed {version, build}, Application Updated {version, build, previous_version, previous_build}, Application Opened {from_background, version, build} and Application Backgrounded. They go through the consent gate like any other event.

Delivery & offline

Events are written to a SQLite queue before the call returns, so nothing is lost if the process dies. A batch is reserved while in flight and acknowledged on a 2xx; a batch still reserved at the next launch is re-sent (at-least-once — downstream dedupes on messageId). Retry policy is the browser SDK’s: 4 attempts with 500/1000/2000 ms back-off on 5xx, 429 and network errors; any other 4xx drops the batch. After exhausted retries the batch stays queued and the SDK backs off (30 s → 5 min) until the network returns or the app comes to the foreground.

Ingest answers 200 even on partial failure. Register an SdkListener to see BatchResult.success / errors, delivery failures and dropped events:

Zeotap.getInstance().addListener(object : SdkListener { override fun onBatchResult(result: BatchResult) { if (!result.success) Log.w("cdp", result.errors.toString()) } override fun onEventDropped(type: String, reason: DropReason, message: String) { … } })

ProGuard / R8

Consumer rules ship inside the AAR; no configuration is needed.

Troubleshooting

  • 401 write key not found — the key is wrong or revoked; the SDK drops the batch and reports onDeliveryFailure(401, …).
  • Events accepted but nothing downstream — a write key in pending_schema status returns 200 and publishes nothing until its schema is reviewed in Streams > Event Sources.
  • Let’s Encrypt proxies on Android 6/7 — API 23–24 lack the ISRG Root X1 certificate; a first-party proxy on such a certificate fails on those devices while the Zeotap-hosted ingest works.
Last updated on