diff --git a/app/src/main/java/one/mixin/android/MixinApplication.kt b/app/src/main/java/one/mixin/android/MixinApplication.kt index bd8ddb13b7..a61b637f98 100644 --- a/app/src/main/java/one/mixin/android/MixinApplication.kt +++ b/app/src/main/java/one/mixin/android/MixinApplication.kt @@ -82,6 +82,7 @@ import one.mixin.android.util.BiometricUtil import one.mixin.android.ui.web.clips import one.mixin.android.ui.web.refresh import one.mixin.android.ui.web.releaseAll +import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.util.CursorWindowFixer import one.mixin.android.util.MemoryCallback import one.mixin.android.util.debug.FileLogTree @@ -214,6 +215,9 @@ open class MixinApplication : AppsFlyerLib.getInstance().init(BuildConfig.APPSFLYER_DEV_KEY, object : AppsFlyerConversionListener { override fun onConversionDataSuccess(conversionData: Map) { Timber.d("AppsFlyer Conversion Data: $conversionData") + if (Session.checkToken()) { + AnalyticsTracker.updateAppsFlyerConversionUserProperties(conversionData) + } } override fun onConversionDataFail(error: String) { @@ -228,7 +232,7 @@ open class MixinApplication : Timber.e("AppsFlyer Attribution Failure: $error") } }, this) - AppsFlyerLib.getInstance().start(this) + Session.getAccount()?.let { AnalyticsTracker.setAppsFlyerCustomerUserId(it) } val firebaseAnalytics = FirebaseAnalytics.getInstance(this) val appInstanceIdTask = firebaseAnalytics.appInstanceId val sessionIdTask = firebaseAnalytics.sessionId @@ -247,6 +251,13 @@ open class MixinApplication : } } + private fun startAppsFlyer(activity: Activity) { + if (BuildConfig.APPSFLYER_DEV_KEY.isBlank()) { + return + } + AppsFlyerLib.getInstance().start(activity) + } + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { super.onConfigurationChanged(newConfig) val activity = topActivity @@ -426,6 +437,7 @@ open class MixinApplication : appAuthShown = true } if (activityReferences == 1 && activity !is AppAuthActivity && !isActivityChangingConfigurations) { + startAppsFlyer(activity) checkAndShowAppAuth(activity) } } diff --git a/app/src/main/java/one/mixin/android/job/DecryptMessage.kt b/app/src/main/java/one/mixin/android/job/DecryptMessage.kt index 54ee20d734..58eb5b38ed 100644 --- a/app/src/main/java/one/mixin/android/job/DecryptMessage.kt +++ b/app/src/main/java/one/mixin/android/job/DecryptMessage.kt @@ -153,6 +153,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress import timber.log.Timber import java.io.File import java.io.IOException +import java.math.BigDecimal import java.util.UUID class DecryptMessage(private val lifecycleScope: CoroutineScope) : Injector() { @@ -1194,7 +1195,15 @@ class DecryptMessage(private val lifecycleScope: CoroutineScope) : Injector() { insertMessage(message, data) jobManager.addJobInBackground(RefreshTokensJob(snapshot.assetId, data.conversationId, data.messageId)) jobManager.addJobInBackground(SyncOutputJob()) - runBlocking { AnalyticsTracker.setAssetLevel(tokenDao.findTotalUSDBalance() ?: 0) } + runBlocking { + AnalyticsTracker.setAssetLevel(tokenDao.findTotalUSDBalance() ?: 0) + val receivedAmount = snapshot.amount.toBigDecimalOrNull() + if (receivedAmount != null && receivedAmount > BigDecimal.ZERO) { + val token = tokenDao.simpleAsset(snapshot.assetId) + val priceUsd = token?.priceUsd?.toBigDecimalOrNull() ?: BigDecimal.ZERO + AnalyticsTracker.trackAssetReceiveSuccess(token?.symbol, receivedAmount.multiply(priceUsd)) + } + } if (snapshot.amount.toFloat() > 0) { generateNotification(message, data) diff --git a/app/src/main/java/one/mixin/android/session/AccountSessionInitializer.kt b/app/src/main/java/one/mixin/android/session/AccountSessionInitializer.kt index e83b9add49..89c3091859 100644 --- a/app/src/main/java/one/mixin/android/session/AccountSessionInitializer.kt +++ b/app/src/main/java/one/mixin/android/session/AccountSessionInitializer.kt @@ -14,6 +14,7 @@ import one.mixin.android.extension.decodeBase64 import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.getStringDeviceId import one.mixin.android.extension.putString +import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.util.database.clearJobsAndRawTransaction import one.mixin.android.vo.Account @@ -31,6 +32,7 @@ suspend fun initializeAccountSession( Session.storeEd25519Seed(privateKey.base64Encode()) Session.storePinToken(pinToken.base64Encode()) Session.storeAccount(account) + AnalyticsTracker.setAppsFlyerCustomerUserId(account) // Enter the user scope and migrate databases BEFORE clearing anything // This ensures we operate on the correct scoped database after migration diff --git a/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt b/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt index f72666aa20..3e3267fefc 100644 --- a/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt @@ -360,6 +360,9 @@ class MnemonicPhraseFragment : BaseFragment(R.layout.fragment_compose) { if (r?.isSuccess == true) { val account = r.data!! initializeAccountSession(requireContext(), account, sessionKey) + if (words.isNullOrEmpty()) { + AnalyticsTracker.trackSignUpAccountCreated(account) + } when { account.fullName.isNullOrBlank() -> { withContext(Dispatchers.IO) { diff --git a/app/src/main/java/one/mixin/android/ui/wallet/HiddenAssetsFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/HiddenAssetsFragment.kt index 0641af88e2..1ccea45d31 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/HiddenAssetsFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/HiddenAssetsFragment.kt @@ -21,6 +21,8 @@ import one.mixin.android.ui.common.BaseFragment import one.mixin.android.ui.common.recyclerview.HeaderAdapter import one.mixin.android.ui.wallet.adapter.AssetItemCallback import one.mixin.android.ui.wallet.adapter.WalletAssetAdapter +import one.mixin.android.util.analytics.AnalyticsTracker +import one.mixin.android.util.analytics.AnalyticsTracker.TradeWallet import one.mixin.android.util.viewBinding import one.mixin.android.vo.safe.TokenItem import kotlin.math.abs @@ -66,6 +68,7 @@ class HiddenAssetsFragment : BaseFragment(R.layout.fragment_hidden_assets), Head val asset = assetsAdapter.data!![assetsAdapter.getPosition(hiddenPos)] val deleteItem = assetsAdapter.removeItem(hiddenPos)!! lifecycleScope.launch { + AnalyticsTracker.trackAssetVisibility(false, TradeWallet.MAIN, AnalyticsTracker.AssetSource.WALLET_HOME) walletViewModel.updateAssetHidden(asset.assetId, false) val anchorView = assetsRv @@ -74,6 +77,7 @@ class HiddenAssetsFragment : BaseFragment(R.layout.fragment_hidden_assets), Head .setAction(R.string.UNDO) { assetsAdapter.restoreItem(deleteItem, hiddenPos) lifecycleScope.launch(Dispatchers.IO) { + AnalyticsTracker.trackAssetVisibility(true, TradeWallet.MAIN, AnalyticsTracker.AssetSource.WALLET_HOME) walletViewModel.updateAssetHidden(asset.assetId, true) } }.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.wallet_blue)).apply { diff --git a/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt index 03cebb3a8c..e5c036e5d1 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt @@ -138,21 +138,22 @@ class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { close.setOnClickListener { dismiss() } share.setOnClickListener { if (isLoading) return@setOnClickListener - AnalyticsTracker.trackMarketDetailShare(AnalyticsTracker.MarketShareType.SHARE_IMAGE) + AnalyticsTracker.trackShareMarket(AnalyticsTracker.MarketShareType.SHARE_IMAGE) shareToSystem() } mixinContact.setOnClickListener { if (isLoading) return@setOnClickListener + AnalyticsTracker.trackShareMarket(AnalyticsTracker.MarketShareType.MIXIN_CONTACT) shareToMixinContact() } copy.setOnClickListener { if (isLoading) return@setOnClickListener - AnalyticsTracker.trackMarketDetailShare(AnalyticsTracker.MarketShareType.COPY_LINK) + AnalyticsTracker.trackShareMarket(AnalyticsTracker.MarketShareType.COPY_LINK) copyLink() } save.setOnClickListener { if (isLoading) return@setOnClickListener - AnalyticsTracker.trackMarketDetailShare(AnalyticsTracker.MarketShareType.SAVE_TO_ALBUM) + AnalyticsTracker.trackShareMarket(AnalyticsTracker.MarketShareType.SAVE_TO_ALBUM) saveToAlbum() } } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/TransactionsFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/TransactionsFragment.kt index 8c9ebbc36f..61cf5f7b4d 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/TransactionsFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/TransactionsFragment.kt @@ -349,9 +349,10 @@ class TransactionsFragment : BaseFragment(R.layout.fragment_transactions), OnSna bottomBinding.apply { hide.setText(if (asset.hidden == true) R.string.Show else R.string.Hide) hide.setOnClickListener { - AnalyticsTracker.trackAssetDetailHide() + val hidden = asset.hidden != true + AnalyticsTracker.trackAssetVisibility(hidden, TradeWallet.MAIN, AnalyticsTracker.AssetSource.ASSET_DETAIL) lifecycleScope.launch(Dispatchers.IO) { - walletViewModel.updateAssetHidden(asset.assetId, asset.hidden != true) + walletViewModel.updateAssetHidden(asset.assetId, hidden) } bottomSheet.dismiss() mainThreadDelayed({ activity?.onBackPressedDispatcher?.onBackPressed() }, 200) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/Web3HiddenAssetsFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/Web3HiddenAssetsFragment.kt index fee2ddbf88..28a5015240 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/Web3HiddenAssetsFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/Web3HiddenAssetsFragment.kt @@ -23,6 +23,8 @@ import one.mixin.android.ui.common.recyclerview.HeaderAdapter import one.mixin.android.ui.home.web3.Web3ViewModel import one.mixin.android.ui.wallet.adapter.AssetItemCallback import one.mixin.android.ui.wallet.adapter.WalletWeb3TokenAdapter +import one.mixin.android.util.analytics.AnalyticsTracker +import one.mixin.android.util.analytics.AnalyticsTracker.TradeWallet import one.mixin.android.util.viewBinding import one.mixin.android.web3.details.Web3TransactionsFragment import kotlin.math.abs @@ -81,6 +83,7 @@ class Web3HiddenAssetsFragment : BaseFragment(R.layout.fragment_hidden_assets), val asset = assetsAdapter.data!![assetsAdapter.getPosition(hiddenPos)] val deleteItem = assetsAdapter.removeItem(hiddenPos)!! lifecycleScope.launch { + AnalyticsTracker.trackAssetVisibility(false, TradeWallet.WEB3, AnalyticsTracker.AssetSource.WALLET_HOME) web3ViewModel.updateTokenHidden(asset.assetId, asset.walletId, false) val anchorView = assetsRv @@ -89,6 +92,7 @@ class Web3HiddenAssetsFragment : BaseFragment(R.layout.fragment_hidden_assets), .setAction(R.string.UNDO) { assetsAdapter.restoreItem(deleteItem, hiddenPos) lifecycleScope.launch { + AnalyticsTracker.trackAssetVisibility(true, TradeWallet.WEB3, AnalyticsTracker.AssetSource.WALLET_HOME) web3ViewModel.updateTokenHidden(asset.assetId, asset.walletId, true) } }.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.wallet_blue)).apply { diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsRules.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsRules.kt new file mode 100644 index 0000000000..0383b2f553 --- /dev/null +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsRules.kt @@ -0,0 +1,90 @@ +package one.mixin.android.util.analytics + +import java.math.BigDecimal + +internal data class AnalyticsEvent( + val name: String, + val params: Map = emptyMap(), +) + +internal object AnalyticsRules { + private const val NON_ORGANIC = "Non-Organic" + + private val directAppsFlyerEvents = setOf( + "sign_up_start", + "sign_up_account_created", + "login_start", + "buy_start", + "asset_receive_start", + "asset_receive_end", + "asset_receive_success", + "trade_spot_start", + "trade_spot_end", + "trade_perps_open_position_start", + "trade_perps_open_position_end", + "asset_send_start", + "asset_send_end", + "share_market", + "hide_asset", + "show_asset", + ) + + fun appsFlyerEventName(eventName: String): String? = + when (eventName) { + "login_end" -> "af_login" + "sign_up_end" -> "af_complete_registration" + in directAppsFlyerEvents -> eventName + else -> null + } + + fun conversionUserProperties(conversionData: Map): Map { + val status = conversionData["af_status"]?.toString()?.takeIf { it.isNotBlank() } ?: return emptyMap() + val properties = linkedMapOf("af_source" to status) + if (status == NON_ORGANIC) { + conversionData["media_source"]?.toString()?.takeIf { it.isNotBlank() }?.let { + properties["af_media_source"] = it + } + conversionData["campaign"]?.toString()?.takeIf { it.isNotBlank() }?.let { + properties["af_campaign"] = it + } + } + return properties + } + + fun marketShareEvent(type: String) = + AnalyticsEvent("share_market", mapOf("type" to type)) + + fun spotOrdersEvent(type: String) = + AnalyticsEvent("trade_spot_orders", mapOf("type" to type)) + + fun spotOrderDetailEvent(type: String) = + AnalyticsEvent("trade_spot_order_detail", mapOf("type" to type)) + + fun assetVisibilityEvent( + hidden: Boolean, + wallet: String, + source: String, + ) = + if (hidden) { + AnalyticsEvent( + "hide_asset", + mapOf( + "wallet" to wallet, + "source" to source, + ), + ) + } else { + AnalyticsEvent("show_asset", mapOf("wallet" to wallet)) + } + + fun receiveAssetLevel(amountUsd: BigDecimal): String = + when { + amountUsd >= BigDecimal("1000000") -> "v1,000,000" + amountUsd >= BigDecimal("100000") -> "v100,000" + amountUsd >= BigDecimal("10000") -> "v10,000" + amountUsd >= BigDecimal("1000") -> "v1,000" + amountUsd >= BigDecimal("100") -> "v100" + amountUsd > BigDecimal.ZERO -> "v1" + else -> "v0" + } +} diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt index c55d5ffdc2..808f2e09bf 100644 --- a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt @@ -3,7 +3,9 @@ package one.mixin.android.util.analytics import android.content.Context import android.os.Bundle import androidx.core.app.NotificationManagerCompat +import com.appsflyer.AppsFlyerLib import com.google.firebase.analytics.FirebaseAnalytics +import one.mixin.android.BuildConfig import one.mixin.android.MixinApplication import one.mixin.android.vo.Account import one.mixin.android.vo.Plan @@ -13,11 +15,41 @@ object AnalyticsTracker { private val firebaseAnalytics by lazy { FirebaseAnalytics.getInstance(MixinApplication.get()) } private fun logEvent(name: String) { - firebaseAnalytics.logEvent(name, null) + logEvent(name, null) } private inline fun logEvent(name: String, block: Bundle.() -> Unit) { - firebaseAnalytics.logEvent(name, Bundle().apply(block)) + logEvent(name, Bundle().apply(block)) + } + + private fun logEvent(event: AnalyticsEvent) { + logEvent( + event.name, + Bundle().apply { + event.params.forEach { (key, value) -> + putString(key, value) + } + }.takeIf { event.params.isNotEmpty() }, + ) + } + + private fun logEvent(name: String, params: Bundle?) { + firebaseAnalytics.logEvent(name, params) + if (BuildConfig.APPSFLYER_DEV_KEY.isBlank()) { + return + } + AnalyticsRules.appsFlyerEventName(name)?.let { appsFlyerEventName -> + AppsFlyerLib.getInstance().logEvent(MixinApplication.get(), appsFlyerEventName, params?.toAppsFlyerValues()) + } + } + + @Suppress("DEPRECATION") + private fun Bundle.toAppsFlyerValues(): Map? { + val values = HashMap() + keySet().forEach { key -> + get(key)?.let { values[key] = it } + } + return values.takeIf { it.isNotEmpty() } } fun trackSignUpStart(source: String) { @@ -26,6 +58,10 @@ object AnalyticsTracker { } } + fun trackSignUpAccountCreated(account: Account) { + setAppsFlyerCustomerUserId(account) + logEvent("sign_up_account_created") + } fun trackSignUpCaptcha() { logEvent("sign_up_captcha") @@ -128,6 +164,19 @@ object AnalyticsTracker { firebaseAnalytics.setUserProperty("asset_level", level) } + fun setAppsFlyerCustomerUserId(account: Account) { + if (BuildConfig.APPSFLYER_DEV_KEY.isBlank()) { + return + } + AppsFlyerLib.getInstance().setCustomerUserId(account.userId) + } + + fun updateAppsFlyerConversionUserProperties(conversionData: Map) { + AnalyticsRules.conversionUserProperties(conversionData).forEach { (key, value) -> + firebaseAnalytics.setUserProperty(key, value) + } + } + fun trackAssetDetail(wallet: String, source: String) { logEvent("asset_detail") { putString("wallet", wallet) @@ -135,8 +184,8 @@ object AnalyticsTracker { } } - fun trackAssetDetailHide() { - logEvent("asset_detail_hide") + fun trackAssetVisibility(hidden: Boolean, wallet: String, source: String) { + logEvent(AnalyticsRules.assetVisibilityEvent(hidden, wallet, source)) } fun trackAllTransactions(source: String) { @@ -183,6 +232,13 @@ object AnalyticsTracker { logEvent("asset_receive_end") } + fun trackAssetReceiveSuccess(assetSymbol: String?, amountUsd: BigDecimal) { + logEvent("asset_receive_success") { + putString("receive_asset_symbol", assetSymbol) + putString("receive_asset_level", AnalyticsRules.receiveAssetLevel(amountUsd)) + } + } + fun trackAssetSendStart(wallet: String, source: String) { logEvent("asset_send_start") { putString("wallet", wallet) @@ -475,6 +531,7 @@ object AnalyticsTracker { object MarketShareType { const val SHARE_IMAGE = "share_image" + const val MIXIN_CONTACT = "mixin_contact" const val COPY_LINK = "copy_link" const val SAVE_TO_ALBUM = "save_to_album" } @@ -750,15 +807,11 @@ object AnalyticsTracker { } fun trackSpotTransactions(type: String) { - logEvent("trade_spot_transactions") { - putString("type", type) - } + logEvent(AnalyticsRules.spotOrdersEvent(type)) } fun trackSpotDetail(type: String) { - logEvent("trade_spot_detail") { - putString("type", type) - } + logEvent(AnalyticsRules.spotOrderDetailEvent(type)) } fun trackSpotGuide(type: String, source: String) { @@ -847,10 +900,8 @@ object AnalyticsTracker { } } - fun trackMarketDetailShare(type: String) { - logEvent("market_detail_share") { - putString("type", type) - } + fun trackShareMarket(type: String) { + logEvent(AnalyticsRules.marketShareEvent(type)) } fun trackMarketFavoriteAdd(source: String) { diff --git a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt index 086bd6748c..f856626790 100644 --- a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt +++ b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt @@ -447,8 +447,10 @@ class Web3TransactionsFragment : BaseFragment(R.layout.fragment_web3_transaction hide.setText(if (token.hidden == true) R.string.Show else R.string.Hide) hide.setOnClickListener { + val hidden = token.hidden != true + AnalyticsTracker.trackAssetVisibility(hidden, TradeWallet.WEB3, AnalyticsTracker.AssetSource.ASSET_DETAIL) lifecycleScope.launch(Dispatchers.IO) { - web3ViewModel.updateTokenHidden(token.assetId, token.walletId, token.hidden != true) + web3ViewModel.updateTokenHidden(token.assetId, token.walletId, hidden) } bottomSheet.dismiss() mainThreadDelayed({ activity?.onBackPressedDispatcher?.onBackPressed() }, 200) diff --git a/app/src/test/java/one/mixin/android/util/analytics/AnalyticsRulesTest.kt b/app/src/test/java/one/mixin/android/util/analytics/AnalyticsRulesTest.kt new file mode 100644 index 0000000000..5d2435c800 --- /dev/null +++ b/app/src/test/java/one/mixin/android/util/analytics/AnalyticsRulesTest.kt @@ -0,0 +1,78 @@ +package one.mixin.android.util.analytics + +import kotlin.test.assertEquals +import org.junit.Test + +class AnalyticsRulesTest { + @Test + fun marketShareEventUsesShareMarketNameAndTypeParam() { + val event = AnalyticsRules.marketShareEvent(AnalyticsTracker.MarketShareType.MIXIN_CONTACT) + + assertEquals("share_market", event.name) + assertEquals(mapOf("type" to "mixin_contact"), event.params) + } + + @Test + fun hideAssetEventUsesWalletAndSourceParams() { + val event = AnalyticsRules.assetVisibilityEvent( + hidden = true, + wallet = AnalyticsTracker.TradeWallet.MAIN, + source = AnalyticsTracker.AssetSource.ASSET_DETAIL, + ) + + assertEquals("hide_asset", event.name) + assertEquals( + mapOf( + "wallet" to "main", + "source" to "asset_detail", + ), + event.params, + ) + } + + @Test + fun showAssetEventUsesOnlyWalletParam() { + val event = AnalyticsRules.assetVisibilityEvent( + hidden = false, + wallet = AnalyticsTracker.TradeWallet.WEB3, + source = AnalyticsTracker.AssetSource.WALLET_HOME, + ) + + assertEquals("show_asset", event.name) + assertEquals(mapOf("wallet" to "web3"), event.params) + } + + @Test + fun marketAndAssetVisibilityEventsSyncToAppsFlyer() { + assertEquals("share_market", AnalyticsRules.appsFlyerEventName("share_market")) + assertEquals("hide_asset", AnalyticsRules.appsFlyerEventName("hide_asset")) + assertEquals("show_asset", AnalyticsRules.appsFlyerEventName("show_asset")) + } + + @Test + fun spotOrdersEventUsesRenamedEventNameAndTypeParam() { + val event = AnalyticsRules.spotOrdersEvent(AnalyticsTracker.SpotTradeType.ADVANCED) + + assertEquals("trade_spot_orders", event.name) + assertEquals(mapOf("type" to "advanced"), event.params) + } + + @Test + fun spotOrderDetailEventUsesRenamedEventNameAndTypeParam() { + val event = AnalyticsRules.spotOrderDetailEvent(AnalyticsTracker.SpotTradeType.SIMPLE) + + assertEquals("trade_spot_order_detail", event.name) + assertEquals(mapOf("type" to "simple"), event.params) + } + + @Test + fun receiveAssetLevelUsesV0ForNoMoney() { + assertEquals("v0", AnalyticsRules.receiveAssetLevel("0".toBigDecimal())) + } + + @Test + fun receiveAssetLevelUsesV1ForPositiveAmountsUnderV100() { + assertEquals("v1", AnalyticsRules.receiveAssetLevel("0.01".toBigDecimal())) + assertEquals("v1", AnalyticsRules.receiveAssetLevel("99.99".toBigDecimal())) + } +}