Skip to content

feat(analytics): time each phase of a Coinbase add-money purchase - #1935

Merged
bmc08gt merged 2 commits into
code/cashfrom
feat/analytics-add-money-timing
Nov 2, 2026
Merged

bmc08gt merged 2 commits into
code/cashfrom
feat/analytics-add-money-timing

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Adds seven optional duration properties (ms) to the Add Money event so a Mixpanel dashboard can show which phase of a Coinbase purchase got slower since Solana's Alpenglow upgrade. They're sent only when Method is Coinbase. On a failure, the phases that finished are sent with Total Time and the rest are absent.

Property Starts Ends
Order Time The user confirms the amount Coinbase returns the placed order
Payment Time Order placed Coinbase's onramp_api.payment_authorized event (includes the user's own time on the pay sheet)
Coinbase Time onramp_api.payment_authorized Coinbase's onramp_api.polling_success event
Deposit Time onramp_api.polling_success The app first sees a nonzero USDC balance in the user's account (includes waiting for Coinbase to send it on-chain)
Swap Time USDC seen The USDC to USDF swap returns success
Credit Time Swap succeeded The app sees the USDF credited and shows success
Total Time The user confirms the amount Add Money is sent, success or failure

iOS needs to match one rule that isn't obvious from the table: when the user is sent to verify a phone number or email first, both clocks restart when they come back and confirm the amount again. Android re-dispatches OnAmountConfirmed after verification, so this falls out of where the timer starts. The rule is in the event description.

Android sender

AddMoneyTimer (shared/tokens, commonMain) holds one monotonic mark per moment and computes the phases. Its TimeSource is injectable, and tests drive it with TestTimeSource. SwapViewModel starts it on amount confirm and reads it in one helper that every Add Money result send now goes through.

Two moments had no outlet before:

  • payment_authorized only paused the webview watchdog. It now reaches CoinbaseOnRampController, which emits it and polling_success on a milestones SharedFlow. It isn't a new CoinbaseOnRampState because the handler calls reset() right after Completed, and the conflated StateFlow can skip it.
  • UsdcDepositSweep emits UsdcSeen and Swapped on its own milestones flow. It's a singleton that the session's DepositDelegate also starts, so a call from the view model can be a no-op while a session sweep does the work. The view model listens to whichever sweep runs. The timer ignores sweep moments before polling_success, so a sweep of leftover USDC can't count as the deposit.

The generated AddMoneyEvents.result keeps its four leading parameters, so existing callers compile unchanged. AddMoneyEventsTest.resultIsTheEventNamedAddMoney is untouched. SwapViewModelErrorTest asserts the whole event for a full Coinbase success, a delivery failure (Order/Payment/Coinbase/Total only), and a Phantom deposit with no phase properties.

The controller's onPaymentAuthorized only has a negative test (no emit outside a payment). Getting to Paying there needs the JWT and order stubs. CoinbaseOnRampEventHandlerTest covers the webview event reaching the callback.

Catalogue descriptions

Token Purchase With {method}, Onramp: {step} and Create Account Payment drop the "Android only; part 2 decides whether iOS sends it" wording, because iOS sends all three (a parallel iOS PR is wiring them). Create Account Payment now says it's the account-creation payment, distinct from Create Account at registration. Drift notes are unchanged.

Follow-up

The direct launchpad-token buy through Coinbase is out of scope. addMoneyMethod is null when the mint isn't USDF, so that path sends no Add Money and no phase times.

Seven optional duration properties split a Coinbase add-money purchase
into Order, Payment, Coinbase, Deposit, Swap and Credit phases plus a
total, so a dashboard can show which phase slowed down. The event
description names each property's start and end moment in
platform-neutral words so iOS can time the same spans.

Token Purchase With {method}, Onramp: {step} and Create Account Payment
drop the "part 2 decides whether iOS sends it" wording: iOS sends all
three.
AddMoneyTimer holds a monotonic mark per moment and turns them into the
Add Money phase durations; the clock is injectable so tests drive it with
TestTimeSource. SwapViewModel starts it when the amount is confirmed and
reads it on every Coinbase Add Money send, success or failure.

Two moments had no outlet. The webview's payment_authorized event now
reaches CoinbaseOnRampController, which emits it and polling_success on a
non-conflated milestones flow; the state flow can skip Completed when the
handler resets right after. UsdcDepositSweep emits UsdcSeen and Swapped
on its own milestones flow. It is a singleton the session also starts, so
the view model listens to whichever sweep runs; the timer ignores sweep
moments before polling_success so a leftover-balance sweep can't count.
@bmc08gt bmc08gt self-assigned this Nov 2, 2026
@github-actions github-actions Bot added type: feature New functionality area: onramp Deposit, purchase, Coinbase, fiat on-ramp area: tokens Token accounts, balances, token info labels Nov 2, 2026
@bmc08gt
bmc08gt merged commit 6cd6998 into code/cash Nov 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: onramp Deposit, purchase, Coinbase, fiat on-ramp area: tokens Token accounts, balances, token info type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant