Skip to content

Commit 2a0ae79

Browse files
authored
refactor(analytics): send every event through FlipcashAnalytics.track (#1554)
* refactor(analytics): add the track interface beside the legacy service * refactor(analytics): convert Android domain types for the shared builders * refactor(analytics): move chat events onto the shared builders * refactor(analytics): move transfer events onto the shared builders Every transfer call site now tracks a TransferEvents builder. The amount conversion follows the type each caller already had (LocalFiat, or the scanned Fiat on a failed grab), so the property block is unchanged. Grab Time now goes out as a number instead of a string. The session delegate tests assert the event each grab, give, claim and cash-link share sends. TippingCoordinator lost its transfer call in #1463; only its unused Analytics import goes here. * refactor(analytics): move add money events onto the shared builders Every add-money call site now tracks an AddMoneyEvents builder instead of the legacy Analytics.AddMoney enums and FlipcashAnalyticsService methods. Fiat/LocalFiat amounts and thrown errors go through their .analytics converters, so each property block is unchanged except numbers going out as numbers instead of strings. Analytics.AddMoneySource, AddMoneyMethod, the AddMoneyEvent sealed interface, and the Mixpanel delegate overrides are dead now that every caller uses the shared builder, so they are removed. * refactor(analytics): move onramp events onto the shared builders The five contact-verification screens now track OnrampEvents.step(...) from the same LaunchedEffect. The events carry no properties, and a new test pins each step to the name the screens sent before. openOnramp, onrampPurchase, OnrampSource, OnrampPurchaseStep and their eight events had no callers and are deleted. * refactor(analytics): move wallet events onto the shared builders Wallet connect/request-amount/failed/cancel in SwapViewModel now go through WalletEvents instead of the bespoke FlipcashAnalyticsService methods, so amounts and providers convert through .analytics like the other builder call sites. transactionSubmittedToWallet had no caller and is deleted along with its event and property mapping. * refactor(analytics): move token info events onto the shared builders TokenInfoEntry.Discovery, Chat and ChatGate used to report as "Token Info: Opened From Wallet". They now send Opened From Discovery, Chat and Chat Gate, so the Wallet count drops by what those three entries carried. Deeplink and Wallet are unchanged. The mapping has no else branch, so a new TokenInfoEntry fails to compile until it is mapped. TokenInfoSource.Give and its event have had no caller since 6419a95 and are deleted. * refactor(analytics): move buy and sell events onto the shared builders SwapViewModel.trackTransaction now calls the generated SwapEvents.purchase and SwapEvents.sell builders instead of the ad-hoc Analytics.buy/sell path, dropping the local Analytics.SwapMethod/PurchaseMethod enums and the TokenTransactionEvent sealed hierarchy they drove. Fee is sent as a decimal (events.toml already defines it as `type = "decimal"`) instead of a string. * refactor(analytics): move scan events onto the shared builders Gallery scan (Scanner.kt), tip card scan (CodeScanDelegate) and tip card present (TipCardDelegate) now call analytics.track(ScanEvents.*) instead of the FlipcashAnalyticsService methods, which are deleted along with their Events.kt classes and MixpanelAnalyticsDelegate wiring. Tier, Zoom and Time go out as numbers and Exhausted as a flag, matching the shared builder; Scanner.kt has no test harness so its three call sites are reviewed by diff only. CodeScanDelegateTest and TipCardDelegateTest now assert against RecordingAnalytics with ScanEvents builders instead of verifying the legacy methods. * refactor(analytics): move deeplink events onto the shared builders App.kt, Scanner.kt and CashLinkDelegate.kt track the DeeplinkEvents builders instead of the legacy service methods. Two values change: - Type comes from DeeplinkType.analytics, an explicit string per subclass, instead of javaClass.simpleName. The strings match the source class names, but no keep rule covers DeeplinkType, so a minified release build may have sent R8-renamed names until now. - The parse-failure Error carries the URL without its query or fragment, which can hold secrets. CashLinkDelegateTest asserts the routed event, with the type as a string literal, on each early return and on success. * refactor(analytics): move display name events onto the shared builders Replaces FlipcashAnalyticsService.displayNameSubmitted(source, hadPreviousName) with DisplayNameEvents.set/updated from the generated builder, called directly from NameEntryViewModel. Deletes the old DisplayNameEvent.Set/Updated sealed classes from Events.kt, the interface/stub method from Analytics.kt, and the Mixpanel override, since nothing else referenced them. NameEntryViewModel now takes a DispatcherProvider (wired to BaseViewModel's defaultDispatcher) so its CheckName pipeline runs deterministically under a test dispatcher, matching WalletViewModel's existing pattern; needed to cover the branch through NameEntryViewModelTest with RecordingAnalytics. * refactor(analytics): move error modal events onto the shared builders NavMessagingEntryDecorator.kt tracks ErrorModalEvents.displayed instead of the legacy displayedErrorModal method. Name, keys (Title, Message, Screen, Call Site) and value types are unchanged; Screen and Call Site still drop from the map when null, matching :libs:analytics-events' ErrorModalEventsTest, which already pins that exact name and property map. The decorator has no test harness, so the call site was reviewed by diff against that existing builder test rather than given a new one. Deletes displayedErrorModal from FlipcashAnalyticsService, StubFlipcashAnalytics and MixpanelAnalyticsDelegate, and ErrorModalDisplayed from Events.kt. No dead declarations in this domain. * refactor(analytics): move button and account events onto the shared builders Button taps track ButtonEvents.tapped with the shared Button enum, and the account actions and paidForAccount track AccountEvents. Every event name is unchanged; Create Account Payment's Fiat goes out as a number. Actions.kt is gone, along with Button.AllowCamera and Button.Sell, which had no callers. Compose callers read the service through rememberAnalytics(), since LocalAnalytics is typed as the base service without track(). The login tests assert the tracked button events with RecordingAnalytics instead of verifying action() on a mock. * refactor(analytics): delete the legacy analytics service Every caller now goes through FlipcashAnalytics.track/increment, so FlipcashAnalyticsService, StubFlipcashAnalytics, Events.kt and :libs:analytics have no users left. LocalAnalytics moves into the Flipcash analytics module, typed as FlipcashAnalytics, with previews providing FlipcashAnalytics.None. The delegate's app-start trace went with the lifecycle hooks it lived in; nothing called those hooks. * test(chat): pin State: Failure on a failed chat send code/cash left successful at its default on this path, so a failed Sent Cash / Sent Tip reported State: Success; this branch sends Failure and the test keeps it that way. * refactor(analytics): move RecordingAnalytics into test fixtures It sat in the main source set, so it shipped in the app and production code could inject it. It now lives in the analytics module's testFixtures, which the nine modules whose tests use it pull in with testImplementation(testFixtures(...)), the same way :libs:coroutines and :ui:resources share their fakes. The fixture compilation needs the Compose BOM and ui on its classpath because the module applies the Compose compiler. * refactor(tipping): drop the unused analytics dependency from TippingCoordinator TippingCoordinator took a FlipcashAnalytics it never called, both before and after the track() migration. Remove the constructor parameter and the module's dependency on :apps:flipcash:shared:analytics. * test(messenger): run the chat send-failure test on the test dispatcher ChatViewModel dispatched events on the real Dispatchers.Default, so ChatSendFailureAnalyticsTest polled for up to 5s and could leave the send handler running after the test returned. CI then failed ReportViewModelTest, the next messenger test, with UncaughtExceptionsBeforeTest, which is how kotlinx-coroutines-test reports an exception thrown by an earlier test's leftover coroutine. It did not reproduce locally. Inject DispatcherProvider into ChatViewModel, as NameEntryViewModel does, and pass the test's dispatcher so no work outlives the test. Production still gets Dispatchers.Default.
1 parent 3e097f8 commit 2a0ae79

87 files changed

Lines changed: 1767 additions & 1559 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import androidx.fragment.app.FragmentActivity
1212
import androidx.lifecycle.lifecycleScope
1313
import kotlinx.coroutines.Dispatchers
1414
import kotlinx.coroutines.launch
15-
import com.flipcash.app.analytics.FlipcashAnalyticsService
15+
import com.flipcash.app.analytics.FlipcashAnalytics
1616
import com.flipcash.app.android.BuildConfig
1717
import com.flipcash.app.appsettings.AppSettingsCoordinator
1818
import com.flipcash.app.appsettings.LocalAppSettings
@@ -49,7 +49,7 @@ import com.flipcash.app.updates.AppUpdateController
4949
import com.flipcash.app.updates.LocalAppUpdater
5050
import com.flipcash.services.user.UserManager
5151
import com.flipcash.shared.tipping.TippingCoordinator
52-
import com.getcode.libs.analytics.LocalAnalytics
52+
import com.flipcash.app.analytics.LocalAnalytics
5353
import com.getcode.opencode.compose.LocalExchange
5454
import com.getcode.opencode.exchange.Exchange
5555
import com.getcode.ui.testing.LocalUiTesting
@@ -118,7 +118,7 @@ class MainActivity : FragmentActivity() {
118118
lateinit var featureFlagController: FeatureFlagController
119119

120120
@Inject
121-
lateinit var analytics: FlipcashAnalyticsService
121+
lateinit var analytics: FlipcashAnalytics
122122

123123
@Inject
124124
lateinit var phoneUtils: PhoneUtils
@@ -241,4 +241,4 @@ private fun Activity.handleUncaughtException() {
241241
killProcess(myPid())
242242
exitProcess(2)
243243
}
244-
}
244+
}

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ import androidx.navigation3.runtime.NavBackStack
3737
import androidx.navigation3.runtime.NavKey
3838
import androidx.navigation3.scene.OverlayScene
3939
import androidx.navigation3.scene.SinglePaneSceneStrategy
40+
import com.flipcash.analytics.events.DeeplinkEvents
4041
import com.flipcash.app.analytics.rememberAnalytics
42+
import com.flipcash.app.analytics.withoutQueryOrFragment
4143
import com.flipcash.app.android.BuildConfig
4244
import com.flipcash.app.bill.customization.BillPlaygroundScaffold
4345
import com.flipcash.app.cardexpand.CardExpansionController
@@ -117,7 +119,7 @@ internal fun App(
117119
var deeplinkHandled by remember { mutableStateOf(false) }
118120
val userManager = LocalUserManager.current!!
119121
DeepLinkListener {
120-
analytics.deeplinkOpened(it.data)
122+
analytics.track(DeeplinkEvents.open(it.data.withoutQueryOrFragment()))
121123
deepLink = it
122124
}
123125

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavMessagingEntryDecorator.kt‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.runtime.remember
66
import androidx.navigation3.runtime.NavBackStack
77
import androidx.navigation3.runtime.NavEntryDecorator
88
import androidx.navigation3.runtime.NavKey
9+
import com.flipcash.analytics.events.ErrorModalEvents
910
import com.flipcash.app.analytics.rememberAnalytics
1011
import com.flipcash.app.core.AppRoute
1112
import com.getcode.manager.BottomBarManager
@@ -46,11 +47,13 @@ fun NavMessagingEntryDecorator(
4647
BottomBarManager.BottomBarMessageType.DESTRUCTIVE -> Unit
4748
BottomBarManager.BottomBarMessageType.ERROR -> {
4849
val screen = backStack.lastOrNull()?.screenName()
49-
analytics.displayedErrorModal(
50-
title = message.title,
51-
message = message.subtitle,
52-
screen = screen,
53-
callSite = message.callSite,
50+
analytics.track(
51+
ErrorModalEvents.displayed(
52+
title = message.title,
53+
message = message.subtitle,
54+
screen = screen,
55+
callSite = message.callSite,
56+
)
5457
)
5558
}
5659
BottomBarManager.BottomBarMessageType.WARNING -> Unit

‎apps/flipcash/features/balance/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515

1616
implementation(project(":apps:flipcash:shared:transaction-history"))
1717
implementation(project(":apps:flipcash:shared:analytics"))
18+
testImplementation(testFixtures(project(":apps:flipcash:shared:analytics")))
1819
implementation(project(":apps:flipcash:shared:chat"))
1920
implementation(project(":apps:flipcash:shared:featureflags"))
2021
implementation(project(":apps:flipcash:shared:funding"))

‎apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.flipcash.app.balance.internal
22

33
import androidx.lifecycle.viewModelScope
4-
import com.flipcash.app.analytics.Analytics
5-
import com.flipcash.app.analytics.FlipcashAnalyticsService
4+
import com.flipcash.analytics.AddMoneySource
5+
import com.flipcash.analytics.events.AddMoneyEvents
6+
import com.flipcash.app.analytics.FlipcashAnalytics
67
import com.flipcash.app.core.ui.onboarding.TutorialItem
78
import com.flipcash.app.core.AppRoute
89
import com.flipcash.shared.transactionhistory.ActivityFeedCoordinator
@@ -35,7 +36,7 @@ internal class WalletViewModel @Inject constructor(
3536
userFlags: UserFlagsCoordinator,
3637
dispatchers: DispatcherProvider,
3738
purchaseMethodController: PurchaseMethodController,
38-
analytics: FlipcashAnalyticsService,
39+
analytics: FlipcashAnalytics,
3940
chatCoordinator: ChatCoordinator,
4041
feedCoordinator: ActivityFeedCoordinator,
4142
tokenCoordinator: TokenCoordinator,
@@ -176,7 +177,7 @@ internal class WalletViewModel @Inject constructor(
176177
eventFlow
177178
.filterIsInstance<Event.PresentDepositOptions>()
178179
.mapNotNull {
179-
analytics.addMoneyOpened(Analytics.AddMoneySource.Balance)
180+
analytics.track(AddMoneyEvents.opened(AddMoneySource.BALANCE))
180181
purchaseMethodController.presentDepositOptions(popToRoot = true) }
181182
.onEach { route -> dispatchEvent(Event.OpenScreen(route)) }
182183
.launchIn(viewModelScope)
@@ -243,4 +244,4 @@ internal class WalletViewModel @Inject constructor(
243244
}
244245
}
245246
}
246-
}
247+
}

‎apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceViewModelTest.kt‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.flipcash.app.balance.internal
22

33
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4-
import com.flipcash.app.analytics.StubFlipcashAnalytics
4+
import com.flipcash.analytics.AddMoneySource
5+
import com.flipcash.analytics.events.AddMoneyEvents
6+
import com.flipcash.app.analytics.RecordingAnalytics
57
import com.flipcash.app.core.AppRoute
68
import com.flipcash.app.core.MainCoroutineRule
79
import com.flipcash.app.core.dispatchers.TestDispatchers
@@ -55,14 +57,16 @@ class BalanceViewModelTest {
5557
every { pending } returns MutableStateFlow(null)
5658
}
5759

60+
private val analytics = RecordingAnalytics()
61+
5862
private lateinit var dispatchers: TestDispatchers
5963

6064
private fun createViewModel() = WalletViewModel(
6165
userManager = userManager,
6266
userFlags = userFlags,
6367
dispatchers = dispatchers,
6468
purchaseMethodController = purchaseMethodController,
65-
analytics = StubFlipcashAnalytics(),
69+
analytics = analytics,
6670
chatCoordinator = chatCoordinator,
6771
feedCoordinator = feedCoordinator,
6872
tokenCoordinator = tokenCoordinator,
@@ -83,6 +87,7 @@ class BalanceViewModelTest {
8387
// The removed AddMoneyUX flag used to short-circuit to a plain Deposit route;
8488
// the add-money sheet must now always be presented.
8589
coVerify(exactly = 1) { purchaseMethodController.presentDepositOptions(popToRoot = true) }
90+
assertEquals(listOf(AddMoneyEvents.opened(AddMoneySource.BALANCE)), analytics.events)
8691
}
8792

8893
@Test

‎apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/WalletMilestoneGatingTest.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.flipcash.app.balance.internal
22

33
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4-
import com.flipcash.app.analytics.StubFlipcashAnalytics
4+
import com.flipcash.app.analytics.FlipcashAnalytics
55
import com.flipcash.app.core.ui.onboarding.TutorialItem
66
import com.flipcash.app.core.MainCoroutineRule
77
import com.flipcash.app.core.dispatchers.TestDispatchers
@@ -67,7 +67,7 @@ class WalletMilestoneGatingTest {
6767
userFlags = mockk<UserFlagsCoordinator>(relaxed = true),
6868
dispatchers = dispatchers,
6969
purchaseMethodController = mockk<PurchaseMethodController>(relaxed = true),
70-
analytics = StubFlipcashAnalytics(),
70+
analytics = FlipcashAnalytics.None,
7171
chatCoordinator = chatCoordinator,
7272
feedCoordinator = feedCoordinator,
7373
tokenCoordinator = tokenCoordinator,

‎apps/flipcash/features/cash/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation(libs.kotlin.stdlib)
1717
implementation(project(":apps:flipcash:shared:amount-entry"))
1818
implementation(project(":apps:flipcash:shared:analytics"))
19+
testImplementation(testFixtures(project(":apps:flipcash:shared:analytics")))
1920
implementation(project(":apps:flipcash:shared:featureflags"))
2021
implementation(project(":apps:flipcash:shared:session"))
2122
implementation(project(":apps:flipcash:shared:tokens"))

‎apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.flipcash.app.cash.internal
22

33
import androidx.lifecycle.viewModelScope
4-
import com.flipcash.app.analytics.Analytics
5-
import com.flipcash.app.analytics.FlipcashAnalyticsService
4+
import com.flipcash.analytics.AddMoneySource
5+
import com.flipcash.analytics.events.AddMoneyEvents
6+
import com.flipcash.app.analytics.FlipcashAnalytics
67
import com.flipcash.app.core.AppRoute
78
import com.flipcash.app.core.bill.Scannable
89
import com.flipcash.app.core.tokens.SwapPurpose
@@ -58,7 +59,7 @@ internal class CashScreenViewModel @Inject constructor(
5859
private val verifiedFiatCalculator: VerifiedFiatCalculator,
5960
tokenCoordinator: TokenCoordinator,
6061
transactionController: TransactionOperations,
61-
analytics: FlipcashAnalyticsService,
62+
analytics: FlipcashAnalytics,
6263
dispatchers: DispatcherProvider,
6364
) : BaseViewModel<CashScreenViewModel.State, CashScreenViewModel.Event>(
6465
initialState = State(),
@@ -289,7 +290,7 @@ internal class CashScreenViewModel @Inject constructor(
289290
// route directly to the swap amount screen, skipping token info
290291
val mint = stateFlow.value.selectedTokenAddress!!
291292
if (mint == Mint.usdf) {
292-
analytics.addMoneyOpened(Analytics.AddMoneySource.GiveShortfall)
293+
analytics.track(AddMoneyEvents.opened(AddMoneySource.GIVE_SHORTFALL))
293294
}
294295
dispatchEvent(
295296
Event.OpenScreen(
@@ -340,4 +341,4 @@ internal class CashScreenViewModel @Inject constructor(
340341
}
341342
}
342343
}
343-
}
344+
}

‎apps/flipcash/features/cash/src/test/kotlin/com/flipcash/app/cash/internal/CashScreenViewModelTest.kt‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.flipcash.app.cash.internal
22

33
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4-
import com.flipcash.app.analytics.StubFlipcashAnalytics
4+
import com.flipcash.analytics.AddMoneySource
5+
import com.flipcash.analytics.events.AddMoneyEvents
6+
import com.flipcash.app.analytics.RecordingAnalytics
57
import com.flipcash.app.core.MainCoroutineRule
68
import com.flipcash.app.core.dispatchers.TestDispatchers
79
import com.flipcash.app.tokens.TokenCoordinator
@@ -51,6 +53,8 @@ class CashScreenViewModelTest {
5153
private val tokenCoordinator: TokenCoordinator = mockk(relaxed = true)
5254
private val transactionController: TransactionOperations = mockk(relaxed = true)
5355

56+
private val analytics = RecordingAnalytics()
57+
5458
private lateinit var dispatchers: TestDispatchers
5559

5660
private val updateStateForEvent = CashScreenViewModel.updateStateForEvent
@@ -79,7 +83,7 @@ class CashScreenViewModelTest {
7983
verifiedFiatCalculator = verifiedFiatCalculator,
8084
tokenCoordinator = tokenCoordinator,
8185
transactionController = transactionController,
82-
analytics = StubFlipcashAnalytics(),
86+
analytics = analytics,
8387
dispatchers = dispatchers,
8488
)
8589
}
@@ -266,4 +270,26 @@ class CashScreenViewModelTest {
266270
val result = vm.checkSendLimit()
267271
assertFalse(result, "checkSendLimit should return false when amount is within limit")
268272
}
273+
274+
@Test
275+
fun `a USDF shortfall tracks Add Money Opened from Give Shortfall`() = runTest(mainCoroutineRule.dispatcher) {
276+
dispatchers = TestDispatchers(testScheduler)
277+
val vm = createViewModel()
278+
vm.dispatchEvent(CashScreenViewModel.Event.OnTokenSelected(Mint.usdf))
279+
vm.dispatchEvent(CashScreenViewModel.Event.AddCashToWallet(Fiat(5.0)))
280+
advanceUntilIdle()
281+
282+
assertEquals(listOf(AddMoneyEvents.opened(AddMoneySource.GIVE_SHORTFALL)), analytics.events)
283+
}
284+
285+
@Test
286+
fun `a shortfall in another token tracks nothing`() = runTest(mainCoroutineRule.dispatcher) {
287+
dispatchers = TestDispatchers(testScheduler)
288+
val vm = createViewModel()
289+
vm.dispatchEvent(CashScreenViewModel.Event.OnTokenSelected(Mint("kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6")))
290+
vm.dispatchEvent(CashScreenViewModel.Event.AddCashToWallet(Fiat(5.0)))
291+
advanceUntilIdle()
292+
293+
assertTrue(analytics.events.isEmpty())
294+
}
269295
}

0 commit comments

Comments
 (0)