---
url: https://simpledatalogs.com/docs/sdks/android.md
description: >-
  The simpledatalogs Android and Kotlin SDK; install, configure, track,
  autocapture, the opt-out, WorkManager, the JVM server client, the crash-safe
  outbox and caps.
---

# Android

The SimpleDataLogs Android SDK tracks events from Android apps in Kotlin or Java. It has no dependencies besides the Kotlin standard library, and keeps a crash-safe outbox on disk, so an event is never lost to a crash or a killed process.

* Android 5.0+ (minSdk 21).
* Two artifacts: `com.xkdlabs:simpledatalogs` (the Android library) and `com.xkdlabs:simpledatalogs-core` (pure Kotlin/JVM, for servers).
* No AndroidX, no OkHttp, no JSON library.

## Install

In your app module's `build.gradle.kts`:

```kotlin
dependencies {
    implementation("com.xkdlabs:simpledatalogs:0.1.0")
}
```

It comes from Maven Central. The library's manifest adds the `INTERNET` permission.

::: info Early access
The library isn't on Maven Central yet. Until it is, build it from GitHub into your local Maven repository:

```sh
git clone https://github.com/XKD-Labs/simpledatalogs-android.git
cd simpledatalogs-android
./gradlew publishToMavenLocal
```

Then add `mavenLocal()` to your repositories in `settings.gradle.kts`, and keep the `implementation` line above.
:::

## Quick start

```kotlin
import com.xkdlabs.simpledatalogs.SimpleDataLogs

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // The opt-out first, then identity and default properties, then the key.
        SimpleDataLogs.setEnabled(userAllowsUsageStatistics)
        SimpleDataLogs.setDeviceId(anonymousDeviceId)
        SimpleDataLogs.setDefaultProperties(mapOf("app_version" to BuildConfig.VERSION_NAME))
        SimpleDataLogs.configure(this, ingestKey = "sdl_pub_…")
    }
}

// Anywhere, from any thread; it returns at once.
SimpleDataLogs.track("level_complete", mapOf("level" to 3, "is_pro" to true))
```

Register `MyApplication` with `android:name` on the `<application>` tag. Use a **public key** (`sdl_pub_…`) in apps.

From Java, every method is static: `SimpleDataLogs.track("signup", Collections.singletonMap("plan", "pro"));`.

## Autocapture

One argument sends launches, sessions, crashes and screen views without any `track` call. It is off until you pass it.

```kotlin
SimpleDataLogs.configure(this, ingestKey = "sdl_pub_…", autocapture = Autocapture.ALL)
```

| Option | What you get |
|---|---|
| `Autocapture.APP_LAUNCHES` | `app_launched` once per process start that shows UI, with `is_first_launch` on the very first |
| `Autocapture.SESSIONS` | `session_start`, a `session_id` on every event, and `session_end` (`duration_s`) once the app has been in the background 30 minutes |
| `Autocapture.CRASHES` | An uncaught exception is saved at the crash and sent as `app_crashed` (`exception_name`, `reason`, `stack`) on the next launch; your crash reporter still runs |
| `Autocapture.SCREEN_VIEWS` | `screen_view` with `screen` = the activity's class name each time an activity resumes |

`Autocapture.ALL` is all four; combine with `+` to pick, e.g. `Autocapture.SESSIONS + Autocapture.CRASHES`.

* **Screen views by hand:** `SimpleDataLogs.trackScreen("Settings")`. Use it for Compose destinations and fragments, which activity callbacks can't see.
* A rotation is not a trip to the background, so it neither flushes nor touches the session.
* Native (NDK) crashes and ANRs are not captured.
* The opt-out applies: with `setEnabled(false)`, nothing is captured and a saved crash is discarded.

## Active and background

Every event tracked now says whether someone was using the app, so the [Sessions tab](/guide/web-analytics) can tell active sessions from passive ones:

* `app_state` is `active` while one of your activities is started, else `background` (a worker, a service, a receiver).
* `engaged_ms` is the time in front since the previous event.
* Mark background work yourself with `track("sync_finished", mapOf("app_state" to "background"))`; your value wins.
* `configure(..., activity = ActivityReporting.OFF)` sends neither. The plain JVM client never does.

## API

| Call | What it does |
|---|---|
| `configure(context, ingestKey, endpoint, autocapture)` | Starts the SDK once; `autocapture` defaults to `Autocapture.NONE`, `endpoint` to `https://edge.simpledatalogs.com`. Later calls are ignored with a warning |
| `track(eventName, properties)` | Records one event. `event_id` and `ts` are stamped now, when the event happens |
| `trackScreen(screenName, properties)` | Records `screen_view` with a `screen` prop |
| `setDeviceId(deviceId)` | Sets, or clears with `null`, the `device_id` on every later event |
| `setDefaultProperties(properties)` | Replaces the properties merged into every event; the event's own properties win on a clash |
| `setEnabled(isEnabled)` | The opt-out. `false` ignores new events and **deletes every unsent event**; `true` collects again |
| `flush()` | Starts sending everything now, without waiting |
| `flushBlocking(timeoutMillis)` | Sends everything and waits; `true` when nothing is left. On the main thread it returns `false` at once |
| `installReferrer(context, callback)` | Reads the Play Store install referrer once and calls back on the main thread with it, or `null`. Needs `com.android.installreferrer:installreferrer:2.2` in your app |

Property values are `String`, `Int`, `Long`, `Short`, `Byte`, `Float`, `Double` or `Boolean`. Anything else, and NaN or infinity, is dropped with a log line.

## The opt-out

Call `setEnabled` **before** `configure`. A user who opted out then never sends a byte, and whatever an earlier launch left on disk is deleted instead of sent.

The SDK doesn't persist the choice. Your app's setting is the source of truth, so pass it on every launch.

## Installs from shared links

To learn which shared link led to an install, add `com.android.installreferrer:installreferrer:2.2` to your app, then on the first launch:

```kotlin
SimpleDataLogs.installReferrer(context) { installReferrer ->
    SimpleDataLogs.track("app_installed", mapOf("install_referrer" to installReferrer))
}
```

A `null` referrer is left out of the props, so the event still counts. See [Share links and installs](/guide/share-links).

## Sending from WorkManager

The SDK flushes when the app goes to the background and resends leftovers on the next launch. To also deliver while the app stays closed, flush from a worker (add `androidx.work:work-runtime-ktx` yourself):

```kotlin
class FlushEventsWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
    override fun doWork(): Result {
        return if (SimpleDataLogs.flushBlocking(20_000)) Result.success() else Result.retry()
    }
}
```

Schedule it as unique periodic work, for example every 6 hours with a network constraint.

## Servers and other JVM apps

`simpledatalogs-core` is pure Kotlin/JVM (Java 8 bytecode) with the same outbox and rules, and sends every 5 seconds:

```kotlin
dependencies {
    implementation("com.xkdlabs:simpledatalogs-core:0.1.0")
}
```

```kotlin
import com.xkdlabs.simpledatalogs.SimpleDataLogsClient

val analytics = SimpleDataLogsClient(
    ingestKey = System.getenv("SIMPLEDATALOGS_KEY"),
    storageDirectory = File("/var/lib/myapp/simpledatalogs"),
)

analytics.track("invoice_paid", mapOf("plan" to "pro", "amount_cents" to 4_900))

// On shutdown: a last flush of up to 5 s; undelivered events stay on disk.
Runtime.getRuntime().addShutdownHook(Thread { analytics.close() })
```

Use a secret key (`sdl_sec_…`) on servers, and give each process its own `storageDirectory`. The constructor also takes `endpoint` and `batchIntervalMillis` (5,000 by default); `shutdown(timeoutMillis)` is `close()` with your own flush limit.

## Privacy

* The SDK sends only the events you track and the autocapture you turn on. It reads no advertising id, Android id, location, contacts or installed apps.
* `device_id` is sent only if you set one; use an anonymous id you generate.
* The outbox lives in `noBackupFilesDir/simpledatalogs/`, so Auto Backup never copies unsent events.
* Crash reports hold the exception class, its message and up to 40 stack frames. If messages can hold personal data, leave `CRASHES` off.

## How it behaves

### Batching

Events go out in batches of 50, or 10 seconds after the first event of a partial batch (5 seconds while the app is debuggable). Every request stays under 256 KB, and bodies of 1 KB or more are gzipped. `track` hands the event to one background thread; disk and network never run on the caller's thread.

### A crash-safe outbox

* Each event is appended to a journal on disk as it is tracked.
* Batches are sealed into files and deleted only after a `202`.
* The next launch resends anything left over, including a journal cut short by a crash.
* A batch file that fails to read three times in a row is renamed `*.unreadable` and skipped.

### Errors and offline

| Answer | What the SDK does |
|---|---|
| `202` | Deletes the batch file |
| `401` | Pauses sending and logs one configuration error; nothing is lost. Re-probes after 1 minute, then every 10 minutes, and on the next launch |
| `413` | Splits the batch in half and resends. A single event still too large is dropped, with a log line |
| `415` | Sets the batch aside as `*.unsupported` and moves on |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, honoring `Retry-After` |

### Caps

* The outbox keeps at most **10,000 events, 10 MB or 500 batch files**. Past that, the oldest batches are dropped, with a log line.
* If more than 5,000 events wait to be written, new ones are skipped, with one log line.

No public method throws, and nothing blocks your app.

## Troubleshooting

The SDK logs to Logcat under the tag `simpledatalogs`:

```sh
adb logcat -s simpledatalogs
```

To send to a local ingest from the emulator:

```kotlin
SimpleDataLogs.configure(this, ingestKey = "sdl_pub_…", endpoint = "http://10.0.2.2:4001")
```

Plain `http` needs a network security config that allows cleartext to that host.

## Next steps

* [Android quickstart](/quickstarts/android): the shortest path.
* [Core concepts](/guide/concepts#deployments): one deployment per build, such as `play` and `beta`.
* [Privacy for senders](/reference/privacy): device ids, not emails.
