cmp-firebase-analytics¶
cmp-firebase-analytics¶
Firebase Analytics for Kotlin Multiplatform — single-module design. Interface + Stub/NoOp/Test variants on all 21 KMP targets, with GitLive Firebase Analytics on 11 of 21 targets (everything Apple/Android/JVM/JS — only watchOS, Linux, Windows, and wasm fall back to NoOp).
What's in the box¶
io.github.mobilebytelabs.kmptoolkit.analytics
├── AnalyticsHelper — interface (logEvent, logScreenView, logError, ...)
├── AnalyticsEvent / Param — type-safe event + param data classes (Firebase-aligned validation)
├── EventTypes / ParamKeys — standard constants for cross-app consistency
├── StubAnalyticsHelper — dev/Kermit logger
├── NoOpAnalyticsHelper — release/test default; events silently discarded
├── TestAnalyticsHelper — captures events for assertion in unit tests
├── EventValidator — taxonomy regex + PII regex check (debug-build use)
├── PerformanceTracker — start/stop timer that emits loading_time events
├── EventRegistry — opt-in declared-event enforcement
├── AnalyticsExtensions — `analytics.log("event") { param(...) }` DSL
├── AnalyticsProvider.kt — `expect fun provideAnalyticsHelper(): AnalyticsHelper`
├── di/AnalyticsModule — factory: Mode.Firebase | Mode.Stub | Mode.NoOp
└── firebase/
└── FirebaseAnalyticsHelper — GitLive-backed concrete impl (firebaseMain only)
Targets — true 21/21 KMP coverage via two transport tiers¶
| Tier | Targets | Count | Recommended helper | Default provideAnalyticsHelper() |
|---|---|---|---|---|
| firebaseMain | Android, JVM, iOS (iosX64/iosArm64/iosSimulatorArm64), macOS (macosX64/macosArm64), tvOS (tvosX64/tvosArm64/tvosSimulatorArm64), JS | 11 | FirebaseAnalyticsHelper (GitLive — full native: DebugView, automatic events, A/B Testing, demographics) |
FirebaseAnalyticsHelper(Firebase.analytics) |
| nonFirebaseMain | watchOS (×4), Linux (×2), mingwX64, wasmJs, wasmWasi | 10 | MeasurementProtocolAnalyticsHelper (HTTP POST to Firebase MP — events land in the SAME Firebase Analytics property + same BigQuery export) |
NoOpAnalyticsHelper (until app wires MP — see below) |
GitLive Firebase Analytics 2.x ships on macOS and tvOS — they're real Apple targets that Firebase iOS SDK supports natively. Only watchOS sits outside on the Apple side (Firebase iOS SDK has no watchOS variant).
For the 10 non-Firebase platforms, MeasurementProtocolAnalyticsHelper provides event capture parity (custom events, user properties, persistent client_id). It uses Firebase's Measurement Protocol REST API — events land in the SAME property and BigQuery dataset as GitLive-emitted events. Trade-offs vs native SDK: no DebugView, no automatic events, no A/B tie-in, ~1h latency to BigQuery (same as GitLive).
provideAnalyticsHelper() defaults to NoOp on nonFirebase platforms because MP requires app-supplied config (measurement_id + api_secret). Apps that want analytics on watchOS / Linux / etc. wire MeasurementProtocolAnalyticsHelper directly in their Koin module — see "Setup → Non-Firebase platforms" below.
Install¶
// gradle/libs.versions.toml
[versions]
cmpFirebaseAnalytics = "..."
[libraries]
cmp-firebase-analytics = { module = "io.github.mobilebytelabs:cmp-firebase-analytics", version.ref = "cmpFirebaseAnalytics" }
// build.gradle.kts (consumer module)
commonMain.dependencies {
implementation(libs.cmp.firebase.analytics)
}
GitLive Firebase Analytics is brought in transitively as api on supported platforms. On non-supported platforms the dependency simply doesn't apply (Gradle source-set hierarchy handles it).
Setup¶
Android¶
- Add
google-services.jsontoapp/ - Apply the plugin:
id("com.google.gms.google-services") version "..." - Add Firebase BoM and Analytics:
iOS / macOS / tvOS¶
- Add
GoogleService-Info.plistto your app target - In your AppDelegate /
@mainApp: - Pod dependencies are bundled by GitLive — see your project's Podfile
JS / JVM¶
Follow GitLive's docs: https://github.com/GitLiveApp/firebase-kotlin-sdk
watchOS / Linux / Windows / wasm — Non-Firebase platforms¶
GitLive doesn't ship on these 10 targets, so use Firebase Measurement Protocol over HTTP for event capture parity.
-
Generate an MP API secret at Firebase Console → Project settings → Integrations → GA4 → Data Streams → {your stream} → Measurement Protocol API secrets → Create.
-
Store the secret in your app's secrets store — env var, encrypted prefs, keychain, or
release-layer/.env(gitignored). NEVER hard-code or commit. -
Wire
MeasurementProtocolAnalyticsHelperin your Koin module:
import com.russhwolf.settings.Settings
import io.github.mobilebytelabs.kmptoolkit.analytics.AnalyticsHelper
import io.github.mobilebytelabs.kmptoolkit.analytics.mp.MeasurementProtocolAnalyticsHelper
import io.github.mobilebytelabs.kmptoolkit.analytics.mp.MpConfig
val analyticsModule = module {
single<AnalyticsHelper> {
MeasurementProtocolAnalyticsHelper(
config = MpConfig(
measurementId = "G-XXXXXXXX", // GA4 measurement ID
apiSecret = SecureStore.read("MP_API_SECRET"), // your secrets store
),
settings = Settings(), // multiplatform-settings
)
}
}
- Events from MP land in the SAME
analytics_*.events_*BigQuery table as GitLive-emitted events./idea analytics --fetchworks identically across all 21 platforms.
What you give up vs native SDK on these platforms:
- No automatic events (first_open, session_start, in_app_purchase) — emit manually if needed
- No DebugView (events visible only in BigQuery, ~1h latency)
- No A/B Testing tie-in
- No demographics inference
- No platform-native session tracking — supply engagement_time_msec param manually if you need engagement metrics
What still works: - Custom event capture with up to 25 params per event - User properties + user ID - Persistent client_id on platforms with KV storage (Apple, JS) — in-memory on Linux native / mingwX64 / wasmWasi - Async batching (5s debounce or 25 events, whichever first) - Silent failure on network errors (analytics never breaks the app)
Usage¶
import io.github.mobilebytelabs.kmptoolkit.analytics.*
import io.github.mobilebytelabs.kmptoolkit.analytics.di.AnalyticsModule
// Easiest path — let the module pick per build:
val analyticsModule = module {
single<AnalyticsHelper> {
AnalyticsModule.analyticsHelper(
if (BuildConfig.DEBUG) AnalyticsModule.Mode.Stub
else AnalyticsModule.Mode.Firebase
)
}
single { AnalyticsModule.performanceTracker(get()) }
}
Then in your ViewModel:
class SettingsViewModel(private val analytics: AnalyticsHelper) : ViewModel() {
init {
analytics.logScreenView("settings", sourceScreen = "home")
}
fun onSaveClick() {
analytics.logButtonClick("save", screenName = "settings")
// ... save logic
}
}
Direct logging¶
analytics.logEvent(EventTypes.BUTTON_CLICK,
ParamKeys.BUTTON_NAME to "save",
ParamKeys.SCREEN_NAME to "settings",
)
// Convenience helpers
analytics.logScreenView("settings", sourceScreen = "home")
analytics.logError("Network timeout", errorCode = "NET_001", screen = "settings")
analytics.logStateTransition("settings", from = "loading", to = "content")
// Builder DSL
analytics.log(EventTypes.FORM_COMPLETED) {
param(ParamKeys.FORM_NAME, "registration")
param(ParamKeys.COMPLETION_TIME, 45)
}
// Performance timing
val tracker = PerformanceTracker(analytics)
tracker.measure("settings_screen_render") { /* render work */ }
Direct Firebase access¶
If you need the underlying GitLive FirebaseAnalytics (e.g., custom user properties beyond the helper API):
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.analytics.analytics
import io.github.mobilebytelabs.kmptoolkit.analytics.firebase.FirebaseAnalyticsHelper
val helper = FirebaseAnalyticsHelper(Firebase.analytics)
FirebaseAnalyticsHelper is only available on firebaseMain (Android/iOS/JS/JVM). Cross-platform code should call provideAnalyticsHelper() instead — it returns the same helper on supported platforms and NoOpAnalyticsHelper elsewhere.
Auto-injected kmp_platform param¶
Every event gets a kmp_platform param injected by the helper — disambiguates events in BigQuery / Firebase Console by platform-of-origin:
| Source target | kmp_platform value |
|---|---|
androidMain |
"android" |
iosMain (×3) |
"ios" |
macosMain (×2) |
"macos" |
tvosMain (×3) |
"tvos" |
watchosMain (×4) |
"watchos" |
jvmMain |
"jvm" |
jsMain |
"js" |
linuxMain (×2) |
"linux" |
mingwMain |
"mingw" |
wasmJsMain |
"wasmjs" |
wasmWasiMain |
"wasmwasi" |
Why a custom key, not GA4's built-in platform:
- GA4's auto-platform is coarse: only "android" | "ios" | "web"
- MP HTTP events don't get auto-platform unless we set it
- Sub-platforms (watchOS vs iOS, tvOS vs macOS — all "Apple") collapse to "ios" in GA4's field
- We need single signal that's reliable across native and MP transports
Override per-helper for finer-grained signal:
FirebaseAnalyticsHelper(Firebase.analytics, platformOverride = "android-tv")
StubAnalyticsHelper(platformOverride = "ios-tablet")
Override per-event by setting kmp_platform manually — auto-injection respects existing values:
analytics.logEvent(EventTypes.BUTTON_CLICK,
ParamKeys.PLATFORM to "android-tablet", // takes precedence over kmpPlatform
ParamKeys.BUTTON_NAME to "save",
)
TestAnalyticsHelper does NOT auto-inject — keeps test assertions explicit.
Firebase Analytics constraints¶
The adapter automatically truncates to Firebase's limits:
| Field | Max | What happens |
|---|---|---|
| Event name | 40 chars | truncated by .take(40) |
| Param key | 40 chars | truncated by .take(40) |
| Param value | 100 chars | truncated by .take(100) |
| User property name | 24 chars | truncated by .take(24) |
| User property value | 36 chars | truncated by .take(36) |
| User ID | 256 chars | truncated by .take(256) |
| Params per event | 25 | enforced upstream by AnalyticsEvent.init (throws on > 25) |
Best practice: design your event taxonomy to fit naturally. The bundled EventValidator enforces a stricter regex (^[a-z][a-z0-9_]{1,39}$) which keeps you in spec.
Testing¶
@Test fun `clicking save logs button_click event`() {
val analytics = TestAnalyticsHelper()
val viewModel = SettingsViewModel(analytics)
viewModel.onSaveClick()
val event = analytics.events.single()
assertEquals(EventTypes.BUTTON_CLICK, event.type)
assertEquals("save", event.extras.first { it.key == ParamKeys.BUTTON_NAME }.value)
}
Privacy¶
- Use the
pii: trueflag on params in your screen YAML (per framework/idea analyticsschema) to mark sensitive fields — these are NEVER auto-instrumented - Hash/obfuscate
user_idbefore passing tosetUserId()— never use raw email/phone - Respect platform settings: iOS App Tracking Transparency (ATT), Android Limited Ad Tracking
- Provide an opt-out toggle in app settings; bind it to swap to
NoOpAnalyticsHelperat runtime
Project consumer pattern¶
my-project/source/my-project/
├── core/
│ └── analytics/ ← thin glue layer per project
│ ├── build.gradle.kts ← depends on cmp-firebase-analytics
│ └── di/AnalyticsModule.kt ← Koin module: pick mode per build flavor
└── feature/settings/
└── SettingsViewModel.kt ← depends only on AnalyticsHelper interface
Heavy lifting is here. Per-project core/analytics is just Koin wiring + project-specific event taxonomy.
Related¶
- Framework
/idea analytics— auto-instrumentation generator + Claude-driven growth analysis - GitLive Firebase Kotlin SDK — https://github.com/GitLiveApp/firebase-kotlin-sdk
- Plan:
plan-layer/plans/PLAN-fw-260504-idea-analytics.md(in claude-product-cycle framework)
License¶
Apache 2.0
Module reference¶
Module Identity (auto-gen)
| Artifact | Package | Current version | Maven | Since | API tier |
|---|---|---|---|---|---|
io.github.mobilebytelabs:cmp-firebase-analytics |
com.mobilebytelabs.kmptoolkit.firebase.analytics |
UNKNOWN |
Central | 2026-05-30 | experimental |
Module purpose (one paragraph):
§2 Per-Platform Parity Matrix (auto-gen)¶
| Target | Source-set present | Real impl | UnsupportedPlatform stub | .kt count | Last reviewed | Coverage | Notes |
|---|---|---|---|---|---|---|---|
| androidMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| iosMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| macosMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| jvmMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| jsMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| wasmJsMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| mingwMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| linuxMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| tvosMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
| watchosMain | ✅ | ✅ real | 0 | 1 | 2026-06-01 | (legacy:full) | — |
Legend (Real impl): ✅ real impl, 🟡 partial / wontfix-OS / wontfix-infra / legacy stub, ⛔ not declared, — N/A.
Legend (Coverage enum, since 2026-06-01): full (all public-API methods backed by OS primitive) · partial (most real; some typed UnsupportedPlatform fallbacks for contracts that don't apply) · wontfix-OS (OS lacks the primitive) · wontfix-infra (impl possible but CI/toolchain blocks it) · (legacy:full|stub) (auto-derived; pre-opt-in modules — add a // LD-2-coverage: {enum} comment to the platform's primary .kt file to graduate). See RULE-LIB-DEVELOPMENT-MD-001 LD-2 + ADRs for accepted wontfix cases.
API reference¶
Each release ships the module's full Dokka HTML site inside its
-javadoc.jar artifact on Maven Central.
In IntelliJ / Android Studio the IDE mounts the jar and surfaces it automatically in hover popups, Quick Documentation, and Symbol search.