From 44fc5b9610f2a25114ef09ed145368296adc4219 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Apr 2026 12:40:31 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../analytics/SwapAmountAnalyticsSender.kt | 67 +++++--- .../v2/impl/amount/model/SwapAmountModel.kt | 39 +++-- .../model/SwapNotificationsModel.kt | 43 +++--- .../analytics/SendWithSwapAnalyticEvents.kt | 55 +++++-- .../SendWithSwapAnalyticsErrorMessages.kt | 11 -- .../confirm/model/SendWithSwapConfirmModel.kt | 23 ++- .../SwapAmountAnalyticsSenderTest.kt | 144 ++++++++++++++---- 7 files changed, 260 insertions(+), 122 deletions(-) delete mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt index d6b20adde2..a349372423 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt @@ -1,38 +1,61 @@ package com.tangem.features.swap.v2.impl.amount.analytics import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents -import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages internal class SwapAmountAnalyticsSender( private val analyticsEventHandler: AnalyticsEventHandler, ) { - private var lastSentErrorMessage: String? = null + private var lastSentEvent: AnalyticsEvent? = null - fun sendErrorIfNeeded(quotes: List, selectedQuote: SwapQuoteUM?) { - val errorMessage = resolveErrorMessage(quotes, selectedQuote) - if (errorMessage == lastSentErrorMessage) return - lastSentErrorMessage = errorMessage - if (errorMessage != null) { - analyticsEventHandler.send( - SendWithSwapAnalyticEvents.SendWithSwapError( - errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount, - message = errorMessage, - ), + fun sendErrorIfNeeded( + quotes: List, + selectedQuote: SwapQuoteUM?, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + hasInsufficientBalance: Boolean, + ) { + val event = resolveEvent( + quotes = quotes, + selectedQuote = selectedQuote, + fromToken = fromToken, + toToken = toToken, + hasInsufficientBalance = hasInsufficientBalance, + ) + if (event?.event == lastSentEvent?.event && event?.params == lastSentEvent?.params) return + lastSentEvent = event + if (event != null) { + analyticsEventHandler.send(event) + } + } + + private fun resolveEvent( + quotes: List, + selectedQuote: SwapQuoteUM?, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + hasInsufficientBalance: Boolean, + ): AnalyticsEvent? { + if (hasInsufficientBalance) { + return SendWithSwapAnalyticEvents.ErrorInsufficientBalance(fromToken = fromToken) + } + if (quotes.isEmpty()) return null + val error = (selectedQuote as? SwapQuoteUM.Error)?.expressError ?: return null + return when (error) { + is ExpressError.AmountError.TooSmallError -> + SendWithSwapAnalyticEvents.ErrorMinAmount(fromToken = fromToken) + is ExpressError.AmountError.TooBigError -> + SendWithSwapAnalyticEvents.ErrorMaxAmount(fromToken = fromToken) + else -> SendWithSwapAnalyticEvents.ErrorExpressQuote( + fromToken = fromToken, + toToken = toToken, + errorDescription = "code=${error.code}", ) } } - - private fun resolveErrorMessage(quotes: List, selectedQuote: SwapQuoteUM?): String? { - if (quotes.isEmpty()) return SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE_NO_PROVIDERS - val error = (selectedQuote as? SwapQuoteUM.Error)?.expressError ?: return null - return when (error) { - is ExpressError.AmountError.TooSmallError -> SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT - is ExpressError.AmountError.TooBigError -> SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT - else -> "${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=${error.code}" - } - } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index b033d4f8cf..3aedd8f612 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -57,7 +57,6 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType -import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask @@ -616,18 +615,35 @@ internal class SwapAmountModel @Inject constructor( | Secondary -> $secondaryStatus """.trimIndent(), ) - analyticsEventHandler.send( - SendWithSwapAnalyticEvents.SendWithSwapError( - errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount, - message = "${SendWithSwapAnalyticsErrorMessages.INVALID_CRYPTOCURRENCIES_STATUS}: " + - "primary=$primaryStatus, secondary=$secondaryStatus", - ), - ) showErrorAlert(errorMessage = null) } } } + private fun sendAmountErrorAnalyticsIfNeeded(quotes: List) { + val content = uiState.value as? SwapAmountUM.Content ?: return + val toCurrency = content.secondaryCryptoCurrencyStatus?.currency ?: return + amountAnalyticsSender.sendErrorIfNeeded( + quotes = quotes, + selectedQuote = content.selectedQuote, + fromToken = content.primaryCryptoCurrencyStatus.currency, + toToken = toCurrency, + hasInsufficientBalance = hasInsufficientBalance(content), + ) + } + + private fun hasInsufficientBalance(content: SwapAmountUM.Content): Boolean { + val primaryBalance = content.primaryCryptoCurrencyStatus.value.amount ?: return false + val fromAmount = when (content.selectedAmountType) { + SwapAmountType.To -> (content.selectedQuote as? SwapQuoteUM.Content)?.fromAmount + SwapAmountType.From -> { + val field = content.primaryAmount as? SwapAmountFieldUM.Content + (field?.amountField as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + } + } ?: return false + return fromAmount > primaryBalance + } + private fun sendAmountScreenOpenedIfNeeded(secondaryStatus: CryptoCurrencyStatus) { if (params !is SwapAmountComponentParams.AmountParams) return if (isAmountScreenOpenedSent) return @@ -711,7 +727,9 @@ internal class SwapAmountModel @Inject constructor( val isAmountScreen = params is SwapAmountComponentParams.AmountParams val isAmountError = amountField?.amountTextField?.isError == true || amountValue.isNullOrZero() if (isAmountScreen && isAmountError) { - uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return + uiState.transformerUpdate(SwapQuoteEmptyStateTransformer) + sendAmountErrorAnalyticsIfNeeded(quotes = emptyList()) + return } val rateType = when (state.selectedAmountType) { @@ -783,8 +801,7 @@ internal class SwapAmountModel @Inject constructor( ), ) if (params is SwapAmountComponentParams.AmountParams) { - val selectedQuote = (uiState.value as? SwapAmountUM.Content)?.selectedQuote - amountAnalyticsSender.sendErrorIfNeeded(quotes, selectedQuote) + sendAmountErrorAnalyticsIfNeeded(quotes) } feeSelectorReloadTrigger.triggerUpdate() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index ca86d23dee..d82b8ace85 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -18,7 +18,6 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents -import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -46,7 +45,7 @@ internal class SwapNotificationsModel @Inject constructor( private val params: SwapNotificationsComponent.Params = paramsContainer.require() private var notificationData = params.swapNotificationData - private var lastSentErrorMessages: Set = emptySet() + private var lastSentErrorKeys: Set>> = emptySet() val uiState: StateFlow> field = MutableStateFlow>(persistentListOf()) @@ -203,32 +202,32 @@ internal class SwapNotificationsModel @Inject constructor( } private fun sendErrorAnalyticsIfNeeded(notifications: List) { - val currentErrors = notifications.mapNotNull { notification -> + val fromToken = notificationData.fromCryptoCurrency ?: return + val toToken = notificationData.toCryptoCurrencyStatus?.currency + + val events = notifications.mapNotNull { notification -> when (notification) { is SwapNotificationUM.Error.InsufficientFunds -> - SendWithSwapAnalyticsErrorMessages.INSUFFICIENT_BALANCE + SendWithSwapAnalyticEvents.ErrorInsufficientBalance(fromToken = fromToken) is SwapNotificationUM.Error.MinimalAmountError -> - SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT + SendWithSwapAnalyticEvents.ErrorMinAmount(fromToken = fromToken) is SwapNotificationUM.Error.MaximumAmountError -> - SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT - is SwapNotificationUM.Warning.ExpressGeneralError -> - "${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=${notification.expressError.code}" - is NotificationUM.Error.DestinationTagRequired -> - SendWithSwapAnalyticsErrorMessages.DESTINATION_TAG_REQUIRED + SendWithSwapAnalyticEvents.ErrorMaxAmount(fromToken = fromToken) + is SwapNotificationUM.Warning.ExpressGeneralError -> toToken?.let { receiveToken -> + SendWithSwapAnalyticEvents.ErrorExpressQuote( + fromToken = fromToken, + toToken = receiveToken, + errorDescription = "code=${notification.expressError.code}", + ) + } else -> null } - }.toSet() - - val newErrors = currentErrors - lastSentErrorMessages - lastSentErrorMessages = currentErrors - - newErrors.forEach { errorMessage -> - analyticsEventHandler.send( - SendWithSwapAnalyticEvents.SendWithSwapError( - errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, - message = errorMessage, - ), - ) } + + val currentKeys = events.map { it.event to it.params }.toSet() + val newEvents = events.filter { it.event to it.params !in lastSentErrorKeys } + lastSentErrorKeys = currentKeys + + newEvents.forEach(analyticsEventHandler::send) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index f753ef6f26..5a68284603 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -4,7 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO -import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE @@ -120,19 +120,51 @@ internal sealed class SendWithSwapAnalyticEvents( params = emptyMap(), ) - data class SendWithSwapError( - val errorScreen: ErrorScreen, - val message: String, + data class ErrorInsufficientBalance( + val fromToken: CryptoCurrency, ) : SendWithSwapAnalyticEvents( - event = when (errorScreen) { - ErrorScreen.Amount -> "Send With Swap Amount Screen Error" - ErrorScreen.Confirm -> "Send With Swap Confirm Screen Error" - }, + event = "Error - Insufficient balance", params = mapOf( - ERROR_MESSAGE to message, + SEND_TOKEN to fromToken.symbol, + SEND_BLOCKCHAIN to fromToken.network.name, ), ) + data class ErrorMinAmount( + val fromToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Error - Min amount", + params = mapOf( + SEND_TOKEN to fromToken.symbol, + SEND_BLOCKCHAIN to fromToken.network.name, + ), + ) + + data class ErrorMaxAmount( + val fromToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Error - Max amount", + params = mapOf( + SEND_TOKEN to fromToken.symbol, + SEND_BLOCKCHAIN to fromToken.network.name, + ), + ) + + data class ErrorExpressQuote( + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + val errorDescription: String? = null, + ) : SendWithSwapAnalyticEvents( + event = "Error - Express quote", + params = buildMap { + put(SEND_TOKEN, fromToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_TOKEN, toToken.symbol) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + if (errorDescription != null) put(ERROR_DESCRIPTION, errorDescription) + }, + ) + class HighPriceImpact( val sendToken: String, val receiveToken: String, @@ -168,11 +200,6 @@ internal sealed class SendWithSwapAnalyticEvents( ), ) - enum class ErrorScreen { - Amount, - Confirm, - } - enum class RateType { Float, Fixed, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt deleted file mode 100644 index 610a9c1159..0000000000 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.swap.v2.impl.sendviaswap.analytics - -internal object SendWithSwapAnalyticsErrorMessages { - const val INSUFFICIENT_BALANCE = "Error - Insufficient balance" - const val MIN_AMOUNT = "Error - Min amount" - const val MAX_AMOUNT = "Error - Max amount" - const val EXPRESS_QUOTE_NO_PROVIDERS = "Error - Express quote no providers found" - const val EXPRESS_QUOTE = "Error - Express quote" - const val DESTINATION_TAG_REQUIRED = "Error - Destination tag required" - const val INVALID_CRYPTOCURRENCIES_STATUS = "Error - Invalid cryptocurrencies status" -} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 809ef27a7f..082d7d8ffa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -322,12 +322,17 @@ internal class SendWithSwapConfirmModel @Inject constructor( isAmountSubtractAvailable = isAmountSubtractAvailable, onExpressError = { expressError -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) - analyticsEventHandler.send( - SendWithSwapAnalyticEvents.SendWithSwapError( - errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, - message = "Express error: $expressError", - ), - ) + val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency + val toCurrency = confirmData.toCryptoCurrencyStatus?.currency + if (fromCurrency != null && toCurrency != null) { + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.ErrorExpressQuote( + fromToken = fromCurrency, + toToken = toCurrency, + errorDescription = "code=${expressError.code}", + ), + ) + } swapAlertFactory.getGenericErrorState( expressError = expressError, onFailedTxEmailClick = { @@ -345,12 +350,6 @@ internal class SendWithSwapConfirmModel @Inject constructor( }, onSendError = { error -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) - analyticsEventHandler.send( - SendWithSwapAnalyticEvents.SendWithSwapError( - errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, - message = "Send error: ${error?.toString().orEmpty()}", - ), - ) swapAlertFactory.getSendTransactionErrorState( error = error, onFailedTxEmailClick = { _ -> diff --git a/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt index 26f07c6275..3f81cf380b 100644 --- a/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt +++ b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt @@ -6,9 +6,10 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents -import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -21,6 +22,21 @@ class SwapAmountAnalyticsSenderTest { private val analyticsEventHandler = mockk(relaxed = true) private val sender = SwapAmountAnalyticsSender(analyticsEventHandler) + private val fromNetwork = mockk(relaxed = true).also { + every { it.name } returns "Ethereum" + } + private val toNetwork = mockk(relaxed = true).also { + every { it.name } returns "Bitcoin" + } + private val fromToken = mockk(relaxed = true).also { + every { it.symbol } returns "ETH" + every { it.network } returns fromNetwork + } + private val toToken = mockk(relaxed = true).also { + every { it.symbol } returns "BTC" + every { it.network } returns toNetwork + } + private val testProvider = ExpressProvider( providerId = "test", name = "Test Provider", @@ -31,21 +47,41 @@ class SwapAmountAnalyticsSenderTest { slippage = null, ) + private fun send( + quotes: List = emptyList(), + selectedQuote: SwapQuoteUM? = null, + hasInsufficientBalance: Boolean = false, + ) = sender.sendErrorIfNeeded( + quotes = quotes, + selectedQuote = selectedQuote, + fromToken = fromToken, + toToken = toToken, + hasInsufficientBalance = hasInsufficientBalance, + ) + @Test - fun `GIVEN empty quotes WHEN sendErrorIfNeeded THEN send no providers error`() { - val eventSlot = slot() - every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit + fun `GIVEN empty quotes WHEN sendErrorIfNeeded THEN do not send analytics`() { + send(quotes = emptyList(), selectedQuote = null) - sender.sendErrorIfNeeded(quotes = emptyList(), selectedQuote = null) - - verify(exactly = 1) { analyticsEventHandler.send(any()) } - val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError - assertThat(event.errorScreen).isEqualTo(SendWithSwapAnalyticEvents.ErrorScreen.Amount) - assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE_NO_PROVIDERS) + verify(exactly = 0) { analyticsEventHandler.send(any()) } } @Test - fun `GIVEN too small error quote WHEN sendErrorIfNeeded THEN send min amount error`() { + fun `GIVEN insufficient balance WHEN sendErrorIfNeeded THEN send ErrorInsufficientBalance with from token params`() { + val eventSlot = slot() + every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit + + send(hasInsufficientBalance = true) + + verify(exactly = 1) { analyticsEventHandler.send(any()) } + val event = eventSlot.captured + assertThat(event).isInstanceOf(SendWithSwapAnalyticEvents.ErrorInsufficientBalance::class.java) + assertThat(event.event).isEqualTo("Error - Insufficient balance") + assertThat(event.params).containsExactly("Send Token", "ETH", "Send Blockchain", "Ethereum") + } + + @Test + fun `GIVEN insufficient balance and express error WHEN sendErrorIfNeeded THEN insufficient balance takes priority`() { val errorQuote = SwapQuoteUM.Error( provider = testProvider, expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")), @@ -53,15 +89,32 @@ class SwapAmountAnalyticsSenderTest { val eventSlot = slot() every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote, hasInsufficientBalance = true) verify(exactly = 1) { analyticsEventHandler.send(any()) } - val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError - assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT) + assertThat(eventSlot.captured).isInstanceOf(SendWithSwapAnalyticEvents.ErrorInsufficientBalance::class.java) } @Test - fun `GIVEN too big error quote WHEN sendErrorIfNeeded THEN send max amount error`() { + fun `GIVEN too small error quote WHEN sendErrorIfNeeded THEN send ErrorMinAmount with from token params`() { + val errorQuote = SwapQuoteUM.Error( + provider = testProvider, + expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")), + ) + val eventSlot = slot() + every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit + + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) + + verify(exactly = 1) { analyticsEventHandler.send(any()) } + val event = eventSlot.captured + assertThat(event).isInstanceOf(SendWithSwapAnalyticEvents.ErrorMinAmount::class.java) + assertThat(event.event).isEqualTo("Error - Min amount") + assertThat(event.params).containsExactly("Send Token", "ETH", "Send Blockchain", "Ethereum") + } + + @Test + fun `GIVEN too big error quote WHEN sendErrorIfNeeded THEN send ErrorMaxAmount with from token params`() { val errorQuote = SwapQuoteUM.Error( provider = testProvider, expressError = ExpressError.AmountError.TooBigError(code = 1002, amount = BigDecimal("1000")), @@ -69,15 +122,17 @@ class SwapAmountAnalyticsSenderTest { val eventSlot = slot() every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) verify(exactly = 1) { analyticsEventHandler.send(any()) } - val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError - assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT) + val event = eventSlot.captured + assertThat(event).isInstanceOf(SendWithSwapAnalyticEvents.ErrorMaxAmount::class.java) + assertThat(event.event).isEqualTo("Error - Max amount") + assertThat(event.params).containsExactly("Send Token", "ETH", "Send Blockchain", "Ethereum") } @Test - fun `GIVEN unknown express error WHEN sendErrorIfNeeded THEN send express quote error with code`() { + fun `GIVEN unknown express error WHEN sendErrorIfNeeded THEN send ErrorExpressQuote with both tokens and code`() { val errorQuote = SwapQuoteUM.Error( provider = testProvider, expressError = ExpressError.InternalError(code = 500), @@ -85,18 +140,26 @@ class SwapAmountAnalyticsSenderTest { val eventSlot = slot() every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) verify(exactly = 1) { analyticsEventHandler.send(any()) } - val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError - assertThat(event.message).isEqualTo("${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=500") + val event = eventSlot.captured + assertThat(event).isInstanceOf(SendWithSwapAnalyticEvents.ErrorExpressQuote::class.java) + assertThat(event.event).isEqualTo("Error - Express quote") + assertThat(event.params).containsExactly( + "Send Token", "ETH", + "Send Blockchain", "Ethereum", + "Receive Token", "BTC", + "Receive Blockchain", "Bitcoin", + "Error Description", "code=500", + ) } @Test fun `GIVEN content quote WHEN sendErrorIfNeeded THEN do not send analytics`() { val contentQuote = mockk() - sender.sendErrorIfNeeded(quotes = listOf(contentQuote), selectedQuote = contentQuote) + send(quotes = listOf(contentQuote), selectedQuote = contentQuote) verify(exactly = 0) { analyticsEventHandler.send(any()) } } @@ -108,8 +171,16 @@ class SwapAmountAnalyticsSenderTest { expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")), ) - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) + + verify(exactly = 1) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN insufficient balance twice WHEN sendErrorIfNeeded THEN send analytics only once`() { + send(hasInsufficientBalance = true) + send(hasInsufficientBalance = true) verify(exactly = 1) { analyticsEventHandler.send(any()) } } @@ -125,8 +196,21 @@ class SwapAmountAnalyticsSenderTest { expressError = ExpressError.AmountError.TooBigError(code = 1002, amount = BigDecimal("1000")), ) - sender.sendErrorIfNeeded(quotes = listOf(smallError), selectedQuote = smallError) - sender.sendErrorIfNeeded(quotes = listOf(bigError), selectedQuote = bigError) + send(quotes = listOf(smallError), selectedQuote = smallError) + send(quotes = listOf(bigError), selectedQuote = bigError) + + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN express error then insufficient balance WHEN sendErrorIfNeeded THEN send analytics twice`() { + val smallError = SwapQuoteUM.Error( + provider = testProvider, + expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")), + ) + + send(quotes = listOf(smallError), selectedQuote = smallError) + send(quotes = listOf(smallError), selectedQuote = smallError, hasInsufficientBalance = true) verify(exactly = 2) { analyticsEventHandler.send(any()) } } @@ -139,9 +223,9 @@ class SwapAmountAnalyticsSenderTest { ) val contentQuote = mockk() - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) - sender.sendErrorIfNeeded(quotes = listOf(contentQuote), selectedQuote = contentQuote) - sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) + send(quotes = listOf(contentQuote), selectedQuote = contentQuote) + send(quotes = listOf(errorQuote), selectedQuote = errorQuote) verify(exactly = 2) { analyticsEventHandler.send(any()) } }