Skip to content

feat(analytics-events): share the analytics event contract - #1538

Merged
bmc08gt merged 37 commits into
code/cashfrom
feat/analytics-events-contract
Sep 24, 2026
Merged

bmc08gt merged 37 commits into
code/cashfrom
feat/analytics-events-contract

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Both apps spell every analytics event name and property key by hand, in parallel, and the inventory found them drifting in both names and values. This adds the shared half of the fix: a KMP module whose events are defined once, in libs/analytics-events/events.toml, and exported to iOS through :kmp:shared-core with a Swift facade. Nothing calls it yet.

Design: docs/superpowers/specs/2026-09-23-shared-analytics-events-design.md in the orchestrator repo, with the per-event table in 2026-09-23-analytics-inventory.md beside it.

The module

:libs:analytics-events is shaped like :libs:reporting. AnalyticsEvent(name, properties) carries a sealed PropertyValue (Text, Number, Flag), because Mixpanel treats "12" and 12 as different values and the type is part of the contract. The value enums and 13 builder objects, one per domain with 45 builders in all, are generated from the catalogue (see below). A null argument omits its key rather than sending an empty value. The module knows nothing about Mixpanel, Fiat, Mint or Throwable, so callers convert their own types.

It covers every event Android sends today, not only the ones the two apps already agree on. Where iOS sends something different, the builder's KDoc has a DRIFT: line saying what iOS sends instead. Android-only events say Android only; part 2 decides whether iOS sends it. Those notes are the worklist for settling the drift, so the two markers are kept distinct.

Each domain has a commonTest suite that writes every expected name, key and value as a string literal and never references the module's enums. A renamed key or a mistyped value then fails the build on its own, rather than changing both sides of the assertion at once. 38 tests, run on the Android host, macosArm64 and iosSimulatorArm64.

The catalogue

events.toml is the source of truth. A build-logic task, generateAnalyticsEvents, writes the builders, value enums and PeopleCounter into build/generated/analytics/commonMain at build time, so no generated code is checked in. AnalyticsEvent.kt and Amount.kt stay hand-written. An entry looks like this:

[[domains.SwapEvents.events]]
builder = "purchase"
event = "Token Purchase With {method}"
params = ["method: PurchaseMethod", "mint: String", "amount: Amount", "error: String?"]
properties = [
  { key = "Mint", value = "mint" },
  { group = "amount" },
  { key = "Error", value = "error" },
]
drift = "(Reserves) iOS also sends State, ..."

The format also covers fixed-format values (format = "Failed to parse deeplink => {url}"), two builders sending one event name (Deeplink: Parse), and the amount and outcome property groups. The header of events.toml lists the steps for adding an event, including the literal test and the iOS release.

The generator rejects a catalogue it would otherwise have to guess at: unknown keys or types, a parameter nothing sends, a template naming a missing parameter, and a duplicate builder or property key. Mint and Exchange Rate may sit beside { group = "amount" }, because amount(...) writes them only when the amount has them. Its 26 tests are in build-logic/convention/src/test.

The builders were hand-written first and moved into the catalogue one domain per commit. The 38 commonTest suites and the 6 Swift tests are unchanged since before the move, so they check that the generated builders send exactly what the hand-written ones did.

The facade renames four types

AnalyticsEvents.swift re-exports the Kotlin types as typealiases, as ReportReason.swift does, and adds scalarProperties, which flattens PropertyValue into a Swift AnalyticsScalar enum for the app to map onto MixpanelType.

Four names change on the way through:

Kotlin Swift Why
AnalyticsEvent TrackedEvent The iOS app declares protocol AnalyticsEvent, and FlipcashCore re-exports SharedCoreKit
Button AnalyticsButton Reads as SwiftUI's Button
State EventState Reads as SwiftUI's @State
Amount AnalyticsAmount Sits next to the app's money types

Against code-ios-app main, the only other exported names that match a Swift declaration are the Analytics.AddMoneySource and Analytics.DisplayNameSource nested enums, which don't collide with a top-level name and go away as iOS migrates.

The builders are Kotlin objects, so Swift calls them through .shared, e.g. ChatEvents.shared.sentMessage(chatType: .group, error: nil). Kotlin/Native keeps the hump on these enum cases: TokenInfoSource.chatGate, AnalyticsButton.shareTokenInfo.

PropertyValue bridges as a protocol with no exhaustiveness, so scalarProperties hits a preconditionFailure on an unmapped case. A fourth value type then fails AnalyticsEventsTests instead of silently dropping properties in production.

Registrations

include and kmpUnitTestModules in settings.gradle.kts, api and export in :kmp:shared-core, and in shared-core-tests.yml the libs/analytics-events/** and generator source path filters plus :libs:analytics-events:iosSimulatorArm64Test. The workflow entries are the ones that fail silently if missing. flipcashTestDebug now also runs :build-logic:convention:test, which is how the generator's tests reach the Flipcash tests lane; build-logic's tests didn't run in CI before.

What's next

The Android refactor that moves every call site onto these builders follows on a branch stacked on this one. iOS is publish-gated: it pins .upToNextMinor(from: "0.7.0"), and the newest flipcash-shared-core-spm release is 0.7.1, so the iOS work waits on the next minor after this merges. If reporting's release hasn't gone out by then, the two ship together.

@bmc08gt
bmc08gt requested a review from jeffyanta as a code owner September 23, 2026 18:14
@bmc08gt bmc08gt self-assigned this Sep 23, 2026
@github-actions github-actions Bot added type: feature New functionality area: build-system Gradle, convention plugins, build-logic and removed type: feature New functionality labels Sep 23, 2026
Adds the `flipcash.analytics.catalogue` convention plugin. It reads a
module's `events.toml` and generates the value enums, `PeopleCounter`, and
one builder object per domain into `commonMain`, in the shapes
`:libs:analytics-events` wrote by hand.

The parser rejects unknown keys, types and groups, parameters nothing
sends, templates naming a missing or optional parameter, and duplicate
builders, cases and property keys. Each message names the entry at fault.
Applies the catalogue plugin, with an events.toml that declares only the
package. Domains move into it in the following commits.
@bmc08gt
bmc08gt merged commit 8cae66d into code/cash Sep 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant