Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-06 12:18:13 +03:00
commit 531eb86ac5
1243 changed files with 49302 additions and 12422 deletions

View file

@ -3,6 +3,8 @@ package com.tangem.core.analytics.models
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL_ID
const val IS_NOT_HTTP_ERROR = "Is not http error"
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
@ -80,7 +82,9 @@ sealed class AnalyticsParam {
data object Backup : ScreensSources("Backup")
data object Onboarding : ScreensSources("Onboarding")
data object LongTap : ScreensSources("Long Tap")
data object Market : ScreensSources("Market")
data object Markets : ScreensSources("Markets")
data object MarketPulse : ScreensSources("Market Pulse")
data object TangemPay : ScreensSources("Tangem Pay")
data object WalletSettings : ScreensSources("Wallet Settings")
data object Upgrade : ScreensSources("Upgrade")
@ -90,6 +94,9 @@ sealed class AnalyticsParam {
data object AddNewWallet : ScreensSources("Add New Wallet")
data object AddNew : ScreensSources("Add New")
data object CreateWallet : ScreensSources("Create Wallet")
data object NewsList : ScreensSources("News List")
data object NewsLink : ScreensSources("News Link")
data object NewsPage : ScreensSources("News Page")
}
sealed class TxSentFrom(val value: String) {
@ -97,24 +104,28 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Swap"), TxData
data class Staking(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Staking"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
@ -122,6 +133,7 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType?,
override val feeToken: String,
) : TxSentFrom("WalletConnect"), TxData
data object Sell : TxSentFrom("Sell")
@ -130,24 +142,28 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("NFT"), TxData
data class SendWithSwap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Send&Swap"), TxData
data class Earning(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Earning"), TxData
}
sealed interface TxData {
val blockchain: String
val token: String
val feeToken: String
val feeType: FeeType?
}
@ -271,6 +287,9 @@ sealed class AnalyticsParam {
const val ENS = "ENS"
const val ENS_ADDRESS = "ENS Address"
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
const val ACCOUNT_DERIVATION_TO = "Account Derivation (to)"
const val FEE_TOKEN = "Fee Token"
const val ACCOUNT_DERIVATION = "Account Derivation"
const val REFERRAL = "Referral"
const val REFERRAL_ID = "Referral_ID"
}

View file

@ -87,6 +87,7 @@ sealed class Basic(
sentFrom.feeType?.value?.let {
this[AnalyticsParam.Key.FEE_TYPE] = it
}
this[AnalyticsParam.Key.FEE_TOKEN] = sentFrom.feeToken
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType

View file

@ -0,0 +1,32 @@
package com.tangem.core.analytics.models
/**
* Marker interface for analytics events that can be throttled by session and/or time.
*
* Events implementing this interface will be tracked by their [oneTimeEventId].
*
* Behavior depends on [throttleSeconds]:
* - **null** (default): One time per session event is sent only once during the application session.
* - **non-null**: Time-based throttling event is not sent if less than [throttleSeconds] seconds
* have passed since the last send for this [oneTimeEventId].
*
* @see Analytics.send
*/
interface OneTimePerSessionEvent {
/**
* Unique identifier for the throttled event.
*
* This ID is used to track whether and when the event was last sent.
* Events with the same [oneTimeEventId] share the same throttling state.
*/
val oneTimeEventId: String
/**
* Minimum interval in seconds between sends for this event.
*
* - **null**: One time per session only. Event is sent at most once per session.
* - **non-null**: Don't send if less than this many seconds have passed since the last send.
*/
val throttleSeconds: Long?
get() = null
}

View file

@ -6,12 +6,14 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.OneTimePerSessionEvent
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/**
[REDACTED_AUTHOR]
@ -31,6 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler {
private val handlers = mutableMapOf<String, AnalyticsHandler>()
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
private val throttledEventsState = ConcurrentHashMap<String, Long>()
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
private val analyticsMutex = Mutex()
@ -85,6 +88,9 @@ object Analytics : GlobalAnalyticsEventHandler {
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) {
return@launch
}
event.params = applyParamsInterceptors(event)
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
@ -130,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler {
return interceptedParams
}
private fun shouldSendThrottledEvent(event: OneTimePerSessionEvent): Boolean {
val now = System.currentTimeMillis()
val id = event.oneTimeEventId
return when (val throttleMs = event.throttleSeconds?.let(TimeUnit.SECONDS::toMillis)) {
null -> throttledEventsState.putIfAbsent(id, now) == null
else -> throttledEventsState.compute(id) { _, lastSendTime ->
when {
lastSendTime == null -> now
now - lastSendTime >= throttleMs -> now
else -> lastSendTime
}
} == now
}
}
private fun createScope(): CoroutineScope {
val name = "Analytics"
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()

View file

@ -0,0 +1,19 @@
package com.tangem.core.analytics.api
import com.tangem.core.analytics.models.AnalyticsEvent
import java.util.concurrent.ConcurrentHashMap
class ResettableOneTimeEventSender(private val analyticsEventHandler: AnalyticsEventHandler) {
private val sentEvents = ConcurrentHashMap<String, AnalyticsEvent>()
fun sendEventOnce(key: String, event: AnalyticsEvent) {
if (sentEvents.putIfAbsent(key, event) == null) {
analyticsEventHandler.send(event)
}
}
fun reset(key: String) {
sentEvents.remove(key)
}
}