
Typed, compile-safe event tracking with app-defined event vocabulary; durable on-disk queue, batched uploads with exponential backoff, privacy-preserving install id, automatic lifecycle events and diagnostics.
Kotlin Multiplatform client for eventbase — typed events, an offline queue, and batched upload.
English | 简体中文
The whole surface is four calls — init, track, setUserId, startFlow. Queueing, retries, install identity, lifecycle events and automatic properties all stay inside the library. The server half is eventbase, which runs in your own Cloudflare Worker.
deviceId if you want one, and declare it yourself.app_opened and app_backgrounded (with duration) are reported without a line of integration code, using the platform's own foreground signal rather than a hand-rolled Activity count that miscounts on rotation.1. Add the dependency. The HTTP engine is yours to choose.
commonMain.dependencies { implementation("wang.harlon:eventbase-kt:<version>") }2. Initialize once, at startup. Android is shown below; the iOS form — where the constants come from instead of BuildConfig — is in the integration guide.
Eventbase.init(
context = this, // Android only: the handle for SharedPreferences
config = EventbaseConfig(
endpoint = "https://api.example.com/t",
appKey = BuildConfig.EVENTBASE_KEY, // public key; shipping it in the APK is fine
appVersion = BuildConfig.VERSION_NAME,
platform = "android",
channel = BuildConfig.CHANNEL,
locale = systemLocaleTag(),
isDebug = BuildConfig.DEBUG,
),
)From here the library owns install id, automatic properties, lifecycle events and the offline queue.
3. Define your vocabulary. It lives in your app, not in the library.
sealed class AppEvent(
override val name: String,
override val props: Map<String, Any?>,
) : Event {
data class ContentOpened(val source: String, val rank: Int, val contentId: String) :
AppEvent("content_opened", mapOf("source" to source, "rank" to rank, "content_id" to contentId))
data class SettingChanged(val key: String, val value: String) :
AppEvent("setting_changed", mapOf("key" to key, "value" to value))
}4. Track.
Eventbase.track(AppEvent.ContentOpened(source = "github", rank = 3, contentId = item.id))
Eventbase.setUserId(identity.id) // after sign-in; later events carry user_id
Eventbase.clearUserId() // on sign-out; the install id is unchanged5. Follow one user journey across a process death.
val flow = Eventbase.startFlow()
Eventbase.track(AuthStarted("sign_in", method = "github"), flow)
// after the browser comes back — possibly in a brand new process
Eventbase.track(AuthFinished("sign_in", "github", outcome = "success"), Eventbase.currentFlow())startFlow() is persisted, so "the user went to the browser and never came back" becomes something you can actually measure.
| Offline queue | On disk, capped at 500, oldest dropped first, events older than 7 days discarded on the way out |
| Flush timing | On backgrounding, and whenever flushAt events have accumulated — deliberately no timer, which on mobile only buys battery drain |
| Retries | Exponential backoff; both 4xx and 204 dequeue, because the server has already decided |
| Install identity | Generated on first launch, changes only on reinstall, derived from no device identifier |
| Automatic properties | app_version, platform, channel, locale, is_debug, session |
| Lifecycle events |
app_opened / app_backgrounded, zero integration code (autoLifecycle = false opts out) |
| Testing |
RecordingSink captures events in-process; assert on names and properties without a server |
| Integration guide | iOS initialization, common scenarios, diagnostic logging, the smoke drill, testing, installId and deviceId
|
| Ingestion protocol | The wire contract, in the server repo — the single source of truth |
| Telemetry design | Event vocabulary and metric definitions |
MIT
Kotlin Multiplatform client for eventbase — typed events, an offline queue, and batched upload.
English | 简体中文
The whole surface is four calls — init, track, setUserId, startFlow. Queueing, retries, install identity, lifecycle events and automatic properties all stay inside the library. The server half is eventbase, which runs in your own Cloudflare Worker.
deviceId if you want one, and declare it yourself.app_opened and app_backgrounded (with duration) are reported without a line of integration code, using the platform's own foreground signal rather than a hand-rolled Activity count that miscounts on rotation.1. Add the dependency. The HTTP engine is yours to choose.
commonMain.dependencies { implementation("wang.harlon:eventbase-kt:<version>") }2. Initialize once, at startup. Android is shown below; the iOS form — where the constants come from instead of BuildConfig — is in the integration guide.
Eventbase.init(
context = this, // Android only: the handle for SharedPreferences
config = EventbaseConfig(
endpoint = "https://api.example.com/t",
appKey = BuildConfig.EVENTBASE_KEY, // public key; shipping it in the APK is fine
appVersion = BuildConfig.VERSION_NAME,
platform = "android",
channel = BuildConfig.CHANNEL,
locale = systemLocaleTag(),
isDebug = BuildConfig.DEBUG,
),
)From here the library owns install id, automatic properties, lifecycle events and the offline queue.
3. Define your vocabulary. It lives in your app, not in the library.
sealed class AppEvent(
override val name: String,
override val props: Map<String, Any?>,
) : Event {
data class ContentOpened(val source: String, val rank: Int, val contentId: String) :
AppEvent("content_opened", mapOf("source" to source, "rank" to rank, "content_id" to contentId))
data class SettingChanged(val key: String, val value: String) :
AppEvent("setting_changed", mapOf("key" to key, "value" to value))
}4. Track.
Eventbase.track(AppEvent.ContentOpened(source = "github", rank = 3, contentId = item.id))
Eventbase.setUserId(identity.id) // after sign-in; later events carry user_id
Eventbase.clearUserId() // on sign-out; the install id is unchanged5. Follow one user journey across a process death.
val flow = Eventbase.startFlow()
Eventbase.track(AuthStarted("sign_in", method = "github"), flow)
// after the browser comes back — possibly in a brand new process
Eventbase.track(AuthFinished("sign_in", "github", outcome = "success"), Eventbase.currentFlow())startFlow() is persisted, so "the user went to the browser and never came back" becomes something you can actually measure.
| Offline queue | On disk, capped at 500, oldest dropped first, events older than 7 days discarded on the way out |
| Flush timing | On backgrounding, and whenever flushAt events have accumulated — deliberately no timer, which on mobile only buys battery drain |
| Retries | Exponential backoff; both 4xx and 204 dequeue, because the server has already decided |
| Install identity | Generated on first launch, changes only on reinstall, derived from no device identifier |
| Automatic properties | app_version, platform, channel, locale, is_debug, session |
| Lifecycle events |
app_opened / app_backgrounded, zero integration code (autoLifecycle = false opts out) |
| Testing |
RecordingSink captures events in-process; assert on names and properties without a server |
| Integration guide | iOS initialization, common scenarios, diagnostic logging, the smoke drill, testing, installId and deviceId
|
| Ingestion protocol | The wire contract, in the server repo — the single source of truth |
| Telemetry design | Event vocabulary and metric definitions |
MIT