--- name: kmp-starter-feature-analytics description: The KMP Starter Template analytics system — AppEvent/EventsTracker, Analytics routing, Mixpanel + Firebase providers, combining providers, and runtime swaps. author: DevAtrii license: MIT --- # Analytics 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 | Piece | Module | Path | | --- | --- | --- | | `AppEvent` (base) | analytics domain | `features/analytics/domain/.../AppEvent.kt` | | `EventsTracker` / `Analytics` / `StarterAnalyticsProvider` | analytics domain | `features/analytics/domain/...` | | `StarterAnalyticsProviderId` / `StarterAnalyticsProviderIds` | analytics domain | `features/analytics/domain/.../StarterAnalyticsProviderId.kt` | | Router + `analyticsDomainModule` | analytics domain | `features/analytics/domain/.../AnalyticsRouter.kt` | | Mixpanel `EventsTrackerImpl` + `analyticsDataModule` | analytics data | `features/analytics/data/...` | | `FirebaseStarterAnalyticsProvider` + `analyticsFirebaseDataModule` | analytics data-firebase | `features/analytics/data-firebase/...` | | `AppEvents` (sealed) | core domain | `features/core/domain/.../AppEvents.kt` | ## 1. Define events (one sealed hierarchy) Keep **one** sealed `AppEvents` hierarchy in core domain. Each nested type **is** an event: ```kotlin sealed class AppEvents( event: String, properties: Map? = null, ) : AppEvent(event, properties) { constructor(event: String) : this(event, properties = null) constructor(event: String, pair: Pair? = 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: ```kotlin class SignInViewModel( private val eventsTracker: EventsTracker, ) : MviViewModel() { 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: ```kotlin 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`, 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. ```kotlin 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`: ```kotlin 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`