Analytics

SkillDatabases & data

The KMP Starter Template analytics system — AppEvent/EventsTracker, Analytics routing, Mixpanel + Firebase providers, combining providers, and runtime swaps.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Analytics skill

What this skill tells your AI

The instructions your AI receives, as published by devatrii/kmp-starter-template in .agents/skills/kmp-starter/starter-features/analytics/SKILL.md and read by ahel’s review.

Modular, provider-agnostic analytics. Track type-safe AppEvent models. Routing lives in domain. You pass providers in initAnalytics { providers(...) } after each backend's own init*. Mixpanel and Firebase Analytics are opt-in.

Kotlin types are StarterAnalyticsProvider / StarterAnalyticsProviderId / StarterAnalyticsProviderIds. Swift still sees AnalyticsProvider via @ObjCName.

Where things live

PieceModulePath
AppEvent (base)analytics domainfeatures/analytics/domain/.../AppEvent.kt
EventsTracker / Analytics / StarterAnalyticsProvideranalytics domainfeatures/analytics/domain/...
StarterAnalyticsProviderId / StarterAnalyticsProviderIdsanalytics domainfeatures/analytics/domain/.../StarterAnalyticsProviderId.kt
Router + analyticsDomainModuleanalytics domainfeatures/analytics/domain/.../AnalyticsRouter.kt
Mixpanel EventsTrackerImpl + analyticsDataModuleanalytics datafeatures/analytics/data/...
FirebaseStarterAnalyticsProvider + analyticsFirebaseDataModuleanalytics data-firebasefeatures/analytics/data-firebase/...
AppEvents (sealed)core domainfeatures/core/domain/.../AppEvents.kt

1. Define events (one sealed hierarchy)

Keep one sealed AppEvents hierarchy in core domain. Each nested type is an event:

sealed class AppEvents(
    event: String,
    properties: Map<String, Any>? = null,
) : AppEvent(event, properties) {

    constructor(event: String) : this(event, properties = null)

    constructor(event: String, pair: Pair<String, Any>? = null) : this(
        event = event,
        properties = if (pair != null) mapOf(pair) else mapOf(),
    )

    data object DummyEvent : AppEvents(event = "dummy_event")

    data class OnPurchaseSuccess(
        val productId: String,
    ) : AppEvents(
        event = "purchase_success",
        pair = "product_id" to productId,
    )

    data class OnPurchaseFailure(
        val productId: String,
        val error: String,
    ) : AppEvents(
        event = "purchase_failure",
        properties = mapOf("product_id" to productId, "error" to error),
    )
}

Rules:

  • Prefer snake_case event names.
  • Single property → pair; multiple → properties = mapOf(...).
  • Do not add a method to EventsTracker per event — the type is the event.

2. Track from the ViewModel

Inject EventsTracker, call track(...) with an AppEvents instance:

class SignInViewModel(
    private val eventsTracker: EventsTracker,
) : MviViewModel<SignInState, SignInActions, SignInEvents>() {

    fun onSignIn(userId: String) {
        viewModelScope.launch {
            eventsTracker.track(
                event = AppEvents.SignInSuccess(userId = userId),
            )
            eventsTracker.setUserId(userId)
            eventsTracker.setUserProperty(key = "plan", value = "pro")
            eventsTracker.setUserProperty(key = "is_pro", value = true)
            eventsTracker.setUserProperties(
                values = mapOf("plan" to "pro", "is_pro" to true, "login_count" to 3),
            )
            eventsTracker.setDefaultEventParameters(
                params = mapOf("app_flavor" to "prod", "build_number" to 42),
            )
        }
    }
}

Keep analytics calls in the presentation layer (ViewModel is best). Typed AppEvent is preferred over the string overloads track(event), track(event, pair), track(event, properties).

EventsTracker also exposes setUserId, setUserProperty / setUserProperties (String keys + Any values), setDefaultEventParameters (Mixpanel registerSuperProperties / Firebase setDefaultEventParameters), optIn/optOut/toggleOptInOut/hasOptedIn, flush, reset. Firebase user properties and GitLive default params stringify. Mixpanel keeps native types.

Koin binds the same router as Analytics and EventsTracker. Inject Analytics only for lookup, combine, or setActiveProviders.

3. Setup (Mixpanel token + Firebase)

Set the Mixpanel token in composeApp/.../core/AppConstants.kt. Firebase uses platform config (google-services.json / GoogleService-Info.plist), not an API token. Wire after Koin:

object AppConstants {
    const val MIXPANEL_API_TOKEN = "add-your-mixpanel-token-here"
}

initMixPanel(apiKey = AppConstants.MIXPANEL_API_TOKEN) {
    logging = platform.debug
}
initFirebaseAnalytics {
    enabled = true
    sessionTimeoutInterval = 30.minutes
    defaultEventParameters = mapOf("app_flavor" to "prod")
    analyticsStorage = FirebaseAnalytics.ConsentStatus.GRANTED
}
initAnalytics {
    enableInstallAttribution = true
    providers(
        MixPanelAnalyticsScope.getProvider(),
        FirebaseAnalyticsScope.getProvider(),
    )
}

initFirebaseAnalytics options: enabled, sessionTimeoutInterval, defaultEventParameters: Map<String, String>, consent fields. No userProperties on init — use setUserProperty after initAnalytics.

enableInstallAttribution = true → Android Play Install Referrer once (install_attribution event + UTM user properties). iOS no-op.

Skip a backend (and its data module) if the consumer does not want it.

Lookups: StarterAnalyticsProviderIds.Mixpanel / .Firebase. Custom id: StarterAnalyticsProviderId("posthog").

4. Multiple providers

All providers passed to initAnalytics start active. setActiveProviders reroutes the same injected instance; SDKs stay alive.

val analytics: Analytics = koinInject()

analytics.provider(StarterAnalyticsProviderIds.Mixpanel).track(AppEvents.DummyEvent)

analytics.combine(
    StarterAnalyticsProviderIds.Mixpanel,
    StarterAnalyticsProviderIds.Firebase,
).track(AppEvents.DummyEvent)

analytics.setActiveProviders(
    StarterAnalyticsProviderIds.Mixpanel,
    StarterAnalyticsProviderIds.Firebase,
)
analytics.setActiveProviders() // disable routed tracking

Add a backend by implementing StarterAnalyticsProvider, giving it its own init* if needed, and passing it in initAnalytics:

initMixPanel(apiKey = token)
initFirebaseAnalytics { }
initAnalytics {
    providers(
        MixPanelAnalyticsScope.getProvider(),
        FirebaseAnalyticsScope.getProvider(),
    )
}

Do not auto-register Mixpanel (or Firebase) as EventsTracker in Koin.

Rules

  • Do not introduce a parallel analytics system.
  • Keep calls in the presentation layer.
  • Register analyticsDomainModule in InitKoin. Call initAnalytics after Koin + provider inits. Mixpanel: analyticsDataModule + initMixPanel + pass getProvider() only if you want it. Firebase: analyticsFirebaseDataModule + initFirebaseAnalytics + pass FirebaseAnalyticsScope.getProvider().

Reference

  • Docs: https://starter.atherio.dev/features/ → Analytics
  • Source: features/analytics/*, features/core/domain/.../AppEvents.kt

Signals

GitHub stars
162
Forks
28
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
kmp-starter-feature-analytics
Source
github.com/devatrii/kmp-starter-template