Skip to main content
Supported on: Android SDK 1.0.33+
The AppDNA SDK is designed to work reliably in offline and low-connectivity environments. Configuration is cached locally, events are persisted to disk, and a bundled fallback config ensures the SDK functions even on first launch without network access.

Config Resolution Priority

The SDK resolves configuration using a three-tier priority system:
The SDK uses a stale-while-revalidate pattern. Cached config is served immediately, whatever its age, while a fresh copy is fetched from the remote source in the background. If that fetch fails, the cached config stays in use.

Config TTL

The cached configuration has a time-to-live (TTL) that defaults to 3600 seconds (1 hour). You can customize this value in AppDNAOptions:
When the TTL expires, the SDK fetches a fresh config from Firestore on the next access. The stale config continues to be used until the fresh config is received.

Config Update Notifications

Listen for config updates by collecting the AppDNA.configUpdated coroutine flow:
AppDNA.configUpdated is a SharedFlow<Unit> that emits whenever the SDK applies a new remote config bundle (the fetch at configure, a TTL refresh, the 15-minute background refresh, or refresh() / AppDNA.forceRefreshConfig()). It mirrors iOS’s Notification.Name(AppDNA.configUpdated) and Flutter/RN bridge events.

Cache Keys

The SDK persists configuration data in SharedPreferences under the namespace ai.appdna.sdk. Per-module keys are isolated so partial failures (e.g., paywall config update mid-fetch) cannot leak state across modules:

Bundled Config

For first-launch scenarios or environments with no network access, include a static configuration file in your app assets:
  1. In the AppDNA Console, open Settings → SDK → Config Bundle. The bundle is regenerated automatically shortly after each publish (usually within about 20 seconds); click Regenerate only if you need it at once.
  2. Click JSON Only. It saves appdna-config.json in the form the SDK reads. To download it in CI instead, see Config Bundles in CI/CD.
  3. Place the file at src/main/assets/appdna-config.json.
Access the current bundled config version programmatically:
The bundled config is a snapshot and will become stale over time. Always ensure the SDK can reach the remote config source for the latest configuration. The bundled config should be treated as a fallback only.

Event Queue

Events tracked with AppDNA.track(...) are not sent immediately. They are queued locally and flushed in batches.

Queue Behavior

Events are flushed on the timer, when the app goes to the background or returns to the foreground, and when the queue reaches the batch size (the adaptive size below, capped by batchSize when set) — whichever comes first. You can also call AppDNA.flush() to trigger an immediate flush.

Persistence

Events are persisted to disk immediately when tracked. This ensures that:
  • Events survive app crashes and force quits.
  • Events survive app restarts and device reboots.
  • Events are delivered on the next successful flush after connectivity is restored.
The event queue is stored in a local SQLite database under the SDK’s private namespace. Events stay queued until they are delivered or dropped: a batch rejected with a 4xx other than 408 or 429, the oldest events beyond 10,000 events or 5 MB, events older than 7 days, and every queued event when analytics consent is revoked. See When Events Are Dropped. Apps upgrading from older SDK versions automatically migrate legacy SharedPreferences-backed event queues on first launch. Going back to an older SDK keeps the queued events only when that version is 1.0.54 or later; it rebuilds a table it cannot use and counts those events as dropped. Going back to 1.0.53 or earlier, the SDK cannot open the newer event database, so events are not recorded until the app is updated again.

Adaptive Batch Sizing

The SDK automatically adjusts batch size based on network conditions reported by ConnectivityManager.NetworkCapabilities:
  • Wi-Fi or Ethernet: 100 events per batch, even when the network is metered
  • Metered cellular (most mobile plans): 20 events per batch
  • Unmetered cellular: 50 events per batch
With batchSize set, the batch size is the smaller of this size and batchSize.

Event Storage Limits

Events are stored locally with the following limits:
  • Maximum 10,000 events in queue
  • Maximum 5 MB of queued events: each event’s JSON plus one byte, the same measure as iOS. The SQLite database file is larger — it also holds its indexes and free pages
Oldest events are dropped when limits are exceeded — over 5 MB, the oldest 10 % at a time until the rest fit — and every dropped event is counted. An event older than 7 days is dropped unsent.

Networking

OkHttp Timeouts

The SDK uses OkHttp3 for all HTTP requests with the following timeout configuration:

Retry Strategy

When a flush attempt fails, the SDK retries with exponential backoff: After 3 failed attempts, the request is abandoned and events remain in the queue for the next flush cycle.

Error Handling

Events that receive a 4xx response other than 408 or 429 are discarded permanently. This prevents malformed events from blocking the queue. Check your event schemas if you see 4xx errors in the SDK logs.

Circuit Breaker

After 5 consecutive upload failures, event uploads are paused: the timer, the batch size and backgrounding do not start an upload (backgrounding schedules no OS background upload, and one already scheduled uploads nothing while paused). Returning to the foreground or calling AppDNA.flush() resets the failure count and resumes uploads. A single failure does not pause uploads, except a 401 or 403 (an invalid or revoked API key): every later batch would be rejected the same way, so it pauses uploads at once, in the in-app queue and the OS background upload alike. Every other failed flush counts toward the consecutive-failure total — whether a 4xx batch was dropped or a 5xx/network error left the batch queued for retry — and only the 5th failure in a row trips the circuit breaker. An OS background upload ends its run after a rejected batch; its next run sends the events behind it.

Request Headers

All requests include the following headers: When analytics consent is set to false:
Events are silently dropped and are not added to the queue. No data is stored or transmitted. When consent is later granted, only events tracked after that point are queued and sent.
Consent state is persisted in SharedPreferences. If a user revokes consent and restarts the app, the SDK remembers the revoked state and continues to drop events until consent is explicitly granted again.

Secure Storage

The following data is stored using Android’s EncryptedSharedPreferences (Android Keystore-backed) for security: EncryptedSharedPreferences uses AES-256-GCM via the Android Keystore, so these values are protected by hardware encryption on supported devices.

Device Identification

The SDK generates an anonymous identifier as a random UUID on first launch and persists it in secure storage. It is not derived from Settings.Secure.ANDROID_ID or any device hardware identifier. The anonymous ID is preserved across AppDNA.reset() — reset() clears only the identified user ID and user traits, leaving the anonymous ID (and its event attribution) intact.

Coroutine Scopes

The SDK uses Kotlin coroutines with the following dispatcher assignments:
All SDK operations are non-blocking. Network requests and persistence operations run on the IO dispatcher, while UI-related callbacks (billing flows, paywall/onboarding rendering) are dispatched on the Main thread.

Summary

Next Steps