Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOB-410 Add tracking for onboarding, transfer, wallet connection #46

Merged
merged 3 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion v4/app/src/main/java/exchange/dydx/trading/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
30 changes: 21 additions & 9 deletions v4/core/src/main/java/exchange/dydx/trading/core/DydxRouterImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,7 @@ class DydxRouterImpl @Inject constructor(

val destinationRoute = destination.route
if (destinationRoute != null) {

val trackingData: MutableMap<String, String> = 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()
Expand Down Expand Up @@ -227,4 +219,24 @@ class DydxRouterImpl @Inject constructor(
}
return route
}

private fun trackRoute(destinationRoute: String, destination: NavDestination, arguments: Bundle?) {
val trackingData: MutableMap<String, String> = 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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +36,8 @@ class DydxOnboardConnectViewModel @Inject constructor(
val abacusStateManager: AbacusStateManagerProtocol,
val platformInfo: PlatformInfo,
private val mutableSetupStatusFlow: MutableStateFlow<DydxWalletSetup.Status.Signed?>,
private val onboardingAnalytics: OnboardingAnalytics,
private val walletAnalytics: WalletAnalytics,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DydxViewModel {

Expand Down Expand Up @@ -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)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<DydxDesktopScanView.ViewState?> = abacusStateManager.state.marketSummary
.map {
createViewState(it)
}
.distinctUntilChanged()
val state: Flow<DydxDesktopScanView.ViewState?> = 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()
},
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +20,7 @@ class DydxTosViewModel @Inject constructor(
private val abacusStateManager: AbacusStateManagerProtocol,
private val router: DydxRouter,
private val setupStatusFlow: StateFlow<DydxWalletSetup.Status.Signed?>,
private val onboardingAnalytics: OnboardingAnalytics,
) : ViewModel(), DydxViewModel {

val state: Flow<DydxTosView.ViewState?> =
Expand Down Expand Up @@ -52,6 +54,8 @@ class DydxTosViewModel @Inject constructor(
mnemonic = mnemonic,
)
}

onboardingAnalytics.log(OnboardingAnalytics.OnboardingSteps.ACKNOWLEDGE_TERMS)
}

router.navigateBack()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DydxOnboardWelcomeView.ViewState?> = flowOf(createViewState())
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DydxProfileButtonsView.ViewState?> =
Expand Down Expand Up @@ -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()
},
)
Expand Down
3 changes: 3 additions & 0 deletions v4/feature/shared/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading