Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 17:59:40 +05:00
commit a22e77ebe8
669 changed files with 27695 additions and 7905 deletions

View file

@ -1,4 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
api(deps.kotlin.serialization)
}

View file

@ -2,6 +2,7 @@ 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
import kotlinx.serialization.Serializable
const val IS_NOT_HTTP_ERROR = "Is not http error"
@ -67,39 +68,41 @@ sealed class AnalyticsParam {
data object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScreensSources(val value: String) {
data object Settings : ScreensSources("Settings")
data object Main : ScreensSources("Main")
data object SignIn : ScreensSources("Sign In")
data object Send : ScreensSources("Send")
data object Intro : ScreensSources("Introduction")
data object MyWallets : ScreensSources("My Wallets")
data object Token : ScreensSources("Token")
data object Stories : ScreensSources("Stories")
data object Buy : ScreensSources("Buy")
data object Swap : ScreensSources("Swap")
data object Sell : ScreensSources("Sell")
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")
data object HardwareWallet : ScreensSources("Hardware Wallet")
data object ImportWallet : ScreensSources("Import Wallet")
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
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")
data object Portfolio : ScreensSources("Portfolio")
data object Staking : ScreensSources("Staking")
data object Earn : ScreensSources("Earn")
@Serializable
enum class ScreensSources(val value: String) {
Settings("Settings"),
Main("Main"),
SignIn("Sign In"),
Send("Send"),
Intro("Introduction"),
MyWallets("My Wallets"),
Token("Token"),
Stories("Stories"),
Buy("Buy"),
Swap("Swap"),
Sell("Sell"),
Backup("Backup"),
Onboarding("Onboarding"),
LongTap("Long Tap"),
Market("Market"),
Markets("Markets"),
MarketPulse("Market Pulse"),
TangemPay("Tangem Pay"),
WalletSettings("Wallet Settings"),
Upgrade("Upgrade"),
HardwareWallet("Hardware Wallet"),
ImportWallet("Import Wallet"),
CreateWalletIntro("Create Wallet Intro"),
AddNewWallet("Add New Wallet"),
AddNew("Add New"),
CreateWallet("Create Wallet"),
NewsList("News List"),
NewsLink("News Link"),
NewsPage("News Page"),
Portfolio("Portfolio"),
Staking("Staking"),
Earn("Earn"),
TangemPayHotWalletOnboarding("TangemPayHotWalletOnboarding"),
}
sealed class TxSentFrom(val value: String) {
@ -205,9 +208,9 @@ sealed class AnalyticsParam {
}
sealed class WalletCreationType(val value: String) {
data object PrivateKey : WalletCreationType("Private key")
data object NewSeed : WalletCreationType("New seed")
data object SeedImport : WalletCreationType("Seed import")
data object PrivateKey : WalletCreationType(value = "Private Key")
data object NewSeed : WalletCreationType(value = "New Seed")
data object SeedImport : WalletCreationType(value = "Seed Import")
}
sealed class WalletType(val value: String) {
@ -253,6 +256,13 @@ sealed class AnalyticsParam {
MobileWallet("Mobile Wallet"),
}
enum class SignInType(val value: String) {
Card("Card"),
Biometric("Biometric"),
NoSecurity("No Security"),
AccessCode("Access Code"),
}
companion object Key {
const val BLOCKCHAIN = "Blockchain"
const val TOKEN_PARAM = "Token"
@ -313,6 +323,11 @@ sealed class AnalyticsParam {
const val RATE_TYPE = "Rate Type"
const val SCREEN_TYPE = "Screen Type"
const val FEE_ASSET_TYPE = "Fee Asset Type"
const val SIGN_IN_TYPE = "Sign in type"
const val WALLETS_COUNT = "Wallets Count"
const val WALLET_TYPE = "Wallet Type"
const val BACKUPED = "Backuped"
const val MEMO = "Memo"
}
}

View file

@ -2,65 +2,41 @@ package com.tangem.core.analytics.models
sealed class Basic(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Basic", event, params) {
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Basic", event = event, params = params) {
/**
* Tracks card scanning from specific entry points (Introduction, Main, My Wallets, Sign In).
* The originating screen is reported via the [AnalyticsParam.SOURCE] parameter.
*/
class CardWasScanned(
source: AnalyticsParam.ScreensSources,
) : Basic(
event = "Card Was Scanned",
params = mapOf(
AnalyticsParam.Key.SOURCE to source.value,
AnalyticsParam.SOURCE to source.value,
),
)
class SignedInLegacy(
currency: AnalyticsParam.WalletType,
batch: String,
signInType: SignInType,
walletsCount: String,
isImported: Boolean,
hasBackup: Boolean?,
) : Basic(
event = "Signed in",
params = buildMap {
put(AnalyticsParam.Key.CURRENCY, currency.value)
put(AnalyticsParam.Key.BATCH, batch)
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
put("Sign in type", signInType.name)
put("Wallets Count", walletsCount)
if (hasBackup != null) {
put("Backuped", if (hasBackup) "Yes" else "No")
}
},
) {
enum class SignInType {
Card, Biometric
}
}
), CriticalEvent
/**
* Tracks any sign-in into a wallet (card scan, FaceID, or wallet switch).
* Counted as a single sign-in per session subsequent card scans within the same session are ignored.
*/
class SignedIn(
signInType: SignInType,
signInType: AnalyticsParam.SignInType,
walletsCount: Int,
isImported: Boolean,
hasBackup: Boolean?,
isBackedUp: Boolean,
) : Basic(
event = "Signed in",
params = buildMap {
put("Sign in type", signInType.value)
put("Wallets Count", walletsCount.toString())
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
if (hasBackup != null) {
put("Backuped", if (hasBackup) "Yes" else "No")
}
put(AnalyticsParam.SIGN_IN_TYPE, signInType.value)
put(AnalyticsParam.WALLETS_COUNT, walletsCount.toString())
put(AnalyticsParam.WALLET_TYPE, if (isImported) "Seed Phrase" else "Seedless")
put(AnalyticsParam.BACKUPED, if (isBackedUp) "Yes" else "No")
},
) {
enum class SignInType(val value: String) {
Card("Card"),
Biometric("Biometric"),
NoSecurity("No Security"),
AccessCode("Access Code"),
}
), CriticalEvent, OneTimePerSessionEvent {
override val oneTimeEventId: String = id
}
class ButtonBuy(
@ -68,40 +44,47 @@ sealed class Basic(
) : Basic(
event = "Button - Buy",
params = buildMap {
put(AnalyticsParam.Key.SOURCE, source.value)
put(AnalyticsParam.SOURCE, source.value)
},
)
class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) :
/**
* Tracks the first time a user wallet is topped up. Sent once per wallet, when the balance
* transitions from zero to positive (Total Balance for multi-currency wallets, or Balance for Note).
* A wallet scanned with a non-zero balance does not count as a top-up the event must be sent
* only after all tokens have finished loading.
*/
class ToppedUp(userWalletId: String, walletType: AnalyticsParam.WalletType) :
Basic(
event = "Topped up",
params = mapOf(AnalyticsParam.Key.CURRENCY to currency.value),
params = mapOf(AnalyticsParam.CURRENCY to walletType.value),
),
OneTimeAnalyticsEvent {
OneTimeAnalyticsEvent, AppsFlyerIncludedEvent, CriticalEvent {
override val oneTimeEventId: String = id + userWalletId
}
/**
* Tracks transaction submission from various screens (Send, Swap, WalletConnect, Sell, Approve, Staking).
*/
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) :
Basic(
event = "Transaction sent",
params = buildMap {
this[AnalyticsParam.Key.SOURCE] = sentFrom.value
put(AnalyticsParam.SOURCE, sentFrom.value)
if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.Key.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.Key.TOKEN_PARAM] = sentFrom.token
sentFrom.feeType?.value?.let {
this[AnalyticsParam.Key.FEE_TYPE] = it
}
this[AnalyticsParam.Key.FEE_TOKEN] = sentFrom.feeToken
this[AnalyticsParam.Key.FEE_ASSET_TYPE] = sentFrom.feeAssetType.value
put(AnalyticsParam.BLOCKCHAIN, sentFrom.blockchain)
put(AnalyticsParam.TOKEN_PARAM, sentFrom.token)
sentFrom.feeType?.value?.let { put(AnalyticsParam.FEE_TYPE, it) }
put(AnalyticsParam.FEE_TOKEN, sentFrom.feeToken)
put(AnalyticsParam.FEE_ASSET_TYPE, sentFrom.feeAssetType.value)
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType
put(AnalyticsParam.PERMISSION_TYPE, sentFrom.permissionType)
}
this["Memo"] = memoType.name
put(AnalyticsParam.MEMO, memoType.name)
},
), AppsFlyerIncludedEvent {
), AppsFlyerIncludedEvent, CriticalEvent {
enum class MemoType {
Empty, Full, Null
}
@ -111,31 +94,33 @@ sealed class Basic(
}
}
/**
* Tracks the user invoking the "Request Support" email flow from various screens of the app.
*/
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
event = "Request Support",
params = mapOf(
AnalyticsParam.Key.SOURCE to source.value,
AnalyticsParam.SOURCE to source.value,
),
), CriticalEvent
/**
* Tracks loading of the user's total balance after sign-in. Reports whether the balance is
* empty, has funds, failed to load, or could not be returned because of a custom token.
*/
class BalanceLoaded(balance: AnalyticsParam.CardBalanceState, tokensCount: Int?) : Basic(
event = "Balance Loaded",
params = buildMap {
put(AnalyticsParam.BALANCE, balance.value)
tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) }
},
), AppsFlyerIncludedEvent, CriticalEvent
class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic(
event = "Token Balance",
params = mapOf(
AnalyticsParam.STATE to balance.value,
AnalyticsParam.TOKEN_PARAM to token,
),
)
class BiometryFailed(
source: AnalyticsParam.ScreensSources,
reason: BiometricFailReason,
) : Basic(
event = "Biometry Failed",
params = mapOf(
AnalyticsParam.Key.SOURCE to source.value,
"Reason" to reason.value,
),
) {
sealed class BiometricFailReason(val value: String) {
data object AuthenticationLockout : BiometricFailReason("BiometricsAuthenticationLockout")
data object AuthenticationLockoutPermanent : BiometricFailReason("BiometricsAuthenticationLockoutPermanent")
data object BiometricsAuthenticationDisabled : BiometricFailReason("BiometricsAuthenticationDisabled")
data object AllKeysInvalidated : BiometricFailReason("AllKeysInvalidated")
data object AuthenticationCancelled : BiometricFailReason("AuthenticationCancelled")
data object AuthenticationAlreadyInProgress : BiometricFailReason("AuthenticationAlreadyInProgress")
data class Other(val reason: String) : BiometricFailReason(reason)
}
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics.models
/**
* Marker interface for analytics events that require special attention.
*
* Implemented by events listed in the analytics specification (events.csv).
*/
interface CriticalEvent

View file

@ -38,6 +38,10 @@ sealed class MainScreenAnalyticsEvent(
event = "Button - Receive",
)
class ButtonAddFunds : MainScreenAnalyticsEvent(
event = "Button - Add Funds",
)
class LimitsClicked : MainScreenAnalyticsEvent(
event = "Limits Clicked",
)

View file

@ -1,9 +1,6 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.getReferralParams
import com.tangem.core.analytics.models.*
sealed class OnboardingAnalyticsEvent(
category: String,
@ -16,23 +13,29 @@ sealed class OnboardingAnalyticsEvent(
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) {
/**
* Tracks the start of the onboarding process.
*/
class Started(
source: String,
source: AnalyticsParam.ScreensSources? = null,
) : Onboarding(
event = "Onboarding Started",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
params = buildMap {
source?.value?.let { put(AnalyticsParam.SOURCE, it) }
},
), CriticalEvent
/**
* Tracks the completion of the onboarding process.
*/
class Finished(
source: String,
source: AnalyticsParam.ScreensSources? = null,
) : Onboarding(
event = "Onboarding Finished",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
params = buildMap {
source?.value?.let { put(AnalyticsParam.SOURCE, it) }
},
), CriticalEvent
class ButtonMobileWallet(
source: String,
@ -49,31 +52,42 @@ sealed class OnboardingAnalyticsEvent(
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding / Create Wallet", event = event, params = params) {
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
/**
* Tracks opening of the create wallet screen.
*/
class ScreenOpened : CreateWallet("Create Wallet Screen Opened"), CriticalEvent
/**
* Tracks the user clicking the "Create Wallet" button.
*/
class ButtonCreateWallet : CreateWallet("Button - Create Wallet"), CriticalEvent
/**
* Tracks the user clicking the "Other Options" button on the create wallet screen.
*/
class ButtonOtherOptions : CreateWallet("Button - Other Options"), CriticalEvent
/**
* Tracks successful wallet creation, either on a Tangem card or as a mobile wallet.
*/
class WalletCreatedSuccessfully(
source: String,
creationType: WalletCreationType = WalletCreationType.NewSeed,
creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey,
seedPhraseLength: Int? = null,
passPhraseState: AnalyticsParam.EmptyFull,
referralId: String?,
source: AnalyticsParam.ScreensSources? = null,
) : CreateWallet(
event = "Wallet Created Successfully",
params = buildMap {
put(AnalyticsParam.SOURCE, source)
put("Creation Type", creationType.value)
put("Passphrase", passPhraseState.value)
if (seedPhraseLength != null) {
put("Seed Phrase Length", seedPhraseLength.toString())
}
source?.value?.let { put(AnalyticsParam.SOURCE, it) }
putAll(getReferralParams(referralId))
},
), AppsFlyerIncludedEvent
sealed class WalletCreationType(val value: String) {
data object NewSeed : WalletCreationType(value = "New Seed")
data object SeedImport : WalletCreationType(value = "Seed Import")
}
), AppsFlyerIncludedEvent, CriticalEvent
}
sealed class SeedPhrase(
@ -82,16 +96,19 @@ sealed class OnboardingAnalyticsEvent(
) : OnboardingAnalyticsEvent(category = "Onboarding / Seed Phrase", event = event, params = params) {
class CreateMobileScreenOpened(
source: String,
source: AnalyticsParam.ScreensSources,
) : SeedPhrase(
event = "Create Mobile Screen Opened",
params = mapOf(
AnalyticsParam.SOURCE to source,
AnalyticsParam.SOURCE to source.value,
),
), AppsFlyerIncludedEvent
class ButtonImportWallet : SeedPhrase("Button - Import Wallet")
class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened")
/**
* Tracks opening of the seed phrase import screen.
*/
class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened"), CriticalEvent
class ButtonImport : SeedPhrase("Button - Import")
}

View file

@ -2,20 +2,22 @@ package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.CriticalEvent
sealed class SignIn(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Sign In", event, params) {
) : AnalyticsEvent(category = "Sign In", event = event, params = params) {
data class ScreenOpened(
val walletsCount: Int,
) : SignIn(
/**
* Tracks the user landing on the app's sign-in screen when a saved card exists.
*/
class ScreenOpened(walletsCount: Int) : SignIn(
event = "Sign In Screen Opened",
params = mapOf(
"Wallets Count" to walletsCount.toString(),
AnalyticsParam.WALLETS_COUNT to walletsCount.toString(),
),
)
), CriticalEvent
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
@ -26,24 +28,17 @@ sealed class SignIn(
) : SignIn(event = "Error - Biometric Updated")
class ButtonWallet(
signInType: SignInType,
signInType: AnalyticsParam.SignInType,
walletsCount: Int,
) : SignIn(
event = "Button - Wallet",
params = buildMap {
put("Wallets Count", walletsCount.toString())
put("Sign in type", signInType.value)
put(AnalyticsParam.WALLETS_COUNT, walletsCount.toString())
put(AnalyticsParam.SIGN_IN_TYPE, signInType.value)
},
) {
enum class SignInType(val value: String) {
Card("Card"),
Biometric("Biometric"),
NoSecurity("No Security"),
AccessCode("Access Code"),
}
}
)
data class ButtonAddWallet(
class ButtonAddWallet(
val sources: AnalyticsParam.ScreensSources,
) : SignIn(
event = "Button - Add Wallet",