Updated on 2026-08-14
This commit is contained in:
parent
0fe533f267
commit
7a7204c6c4
32 changed files with 386 additions and 120 deletions
|
|
@ -169,6 +169,7 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.abTests)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import com.tangem.TangemSdkLogger
|
|||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
|
|
@ -151,4 +153,8 @@ interface ApplicationEntryPoint {
|
|||
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
|
||||
|
||||
fun getWcInitializeUseCase(): WcInitializeUseCase
|
||||
|
||||
fun getTrackingContextProxy(): TrackingContextProxy
|
||||
|
||||
fun getABTestsManager(): ABTestsManager
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
|||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.ParamsInterceptor
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
|
|
@ -240,6 +241,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val wcInitializeUseCase
|
||||
get() = entryPoint.getWcInitializeUseCase()
|
||||
|
||||
private val trackingContextProxy
|
||||
get() = entryPoint.getTrackingContextProxy()
|
||||
|
||||
private val abTestsManager: ABTestsManager
|
||||
get() = entryPoint.getABTestsManager()
|
||||
|
||||
// endregion
|
||||
|
||||
private val appScope = MainScope()
|
||||
|
|
@ -310,6 +317,8 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
|
||||
}
|
||||
|
||||
abTestsManager.init()
|
||||
|
||||
appScope.launch {
|
||||
launch(Dispatchers.IO) {
|
||||
loadNativeLibraries()
|
||||
|
|
@ -385,6 +394,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
userWalletsListRepository = userWalletsListRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
hotWalletFeatureToggles = hotWalletFeatureToggles,
|
||||
trackingContextProxy = trackingContextProxy,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.addHotWalletContext
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.common.extensions.removeContext
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.extensions.setHotWalletContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
|
||||
|
||||
override fun setContext(scanResponse: ScanResponse) {
|
||||
Analytics.setContext(scanResponse)
|
||||
}
|
||||
|
||||
override fun addContext(userWallet: UserWallet) {
|
||||
Analytics.addContext(userWallet)
|
||||
}
|
||||
|
||||
override fun setHotWalletContext() {
|
||||
Analytics.setHotWalletContext()
|
||||
}
|
||||
|
||||
override fun eraseContext() {
|
||||
Analytics.eraseContext()
|
||||
}
|
||||
|
||||
override fun addContext(scanResponse: ScanResponse) {
|
||||
Analytics.addContext(scanResponse)
|
||||
}
|
||||
|
||||
override fun addHotWalletContext() {
|
||||
Analytics.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun removeContext() {
|
||||
Analytics.removeContext()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
|
||||
internal class DefaultChangeCardAnalyticsContextUseCase : ChangeCardAnalyticsContextUseCase {
|
||||
|
||||
override fun invoke(scanResponse: ScanResponse) {
|
||||
Analytics.setContext(scanResponse)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.addHotWalletContext
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.common.extensions.removeContext
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.extensions.setHotWalletContext
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsManager) : TrackingContextProxy {
|
||||
|
||||
override fun setContext(scanResponse: ScanResponse) {
|
||||
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
|
||||
Analytics.setContext(userWalletId, scanResponse)
|
||||
|
||||
abTestsManager.setUserProperties(
|
||||
userId = calculateUserIdHash(userWalletId),
|
||||
batch = scanResponse.card.batchId,
|
||||
productType = scanResponse.productType.name,
|
||||
firmware = scanResponse.card.firmwareVersion.stringValue,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setContext(userWallet: UserWallet) {
|
||||
Analytics.setContext(userWallet)
|
||||
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
setColdWalletUserProperties(userWallet)
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
setHotWalletUserProperties(userWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addContext(userWallet: UserWallet) {
|
||||
Analytics.addContext(userWallet)
|
||||
}
|
||||
|
||||
override fun setHotWalletContext() {
|
||||
Analytics.setHotWalletContext()
|
||||
}
|
||||
|
||||
override fun eraseContext() {
|
||||
Analytics.eraseContext()
|
||||
abTestsManager.removeUserProperties()
|
||||
}
|
||||
|
||||
override fun addContext(scanResponse: ScanResponse) {
|
||||
Analytics.addContext(scanResponse)
|
||||
}
|
||||
|
||||
override fun addHotWalletContext() {
|
||||
Analytics.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun removeContext() {
|
||||
Analytics.removeContext()
|
||||
}
|
||||
|
||||
private fun calculateUserIdHash(userWalletId: UserWalletId?): String? {
|
||||
return userWalletId?.value
|
||||
?.calculateSha256()
|
||||
?.toHexString()
|
||||
}
|
||||
|
||||
private fun setColdWalletUserProperties(userWallet: UserWallet.Cold) {
|
||||
abTestsManager.setUserProperties(
|
||||
userId = calculateUserIdHash(userWallet.walletId),
|
||||
batch = userWallet.scanResponse.card.batchId,
|
||||
productType = userWallet.scanResponse.productType.name,
|
||||
firmware = userWallet.scanResponse.card.firmwareVersion.stringValue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setHotWalletUserProperties(userWallet: UserWallet.Hot) {
|
||||
abTestsManager.setUserProperties(
|
||||
userId = calculateUserIdHash(userWallet.walletId),
|
||||
batch = null,
|
||||
productType = "Mobile Wallet",
|
||||
firmware = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
|
||||
|
|
@ -14,8 +15,7 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc
|
|||
/**
|
||||
* Sets the new context
|
||||
*/
|
||||
fun Analytics.setContext(scanResponse: ScanResponse) {
|
||||
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
fun Analytics.setContext(userWalletId: UserWalletId?, scanResponse: ScanResponse) {
|
||||
if (userWalletId != null) {
|
||||
setUserId(userWalletId.stringValue)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ fun Analytics.addContext(userWallet: UserWallet) {
|
|||
is UserWallet.Hot -> HotWalletContextInterceptor(parent = currentContext)
|
||||
}
|
||||
|
||||
setUserId(userWalletId = userWallet.walletId.stringValue)
|
||||
setUserId(userId = userWallet.walletId.stringValue)
|
||||
addParamsInterceptor(newContext)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.tap.di.analytics
|
||||
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.AppInstanceIdProvider
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
|
||||
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
|
||||
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.tap.common.analytics.DefaultTrackingContextProxy
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -18,13 +17,8 @@ internal object AnalyticsModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase {
|
||||
return DefaultChangeCardAnalyticsContextUseCase()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
|
||||
fun provideAnalyticsContextProxy(abtestsManager: ABTestsManager): TrackingContextProxy =
|
||||
DefaultTrackingContextProxy(abtestsManager)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package com.tangem.tap.domain
|
|||
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
|
|
@ -34,7 +34,8 @@ class TapWalletManager(
|
|||
}
|
||||
|
||||
private suspend fun loadUserWalletData(userWallet: UserWallet) {
|
||||
Analytics.setContext(userWallet)
|
||||
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
|
||||
trackingContextProxy.setContext(userWallet)
|
||||
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -47,6 +48,7 @@ import javax.inject.Singleton
|
|||
internal class LegacyScanProcessor @Inject constructor(
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
) {
|
||||
|
||||
suspend fun scan(
|
||||
|
|
@ -211,7 +213,7 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
},
|
||||
) {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
Analytics.addContext(scanResponse)
|
||||
trackingContextProxy.addContext(scanResponse)
|
||||
onWalletNotCreated()
|
||||
navigateTo(
|
||||
AppRoute.Onboarding(
|
||||
|
|
@ -220,7 +222,7 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
),
|
||||
) { onProgressStateChange(it) }
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
trackingContextProxy.setContext(scanResponse)
|
||||
|
||||
val wasTwinsOnboardingShown =
|
||||
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@ package com.tangem.tap.domain.scanCard.chains
|
|||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.card.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.core.chain.Chain
|
||||
import com.tangem.domain.core.chain.ResultChain
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -34,9 +31,11 @@ class CheckForOnboardingChain(
|
|||
) : ResultChain<ScanCardException, ScanResponse>() {
|
||||
|
||||
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
|
||||
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
|
||||
|
||||
return when {
|
||||
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
|
||||
Analytics.addContext(previousChainResult)
|
||||
trackingContextProxy.addContext(previousChainResult)
|
||||
ScanChainException.OnboardingNeeded(
|
||||
AppRoute.Onboarding(
|
||||
scanResponse = previousChainResult,
|
||||
|
|
@ -45,7 +44,7 @@ class CheckForOnboardingChain(
|
|||
).left()
|
||||
}
|
||||
else -> {
|
||||
Analytics.setContext(previousChainResult)
|
||||
trackingContextProxy.setContext(previousChainResult)
|
||||
|
||||
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
|
||||
.invokeSync()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.common.keyboard.KeyboardValidator
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -38,7 +38,6 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
|
||||
import com.tangem.feature.swap.analytics.StoriesEvents
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.network.exchangeServices.SellService
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
|
|
@ -82,6 +81,7 @@ internal class MainViewModel @Inject constructor(
|
|||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val appRouterConfig: AppRouterConfig,
|
||||
private val sellService: SellService,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ internal class MainViewModel @Inject constructor(
|
|||
.mapLeft { emptyFlow<UserWallet>() }
|
||||
.onRight { wallet ->
|
||||
wallet.distinctUntilChanged()
|
||||
.onEach { Analytics.setContext(it) }
|
||||
.onEach { trackingContextProxy.setContext(it) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ internal class WelcomeMiddleware {
|
|||
val currency = ParamCardCurrencyConverter().convert(
|
||||
value = scanResponse.cardTypesResolver,
|
||||
)
|
||||
Analytics.addContext(scanResponse)
|
||||
|
||||
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
|
||||
trackingContextProxy.addContext(scanResponse)
|
||||
|
||||
if (currency != null) {
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux
|
|||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
|
|
@ -79,4 +80,5 @@ data class DaggerGraphState(
|
|||
val userWalletsListRepository: UserWalletsListRepository? = null,
|
||||
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
|
||||
val tangemHotSdk: TangemHotSdk? = null,
|
||||
val trackingContextProxy: TrackingContextProxy? = null,
|
||||
) : StateType
|
||||
1
core/ab-tests/.gitignore
vendored
Normal file
1
core/ab-tests/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
31
core/ab-tests/build.gradle.kts
Normal file
31
core/ab-tests/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.abtests"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Amplitude experiment */
|
||||
implementation(deps.amplitude.experiment)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.core.abtests.di
|
||||
|
||||
import android.app.Application
|
||||
import com.tangem.core.abtests.BuildConfig
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ABTestsManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideABTestsManager(
|
||||
application: Application,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ABTestsManager {
|
||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||
StubABTestsManager()
|
||||
} else {
|
||||
AmplitudeABTestsManager(
|
||||
application = application,
|
||||
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.abtests.manager
|
||||
|
||||
interface ABTestsManager {
|
||||
|
||||
fun init()
|
||||
|
||||
fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?)
|
||||
|
||||
fun removeUserProperties()
|
||||
|
||||
fun getValue(key: String, defaultValue: String): String
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import android.app.Application
|
||||
import com.amplitude.experiment.Experiment
|
||||
import com.amplitude.experiment.ExperimentClient
|
||||
import com.amplitude.experiment.ExperimentConfig
|
||||
import com.amplitude.experiment.ExperimentUser
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
val apiKeyProvider: Provider<String>,
|
||||
val scope: CoroutineScope,
|
||||
) : ABTestsManager {
|
||||
|
||||
private lateinit var client: ExperimentClient
|
||||
|
||||
override fun init() {
|
||||
if (::client.isInitialized) {
|
||||
Timber.w("AB Tests manager already initialized, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
client = Experiment.initializeWithAmplitudeAnalytics(
|
||||
application = application,
|
||||
apiKey = apiKeyProvider(),
|
||||
config = ExperimentConfig
|
||||
.builder()
|
||||
.automaticFetchOnAmplitudeIdentityChange(true)
|
||||
.build(),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
client.fetch().get()
|
||||
val allVariants = client.all()
|
||||
logAllVariants(allVariants)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to fetch AB test variants")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
val userProperties = mutableMapOf<String, Any>()
|
||||
batch?.let { userProperties[AnalyticsParam.BATCH] = it }
|
||||
productType?.let { userProperties[AnalyticsParam.PRODUCT_TYPE] = it }
|
||||
firmware?.let { userProperties[AnalyticsParam.FIRMWARE] = it }
|
||||
|
||||
client.setUser(
|
||||
ExperimentUser
|
||||
.builder()
|
||||
.userId(userId)
|
||||
.userProperties(userProperties)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
client.setUser(ExperimentUser())
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return client.variant(key).value ?: defaultValue
|
||||
}
|
||||
|
||||
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
|
||||
if (allVariants.isEmpty()) {
|
||||
Timber.d("No variants available")
|
||||
} else {
|
||||
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
||||
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
|
||||
Timber.d(" → Value: ${variant.value ?: "null"}")
|
||||
Timber.d(" → Payload: ${variant.payload}")
|
||||
Timber.d(" → Key: ${variant.key}")
|
||||
Timber.d("-".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SEPARATOR_LENGTH = 50
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
|
||||
internal class StubABTestsManager : ABTestsManager {
|
||||
|
||||
override fun init() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
@ -61,9 +61,9 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
return paramsInterceptors.remove(interceptorId)
|
||||
}
|
||||
|
||||
override fun setUserId(userWalletId: String) {
|
||||
override fun setUserId(userId: String) {
|
||||
analyticsScope.launch {
|
||||
val userIdHash = userWalletId.hexToBytes()
|
||||
val userIdHash = userId.hexToBytes()
|
||||
.calculateSha256()
|
||||
.toHexString()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AnalyticsContextProxy {
|
||||
interface TrackingContextProxy {
|
||||
|
||||
fun setContext(scanResponse: ScanResponse)
|
||||
|
||||
fun setContext(userWallet: UserWallet)
|
||||
|
||||
fun addContext(userWallet: UserWallet)
|
||||
|
||||
fun setHotWalletContext()
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.domain.analytics
|
||||
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
interface ChangeCardAnalyticsContextUseCase {
|
||||
|
||||
operator fun invoke(scanResponse: ScanResponse)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.entry
|
|||
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -33,7 +33,7 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
) : Model() {
|
||||
|
||||
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
|
||||
|
|
@ -48,12 +48,12 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
trackingContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onChildBack() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -26,7 +26,7 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
private val router: Router,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
|
|
@ -40,12 +40,12 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
trackingContextProxy.removeContext()
|
||||
}
|
||||
|
||||
private fun onImportClick() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
|
|||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -26,7 +26,7 @@ internal class CreateWalletBackupModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
|
||||
|
|
@ -41,12 +41,12 @@ internal class CreateWalletBackupModel @Inject constructor(
|
|||
val currentRoute: MutableStateFlow<CreateWalletBackupRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
trackingContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
|
|||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -40,7 +40,7 @@ internal class WalletActivationModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<WalletActivationComponent.Params>()
|
||||
|
|
@ -59,12 +59,12 @@ internal class WalletActivationModel @Inject constructor(
|
|||
val currentRoute: MutableStateFlow<WalletActivationRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
trackingContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onChildBack() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -70,7 +70,7 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
private val accountItemsDelegate: AccountItemsDelegate,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
walletCardItemDelegateFactory: WalletCardItemDelegate.Factory,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase,
|
||||
|
|
@ -112,7 +112,7 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
|
||||
init {
|
||||
getUserWalletUseCase.invoke(params.userWalletId).onRight {
|
||||
analyticsContextProxy.addContext(it)
|
||||
trackingContextProxy.addContext(it)
|
||||
}
|
||||
|
||||
fun combineUI(wallet: UserWallet) = combine(
|
||||
|
|
@ -160,7 +160,7 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
trackingContextProxy.removeContext()
|
||||
}
|
||||
|
||||
private fun isNotificationsPermissionGranted(): Boolean {
|
||||
|
|
@ -263,7 +263,7 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
|
||||
private fun onLinkMoreCardsClick(scanResponse: ScanResponse) {
|
||||
analyticsEventHandler.send(Settings.ButtonCreateBackup)
|
||||
analyticsContextProxy.addContext(scanResponse)
|
||||
trackingContextProxy.addContext(scanResponse)
|
||||
|
||||
router.push(
|
||||
AppRoute.Onboarding(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ compose-lifecycle-runtime = "2.7.0"
|
|||
# region Other libraries
|
||||
appsflyer = "6.17.3"
|
||||
amplitude = "2.36.1"
|
||||
amplitude-experiment = "1.13.1"
|
||||
armadillo = "0.9.0"
|
||||
coil = "2.1.0"
|
||||
compose-shimmer = "1.0.3"
|
||||
|
|
@ -237,6 +238,7 @@ test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orch
|
|||
# region Other
|
||||
appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer" }
|
||||
amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" }
|
||||
amplitude-experiment = { module = "com.amplitude:experiment-android-client", version.ref = "amplitude-experiment" }
|
||||
armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" }
|
||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
|
||||
|
|
|
|||
|
|
@ -32,4 +32,10 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v
|
|||
name = "MOCK_DATA_SOURCE",
|
||||
value = isEnabled.toString(),
|
||||
)
|
||||
|
||||
class ABTestsEnabled(isEnabled: Boolean) : BuildConfigField(
|
||||
type = "Boolean",
|
||||
name = "AB_TESTS_ENABLED",
|
||||
value = isEnabled.toString(),
|
||||
)
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ internal enum class BuildType(
|
|||
BuildConfigField.LogEnabled(isEnabled = true),
|
||||
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
||||
BuildConfigField.MockDataSource(isEnabled = false),
|
||||
BuildConfigField.ABTestsEnabled(isEnabled = false),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ internal enum class BuildType(
|
|||
BuildConfigField.LogEnabled(isEnabled = true),
|
||||
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
||||
BuildConfigField.MockDataSource(isEnabled = true),
|
||||
BuildConfigField.ABTestsEnabled(isEnabled = false),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -72,11 +74,12 @@ internal enum class BuildType(
|
|||
BuildConfigField.LogEnabled(isEnabled = true),
|
||||
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
||||
BuildConfigField.MockDataSource(isEnabled = false),
|
||||
BuildConfigField.ABTestsEnabled(isEnabled = true),
|
||||
),
|
||||
),
|
||||
|
||||
/**
|
||||
* Build type for QA and business
|
||||
* Build type for support with possible bugfixes for certain cases
|
||||
*
|
||||
* Features:
|
||||
* - Env: prod
|
||||
|
|
@ -92,6 +95,7 @@ internal enum class BuildType(
|
|||
BuildConfigField.LogEnabled(isEnabled = false),
|
||||
BuildConfigField.TesterMenuAvailability(isEnabled = false),
|
||||
BuildConfigField.MockDataSource(isEnabled = false),
|
||||
BuildConfigField.ABTestsEnabled(isEnabled = false),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -110,6 +114,7 @@ internal enum class BuildType(
|
|||
BuildConfigField.LogEnabled(isEnabled = false),
|
||||
BuildConfigField.TesterMenuAvailability(isEnabled = false),
|
||||
BuildConfigField.MockDataSource(isEnabled = false),
|
||||
BuildConfigField.ABTestsEnabled(isEnabled = true),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
|
@ -154,6 +154,7 @@ include(":core:analytics")
|
|||
include(":core:analytics:models")
|
||||
include(":core:datasource")
|
||||
include(":core:config-toggles")
|
||||
include(":core:ab-tests")
|
||||
include(":core:navigation")
|
||||
include(":core:res")
|
||||
include(":core:ui")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue