diff --git a/v4/app/src/main/java/exchange/dydx/trading/AppModule.kt b/v4/app/src/main/java/exchange/dydx/trading/AppModule.kt index 3a681609..166977b2 100644 --- a/v4/app/src/main/java/exchange/dydx/trading/AppModule.kt +++ b/v4/app/src/main/java/exchange/dydx/trading/AppModule.kt @@ -161,7 +161,7 @@ interface AppModule { @Binds fun bindTrackingProtocol(abacusTrackingImp: AbacusTrackingImp): TrackingProtocol - @Binds fun bindTracking(abacusTrackingImp: AbacusTrackingImp): Tracking + @Binds fun bindTracking(compositeTracking: CompositeTracking): Tracking @Binds fun bindThreading(abacusThreadingImp: AbacusThreadingImp): ThreadingProtocol diff --git a/v4/core/src/main/java/exchange/dydx/trading/core/AnalyticsSetup.kt b/v4/core/src/main/java/exchange/dydx/trading/core/AnalyticsSetup.kt index 9aead019..2eb8aae4 100644 --- a/v4/core/src/main/java/exchange/dydx/trading/core/AnalyticsSetup.kt +++ b/v4/core/src/main/java/exchange/dydx/trading/core/AnalyticsSetup.kt @@ -3,6 +3,7 @@ package exchange.dydx.trading.core import androidx.fragment.app.FragmentActivity import com.amplitude.android.Amplitude import com.amplitude.android.Configuration +import com.amplitude.android.DefaultTrackingOptions import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase import exchange.dydx.trading.common.R @@ -27,6 +28,7 @@ object AnalyticsSetup { Configuration( apiKey = activity.applicationContext.getString(R.string.amplitude_api_key), context = activity.applicationContext, + defaultTracking = DefaultTrackingOptions.ALL, ), ) compositeTracking.addTracker(AmplitudeTracker(amplitude)) diff --git a/v4/core/src/main/java/exchange/dydx/trading/core/CoreViewModel.kt b/v4/core/src/main/java/exchange/dydx/trading/core/CoreViewModel.kt index 3fe3cd25..b26a4236 100644 --- a/v4/core/src/main/java/exchange/dydx/trading/core/CoreViewModel.kt +++ b/v4/core/src/main/java/exchange/dydx/trading/core/CoreViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.AbacusLocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.platformui.components.PlatformInfo @@ -30,7 +30,7 @@ class CoreViewModel @Inject constructor( val cosmosClient: CosmosV4WebviewClientProtocol, val platformInfo: PlatformInfo, private val abacusStateManager: AbacusStateManagerProtocol, - private val localizer: LocalizerProtocol, + private val localizer: AbacusLocalizerProtocol, @ApplicationContext context: Context, private val cachedFileLoader: CachedFileLoader, private val formatter: DydxFormatter, diff --git a/v4/core/src/main/java/exchange/dydx/trading/core/DydxRouterImpl.kt b/v4/core/src/main/java/exchange/dydx/trading/core/DydxRouterImpl.kt index 9ee1e17a..d231b99d 100644 --- a/v4/core/src/main/java/exchange/dydx/trading/core/DydxRouterImpl.kt +++ b/v4/core/src/main/java/exchange/dydx/trading/core/DydxRouterImpl.kt @@ -84,15 +84,7 @@ class DydxRouterImpl @Inject constructor( val destinationRoute = destination.route if (destinationRoute != null) { - - val trackingData: MutableMap = mutableMapOf() - destination.arguments.keys.forEach { key -> - trackingData[key] = arguments?.getString(key) ?: "" - } - tracker.log( - event = destinationRoute, - data = trackingData, - ) + trackRoute(destinationRoute, destination, arguments) if (tabRoutes.contains(destinationRoute)) { routeQueue.clear() @@ -227,4 +219,24 @@ class DydxRouterImpl @Inject constructor( } return route } + + private fun trackRoute(destinationRoute: String, destination: NavDestination, arguments: Bundle?) { + val trackingData: MutableMap = mutableMapOf() + destination.arguments.keys.forEach { key -> + trackingData[key] = arguments?.getString(key) ?: "" + } + // Remove query parameters from the route and remove the last component if it's a dynamic route + var sanitizedRoute = destinationRoute.split("?").first() + val components = sanitizedRoute.split("/") + val lastComponent = components.last() + if (lastComponent.startsWith("{") && lastComponent.endsWith("}")) { + sanitizedRoute = components.dropLast(1).joinToString("_") + } else { + sanitizedRoute = components.joinToString("_") + } + tracker.log( + event = sanitizedRoute, + data = trackingData, + ) + } } diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectViewModel.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectViewModel.kt index 5d711001..b972aac1 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectViewModel.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectViewModel.kt @@ -16,6 +16,8 @@ import exchange.dydx.platformui.components.PlatformInfo import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes +import exchange.dydx.trading.feature.shared.analytics.OnboardingAnalytics +import exchange.dydx.trading.feature.shared.analytics.WalletAnalytics import exchange.dydx.trading.feature.shared.views.ProgressStepView import exchange.dydx.trading.integration.cosmos.CosmosV4ClientProtocol import kotlinx.coroutines.flow.Flow @@ -34,6 +36,8 @@ class DydxOnboardConnectViewModel @Inject constructor( val abacusStateManager: AbacusStateManagerProtocol, val platformInfo: PlatformInfo, private val mutableSetupStatusFlow: MutableStateFlow, + private val onboardingAnalytics: OnboardingAnalytics, + private val walletAnalytics: WalletAnalytics, savedStateHandle: SavedStateHandle, ) : ViewModel(), DydxViewModel { @@ -69,6 +73,9 @@ class DydxOnboardConnectViewModel @Inject constructor( } } is DydxWalletSetup.Status.Signed -> { + onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.KEY_DERIVATION) + walletAnalytics.logConnected(walletId) + _state.update { state -> state.copy( steps = listOf(step1(status = ProgressStepView.Status.Completed), step2(status = ProgressStepView.Status.Completed)), diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt index cd5c340a..547fea25 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt @@ -33,14 +33,12 @@ fun Preview_dydxDesktopScanView() { object DydxDesktopScanView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, - val text: String?, val closeButtonHandler: () -> Unit = {}, val qrCodeScannedHandler: (String) -> Unit = {}, ) { companion object { val preview = ViewState( localizer = MockLocalizer(), - text = "1.0M", ) } } diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanViewModel.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanViewModel.kt index b7778f74..4679ba2d 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanViewModel.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanViewModel.kt @@ -2,7 +2,6 @@ package exchange.dydx.feature.onboarding.desktopscan import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import exchange.dydx.abacus.output.PerpetualMarketSummary import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol @@ -13,8 +12,10 @@ import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.PortfolioRoutes +import exchange.dydx.trading.feature.shared.analytics.OnboardingAnalytics +import exchange.dydx.trading.feature.shared.analytics.WalletAnalytics import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject @@ -30,19 +31,15 @@ class DydxDesktopScanViewModel @Inject constructor( val platformDialog: PlatformDialog, val platformInfo: PlatformInfo, val starkexLib: StarkexLib, + private val onboardingAnalytics: OnboardingAnalytics, + private val walletAnalytics: WalletAnalytics, ) : ViewModel(), DydxViewModel { - val state: Flow = abacusStateManager.state.marketSummary - .map { - createViewState(it) - } - .distinctUntilChanged() + val state: Flow = MutableStateFlow(createViewState()) - private fun createViewState(marketSummary: PerpetualMarketSummary?): DydxDesktopScanView.ViewState { - val volume = formatter.dollarVolume(marketSummary?.volume24HUSDC) + private fun createViewState(): DydxDesktopScanView.ViewState { return DydxDesktopScanView.ViewState( localizer = localizer, - text = volume, closeButtonHandler = { router.navigateBack() }, @@ -73,6 +70,8 @@ class DydxDesktopScanViewModel @Inject constructor( val mnemonic = parser.asString(map["mnemonic"]) val cosmosAddress = parser.asString(map["cosmosAddress"]) if (mnemonic != null && cosmosAddress != null) { + onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.KEY_DERIVATION) + walletAnalytics.logConnected(null) abacusStateManager.setV4( ethereumAddress = "", mnemonic = mnemonic, diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/tos/DydxTosViewModel.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/tos/DydxTosViewModel.kt index ac0f253f..83fc9a6c 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/tos/DydxTosViewModel.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/tos/DydxTosViewModel.kt @@ -7,6 +7,7 @@ import exchange.dydx.dydxCartera.DydxWalletSetup import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.feature.shared.analytics.OnboardingAnalytics import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -19,6 +20,7 @@ class DydxTosViewModel @Inject constructor( private val abacusStateManager: AbacusStateManagerProtocol, private val router: DydxRouter, private val setupStatusFlow: StateFlow, + private val onboardingAnalytics: OnboardingAnalytics, ) : ViewModel(), DydxViewModel { val state: Flow = @@ -52,6 +54,8 @@ class DydxTosViewModel @Inject constructor( mnemonic = mnemonic, ) } + + onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.ACKNOWLEDGE_TERMS) } router.navigateBack() diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/welcome/DydxOnboardWelcomeViewModel.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/welcome/DydxOnboardWelcomeViewModel.kt index 7ffb7d79..f05f970a 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/welcome/DydxOnboardWelcomeViewModel.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/welcome/DydxOnboardWelcomeViewModel.kt @@ -7,6 +7,7 @@ import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes +import exchange.dydx.trading.feature.shared.analytics.OnboardingAnalytics import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @@ -16,6 +17,7 @@ class DydxOnboardWelcomeViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val router: DydxRouter, + private val onboardingAnalytics: OnboardingAnalytics, ) : ViewModel(), DydxViewModel { val state: Flow = flowOf(createViewState()) @@ -24,6 +26,7 @@ class DydxOnboardWelcomeViewModel @Inject constructor( return DydxOnboardWelcomeView.ViewState( localizer = localizer, ctaAction = { + onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.CHOOSE_WALLET) router.navigateBack() router.navigateTo( route = OnboardingRoutes.wallet_list, diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileButtonsViewModel.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileButtonsViewModel.kt index 1f9eaf32..81e94222 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileButtonsViewModel.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileButtonsViewModel.kt @@ -10,6 +10,7 @@ import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.ProfileRoutes +import exchange.dydx.trading.feature.shared.analytics.WalletAnalytics import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -21,6 +22,7 @@ class DydxProfileButtonsViewModel @Inject constructor( private val abacusStateManager: AbacusStateManagerProtocol, private val router: DydxRouter, private val logoutDialog: PlatformDialog, + private val walletAnalytics: WalletAnalytics, ) : ViewModel(), DydxViewModel { val state: Flow = @@ -55,6 +57,7 @@ class DydxProfileButtonsViewModel @Inject constructor( cancelTitle = localizer.localize("APP.GENERAL.CANCEL"), confirmTitle = localizer.localize("APP.GENERAL.SIGN_OUT"), confirmAction = { + walletAnalytics.logDisconnected(currentWallet?.walletId) abacusStateManager.logOut() }, ) diff --git a/v4/feature/shared/build.gradle b/v4/feature/shared/build.gradle index 22e026d6..2e957c96 100644 --- a/v4/feature/shared/build.gradle +++ b/v4/feature/shared/build.gradle @@ -43,9 +43,12 @@ dependencies { implementation project(path: ':v4:common') implementation project(path: ':v4:utilities') implementation project(path: ':v4:integration:dydxStateManager') + implementation project(path: ':v4:integration:analytics') implementation project(path: ':v4:platformUI') implementation project(':v4:integration:chart') + implementation "dydxprotocol:cartera-android:$carteraVersion" + testImplementation "junit:junit:$junitVersion" // androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeVersion" diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/AnalyticsEvent.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/AnalyticsEvent.kt new file mode 100644 index 00000000..cc149e20 --- /dev/null +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/AnalyticsEvent.kt @@ -0,0 +1,53 @@ +package exchange.dydx.trading.feature.shared.analytics + +// +// Events defined in the v4-web repo. Ideally, we should keep this in-sync with v4-web +// +enum class AnalyticsEvent(val rawValue: String) { + // App + APP_START("AppStart"), + NETWORK_STATUS("NetworkStatus"), + + // Navigation + NAVIGATE_PAGE("NavigatePage"), + NAVIGATE_DIALOG("NavigateDialog"), + NAVIGATE_DIALOG_CLOSE("NavigateDialogClose"), + NAVIGATE_EXTERNAL("NavigateExternal"), + + // Wallet + CONNECT_WALLET("ConnectWallet"), + DISCONNECT_WALLET("DisconnectWallet"), + + // Onboarding + ONBOARDING_STEP_CHANGED("OnboardingStepChanged"), + ONBOARDING_ACCOUNT_DERIVED("OnboardingAccountDerived"), + ONBOARDING_WALLET_IS_NON_DETERMINISTIC("OnboardingWalletIsNonDeterministic"), + + // Transfers + TRANSFER_FAUCET("TransferFaucet"), + TRANSFER_FAUCET_CONFIRMED("TransferFaucetConfirmed"), + TRANSFER_DEPOSIT("TransferDeposit"), + TRANSFER_WITHDRAW("TransferWithdraw"), + + // Trading + TRADE_ORDER_TYPE_SELECTED("TradeOrderTypeSelected"), + TRADE_PLACE_ORDER("TradePlaceOrder"), + TRADE_PLACE_ORDER_CONFIRMED("TradePlaceOrderConfirmed"), + TRADE_CANCEL_ORDER("TradeCancelOrder"), + TRADE_CANCEL_ORDER_CONFIRMED("TradeCancelOrderConfirmed"), + + // Notification + NOTIFICATION_ACTION("NotificationAction") +} + +// +// User properties to be sent to the analytics service +// +enum class UserProperty(val rawValue: String) { + walletAddress("walletAddress"), + walletType("walletType"), + network("network"), + selectedLocale("selectedLocale"), + dydxAddress("dydxAddress"), + subaccountNumber("subaccountNumber") +} diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/OnboardingAnalytics.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/OnboardingAnalytics.kt new file mode 100644 index 00000000..41d0133c --- /dev/null +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/OnboardingAnalytics.kt @@ -0,0 +1,72 @@ +package exchange.dydx.trading.feature.shared.analytics + +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.integration.analytics.Tracking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.plus +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class OnboardingAnalytics @Inject constructor( + private val tracker: Tracking, + private val abacusStateManager: AbacusStateManagerProtocol, +) { + // The three main OnboardingStates: + // - Disconnected + // - WalletConnected + // - AccountConnected + private enum class OnboardingState(val rawValue: String) { + // User is disconnected. + DISCONNECTED("Disconnected"), + + // Wallet is connected. + WALLET_CONNECTED("WalletConnected"), + + // Account is connected. + ACCOUNT_CONNECTED("AccountConnected") + } + + // Enum representing the various steps in the onboarding process. + enum class OnboardingSteps(val rawValue: String) { + // Step: Choose Wallet + CHOOSE_WALLET("ChooseWallet"), + + // Step: Key Derivation + KEY_DERIVATION("KeyDerivation"), + + // Step: Acknowledge Terms + ACKNOWLEDGE_TERMS("AcknowledgeTerms"), + + // Step: Deposit Funds + DEPOSIT_FUNDS("DepositFunds") + } + + private val scope = MainScope() + Dispatchers.IO + + fun log(step: OnboardingSteps) { + abacusStateManager.state.currentWallet + .take(1) + .onEach { + val state = when { + it == null -> OnboardingState.DISCONNECTED + it.cosmoAddress.isNullOrEmpty() -> OnboardingState.ACCOUNT_CONNECTED + else -> OnboardingState.WALLET_CONNECTED + } + + val data = mapOf( + "state" to state.rawValue, + "step" to step.rawValue, + ) + tracker.log( + event = AnalyticsEvent.ONBOARDING_STEP_CHANGED.rawValue, + data = data, + ) + } + .launchIn(scope) + } +} diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/TransferAnalytics.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/TransferAnalytics.kt new file mode 100644 index 00000000..10be3c41 --- /dev/null +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/TransferAnalytics.kt @@ -0,0 +1,51 @@ +package exchange.dydx.trading.feature.shared.analytics + +import exchange.dydx.abacus.output.input.TransferInput +import exchange.dydx.abacus.output.input.TransferInputChainResource +import exchange.dydx.abacus.output.input.TransferInputTokenResource +import exchange.dydx.abacus.utils.filterNotNull +import exchange.dydx.trading.integration.analytics.Tracking +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TransferAnalytics @Inject constructor( + private val tracker: Tracking, +) { + fun logDeposit(transferInput: TransferInput) { + log(event = AnalyticsEvent.TRANSFER_DEPOSIT, transferInput = transferInput) + } + + fun logWithdrawal(transferInput: TransferInput) { + log(event = AnalyticsEvent.TRANSFER_WITHDRAW, transferInput = transferInput) + } + private fun log(event: AnalyticsEvent, transferInput: TransferInput) { + val data: Map = mapOf( + "chainId" to transferInput.chainResource?.chainId?.toString(), + "tokenAddress" to transferInput.tokenResource?.address, + "tokenSymbol" to transferInput.tokenResource?.symbol, + "slippage" to transferInput.summary?.slippage.toString(), + "gasFee" to transferInput.summary?.gasFee.toString(), + "bridgeFee" to transferInput.summary?.bridgeFee.toString(), + "exchangeRate" to transferInput.summary?.exchangeRate.toString(), + "estimatedRouteDuration" to transferInput.summary?.estimatedRouteDuration.toString(), + "toAmount" to transferInput.summary?.toAmount.toString(), + "toAmountMin" to transferInput.summary?.toAmountMin.toString(), + ).filterNotNull() + + tracker.log( + event = event.rawValue, + data = data, + ) + } +} + +private val TransferInput.chainResource: TransferInputChainResource? + get() = chain?.let { chain -> + resources?.chainResources?.get(chain) + } + +private val TransferInput.tokenResource: TransferInputTokenResource? + get() = token?.let { token -> + resources?.tokenResources?.get(token) + } diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/WalletAnalytics.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/WalletAnalytics.kt new file mode 100644 index 00000000..7f9b30be --- /dev/null +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/analytics/WalletAnalytics.kt @@ -0,0 +1,33 @@ +package exchange.dydx.trading.feature.shared.analytics + +import exchange.dydx.abacus.utils.filterNotNull +import exchange.dydx.cartera.CarteraConfig +import exchange.dydx.trading.integration.analytics.Tracking +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WalletAnalytics @Inject constructor( + private val tracker: Tracking, +) { + fun logConnected(walletId: String?) { + log(AnalyticsEvent.CONNECT_WALLET, walletId) + } + + fun logDisconnected(walletId: String?) { + log(AnalyticsEvent.DISCONNECT_WALLET, walletId) + } + + private fun log(event: AnalyticsEvent, walletId: String?) { + val wallet = CarteraConfig.shared?.wallets?.firstOrNull { it.id == walletId } + val walletName = wallet?.userFields?.get("analyticEvent") ?: wallet?.name + val data: Map = mapOf( + "walletType" to walletName, + ).filterNotNull() + + tracker.log( + event = event.rawValue, + data = data, + ) + } +} diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt index 798b99da..1e44ae4f 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt @@ -18,6 +18,8 @@ import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.TransferRoutes +import exchange.dydx.trading.feature.shared.analytics.OnboardingAnalytics +import exchange.dydx.trading.feature.shared.analytics.TransferAnalytics import exchange.dydx.trading.feature.shared.views.InputCtaButton import exchange.dydx.trading.feature.transfer.DydxTransferError import exchange.dydx.trading.feature.transfer.utils.DydxTransferInstanceStoring @@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take import javax.inject.Inject @HiltViewModel @@ -41,6 +44,8 @@ class DydxTransferDepositCtaButtonModel @Inject constructor( @ApplicationContext private val context: Context, private val transferInstanceStore: DydxTransferInstanceStoring, private val errorFlow: MutableStateFlow<@JvmSuppressWildcards DydxTransferError?>, + private val onboardingAnalytics: OnboardingAnalytics, + private val transferAnalytics: TransferAnalytics, ) : ViewModel(), DydxViewModel { private val carteraProvider: CarteraProvider = CarteraProvider(context) private val isSubmittingFlow: MutableStateFlow = MutableStateFlow(false) @@ -151,6 +156,8 @@ class DydxTransferDepositCtaButtonModel @Inject constructor( val hash = eventResult.result val error = eventResult.error if (hash != null) { + sendOnboardingAnalytics() + transferAnalytics.logDeposit(transferInput) abacusStateManager.resetTransferInputFields() transferInstanceStore.addTransferHash( hash = hash, @@ -171,4 +178,16 @@ class DydxTransferDepositCtaButtonModel @Inject constructor( } .launchIn(viewModelScope) } + + private fun sendOnboardingAnalytics() { + abacusStateManager.state.hasAccount + .take(1) + .onEach { hasAccount -> + // only log for newly onboarded users (i.e., user without an account) + if (!hasAccount) { + onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.DEPOSIT_FUNDS) + } + } + .launchIn(viewModelScope) + } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt index 611cbc4e..afafb697 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt @@ -17,6 +17,7 @@ import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.TransferRoutes import exchange.dydx.trading.feature.shared.DydxScreenResult +import exchange.dydx.trading.feature.shared.analytics.TransferAnalytics import exchange.dydx.trading.feature.shared.views.InputCtaButton import exchange.dydx.trading.feature.transfer.DydxTransferError import exchange.dydx.trading.feature.transfer.steps.DydxTransferScreenStep @@ -47,6 +48,7 @@ class DydxTransferWithdrawalCtaButtonModel @Inject constructor( private val cosmosClient: CosmosV4WebviewClientProtocol, private val errorFlow: MutableStateFlow<@JvmSuppressWildcards DydxTransferError?>, private val transferInstanceStore: DydxTransferInstanceStoring, + private val transferAnalytics: TransferAnalytics, ) : ViewModel(), DydxViewModel { private val isSubmittingFlow: MutableStateFlow = MutableStateFlow(false) @@ -187,6 +189,7 @@ class DydxTransferWithdrawalCtaButtonModel @Inject constructor( val error = eventResult.error if (hash != null) { + transferAnalytics.logWithdrawal(transferInput) transferInstanceStore.addTransferHash( hash = hash, fromChainName = abacusStateManager.environment?.chainName, diff --git a/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/DydxGlobalWorkers.kt b/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/DydxGlobalWorkers.kt index 94a7f6b0..4fbe3969 100644 --- a/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/DydxGlobalWorkers.kt +++ b/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/DydxGlobalWorkers.kt @@ -1,7 +1,7 @@ package exchange.dydx.trading.feature.workers import android.content.Context -import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.AbacusLocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.platformui.components.PlatformInfo @@ -13,6 +13,7 @@ import exchange.dydx.trading.feature.workers.globalworkers.DydxCarteraConfigWork import exchange.dydx.trading.feature.workers.globalworkers.DydxRestrictionsWorker import exchange.dydx.trading.feature.workers.globalworkers.DydxTransferSubaccountWorker import exchange.dydx.trading.feature.workers.globalworkers.DydxUpdateWorker +import exchange.dydx.trading.feature.workers.globalworkers.DydxUserTrackingWorker import exchange.dydx.trading.integration.analytics.Tracking import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol import exchange.dydx.utilities.utils.CachedFileLoader @@ -22,7 +23,7 @@ import kotlinx.coroutines.CoroutineScope class DydxGlobalWorkers( override val scope: CoroutineScope, private val abacusStateManager: AbacusStateManagerProtocol, - private val localizer: LocalizerProtocol, + private val localizer: AbacusLocalizerProtocol, private val router: DydxRouter, private val platformInfo: PlatformInfo, private val context: Context, @@ -40,6 +41,7 @@ class DydxGlobalWorkers( DydxRestrictionsWorker(scope, abacusStateManager, localizer, platformInfo), DydxCarteraConfigWorker(scope, abacusStateManager, cachedFileLoader, context), DydxTransferSubaccountWorker(scope, abacusStateManager, cosmosClient, formatter, parser, tracker), + DydxUserTrackingWorker(scope, abacusStateManager, localizer, tracker), ) override var isStarted = false diff --git a/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/globalworkers/DydxUserTrackingWorker.kt b/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/globalworkers/DydxUserTrackingWorker.kt new file mode 100644 index 00000000..9d3dccea --- /dev/null +++ b/v4/feature/workers/src/main/java/exchange/dydx/trading/feature/workers/globalworkers/DydxUserTrackingWorker.kt @@ -0,0 +1,77 @@ +package exchange.dydx.trading.feature.workers.globalworkers + +import exchange.dydx.abacus.protocols.AbacusLocalizerProtocol +import exchange.dydx.cartera.CarteraConfig +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.feature.shared.analytics.UserProperty +import exchange.dydx.trading.integration.analytics.Tracking +import exchange.dydx.utilities.utils.WorkerProtocol +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +class DydxUserTrackingWorker( + override val scope: CoroutineScope, + private val abacusStateManager: AbacusStateManagerProtocol, + private val localizer: AbacusLocalizerProtocol, + private val tracker: Tracking, +) : WorkerProtocol { + override var isStarted = false + + override fun start() { + if (!isStarted) { + isStarted = true + + abacusStateManager.currentEnvironmentId + .filterNotNull() + .onEach { + tracker.setUserProperties( + mapOf( + UserProperty.network.rawValue to it, + ), + ) + } + .launchIn(scope) + + abacusStateManager.state.currentWallet + .onEach { + tracker.setUserId(it?.ethereumAddress ?: it?.cosmoAddress) + val wallet = CarteraConfig.shared?.wallets?.firstOrNull { wallet -> wallet.id == it?.walletId } + tracker.setUserProperties( + mapOf( + UserProperty.walletType.rawValue to wallet?.userFields?.get("analyticEvent"), + UserProperty.dydxAddress.rawValue to it?.cosmoAddress, + ), + ) + } + .launchIn(scope) + + abacusStateManager.state.selectedSubaccount + .map { it?.subaccountNumber } + .distinctUntilChanged() + .onEach { + tracker.setUserProperties( + mapOf( + UserProperty.subaccountNumber.rawValue to it?.let { subaccountNumber -> subaccountNumber?.toString() }, + ), + ) + } + .launchIn(scope) + + tracker.setUserProperties( + mapOf( + UserProperty.selectedLocale.rawValue to localizer.language, + ), + ) + } + } + + override fun stop() { + if (isStarted) { + isStarted = false + } + } +} diff --git a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/AmplitudeTracker.kt b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/AmplitudeTracker.kt index fad34bec..446ae5be 100644 --- a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/AmplitudeTracker.kt +++ b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/AmplitudeTracker.kt @@ -1,11 +1,27 @@ package exchange.dydx.trading.integration.analytics import com.amplitude.android.Amplitude +import com.amplitude.android.events.Identify import exchange.dydx.utilities.utils.jsonStringToMap class AmplitudeTracker( private val amplitude: Amplitude ) : Tracking { + override fun setUserId(userId: String?) { + amplitude.setUserId(userId) + } + + override fun setUserProperties(properties: Map) { + val identify = Identify() + properties.forEach { (key, value) -> + if (value == null) { + identify.unset(key) + } else { + identify.set(key, value) + } + } + amplitude.identify(identify) + } override fun log(event: String, data: String?) { val jsonMap = data?.jsonStringToMap() diff --git a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/CompositeTracker.kt b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/CompositeTracker.kt index 9db3b12b..4d28cbf0 100644 --- a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/CompositeTracker.kt +++ b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/CompositeTracker.kt @@ -15,6 +15,14 @@ class CompositeTracker @Inject constructor() : CompositeTracking { trackers.remove(tracker) } + override fun setUserId(userId: String?) { + trackers.forEach { it.setUserId(userId) } + } + + override fun setUserProperties(properties: Map) { + trackers.forEach { it.setUserProperties(properties) } + } + override fun log(event: String, data: String?) { trackers.forEach { it.log(event, data) } } diff --git a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/FirebaseTracker.kt b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/FirebaseTracker.kt index d5bf2510..70332a67 100644 --- a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/FirebaseTracker.kt +++ b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/FirebaseTracker.kt @@ -7,6 +7,15 @@ import exchange.dydx.utilities.utils.jsonStringToMap class FirebaseTracker( private val firebaseAnalytics: FirebaseAnalytics ) : Tracking { + override fun setUserId(userId: String?) { + firebaseAnalytics.setUserId(userId) + } + + override fun setUserProperties(properties: Map) { + properties.forEach { (key, value) -> + firebaseAnalytics.setUserProperty(key, value) + } + } override fun log(event: String, data: String?) { firebaseAnalytics.logEvent(event) { diff --git a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/Tracking.kt b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/Tracking.kt index c8e7a180..381077e5 100644 --- a/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/Tracking.kt +++ b/v4/integration/analytics/src/main/java/exchange/dydx/trading/integration/analytics/Tracking.kt @@ -4,6 +4,11 @@ import exchange.dydx.abacus.protocols.TrackingProtocol import exchange.dydx.abacus.utils.toJson interface Tracking : TrackingProtocol { + + fun setUserId(userId: String?) + + fun setUserProperties(properties: Map) + override fun log(event: String, data: String?) fun log(event: String, data: Map) { diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusTrackingImp.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusTrackingImp.kt index 8b527e1f..c46ceb0d 100644 --- a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusTrackingImp.kt +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusTrackingImp.kt @@ -2,12 +2,11 @@ package exchange.dydx.dydxstatemanager.protocolImplementations import exchange.dydx.abacus.protocols.TrackingProtocol import exchange.dydx.trading.integration.analytics.CompositeTracking -import exchange.dydx.trading.integration.analytics.Tracking import javax.inject.Inject class AbacusTrackingImp @Inject constructor( private val compositeTracking: CompositeTracking, -) : TrackingProtocol, Tracking { +) : TrackingProtocol { override fun log(event: String, data: String?) { compositeTracking.log(event, data)