Skip to content

feat(analytics): track received tips and messages, tip origin, display name, and token symbols - #629

Merged
bmc08gt merged 9 commits into
mainfrom
feat/analytics-received-events
Aug 22, 2026
Merged

feat(analytics): track received tips and messages, tip origin, display name, and token symbols#629
bmc08gt merged 9 commits into
mainfrom
feat/analytics-received-events

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Adds the receive-side analytics we were missing, plus a few adjacent gaps found along the way. Spec covers both platforms; this is the iOS half — the Android half is code-payments/code-android-app#1304.

What's new

Received tips and messages (people counters)
Three Mixpanel people properties, incremented as inbound messages land:

  • Tips Received — +1 per inbound tipped Cash message
  • Tips Received Value — + the USD-normalised value of that tip
  • Messages Received — +1 per inbound message of any type (a tip bumps both)

People properties are cumulative and unreversible, so double-counting is permanent. The watermark is the conversation's newest stored message id, read before the write: only messages above it are credited, and it only moves forward. A cold catch-up into a conversation the client holds nothing for seeds the store and counts nothing, so this PR is prospective-only — existing history is not retro-counted here. Backfilling it lands in a follow-up PR, on top of these counters.

Both delivery paths are hooked. Live deliveries count in persist(event:) (.newMessages and .chatEvents); gap-filled and reconnect deliveries count inside catchUp's delta-batch closure, which writes through its own path and never routes through persist(event:) — hooking only one would miss every reconnect. catchUp captures a single run-start baseline so batch 2 of a backfill can't count what batch 1 just seeded. resyncAfterReset, loadMessages, and older-paging are deliberately not hooked: they're history loads, and retro-counting them piecemeal is the follow-up PR's job, not the ingestion path's.

Chat cash arrives in the sender's native currency. If no exchange rate is cached for that currency we still increment the count but skip the value — an understated total is recoverable later, a wrong one is not.

Received events
Tip Received (Chat Type, Fiat, Currency, Mint, Token Symbol, Quarks) and Message Received (Chat Type), emitted one per inbound message as the self read pointer advances, over the half-open interval (previousPointer, newPointer]. The two are mutually exclusive — a tip emits only Tip Received.

Origin on Sent Tip
Existing Sent Tip now carries Origin: Tipcard or Chat, so subsequent payments from the money button inside a tip chat are distinguishable from the first tip off the tip card.

Display name events
Display Name Set and Display Name Updated, both carrying Source (Onboarding, My Account, Tip Card Setup). Set-vs-updated is decided by whether a prior name existed, not by which screen you came from — someone who skips onboarding and names themselves from the tip card still gets Set.

Token Symbol alongside every mint
Every event that carries Mint now also carries Token Symbol, and Payment Mint gains Payment Token Symbol. Resolution happens centrally in Analytics.track via a resolver installed at login and backed by the session's stored mint metadata; when a mint isn't cached the property is omitted entirely rather than sent empty.

Hex identity
No change here — iOS already identifies to Mixpanel with lowercase hex. Android moved to match in its half.

Notes for review

Three deliberate deviations from the spec's iOS notes:

  1. The received-event instrument fires from ConversationController.markRead rather than ConversationStore.advanceSelfReadPointer — the store lives in FlipcashCore, which can't reference the app target's Analytics. markRead captures the read pointer before the RPC and reports the crossed window only after a successful advance, so a failed markRead reports nothing and the next attempt retries the same window.
  2. The watermark is the pre-write Database.newestMessageID(conversationID:), not a new column. iOS has no migration framework — bumping SQLiteVersion wipes the local store — and the newest-id read gives the same monotonic-max semantics with zero schema change.
  3. A .catchUp delivery into an empty conversation seeds only and counts nothing; a .live delivery with no watermark still counts. Existing history stays uncounted until the backfill PR.

The receive-side concern lives in its own ConversationReceiptReporter with every dependency injected as a closure, so the counters and events are testable without touching Mixpanel.

@bmc08gt
bmc08gt merged commit 56aae46 into main Aug 22, 2026
@bmc08gt
bmc08gt deleted the feat/analytics-received-events branch August 22, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant