Appearance
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) andcom.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.
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 publishToMavenLocalThen 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 can tell active sessions from passive ones:
app_stateisactivewhile one of your activities is started, elsebackground(a worker, a service, a receiver).engaged_msis 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.
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_idis 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
CRASHESoff.
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
*.unreadableand 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 simpledatalogsTo 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: the shortest path.
- Core concepts: one deployment per build, such as
playandbeta. - Privacy for senders: device ids, not emails.