diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 042bf40240..9423329489 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 7098dad4f2..59dd05efbe 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0920d365f9..7804a24b4f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt deleted file mode 100644 index b566a5472a..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt deleted file mode 100644 index 27f55538da..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt new file mode 100644 index 0000000000..41a70887ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index be2a667bb0..0e011c8b21 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index 60fea2b718..c290c819a2 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 9deccc04e6..76f033a638 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 17ce65a238..06178511a5 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index f632d90194..a3693ba6d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -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() { 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() diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index ddc7090242..8a3d691e60 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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() } .onRight { wallet -> wallet.distinctUntilChanged() - .onEach { Analytics.setContext(it) } + .onEach { trackingContextProxy.setContext(it) } .flowOn(dispatchers.io) .launchIn(viewModelScope) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 6797731a84..f912f0948a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 0b32197bce..8095c3df87 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -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 \ No newline at end of file diff --git a/core/ab-tests/.gitignore b/core/ab-tests/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/ab-tests/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/ab-tests/build.gradle.kts b/core/ab-tests/build.gradle.kts new file mode 100644 index 0000000000..d47ebe83f1 --- /dev/null +++ b/core/ab-tests/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt new file mode 100644 index 0000000000..3934f4ba5c --- /dev/null +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt @@ -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()), + ) + } + } +} \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/ABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/ABTestsManager.kt new file mode 100644 index 0000000000..07f9253ef9 --- /dev/null +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/ABTestsManager.kt @@ -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 +} \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt new file mode 100644 index 0000000000..b1af356975 --- /dev/null +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -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, + 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() + 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) { + 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 + } +} \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/StubABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/StubABTestsManager.kt new file mode 100644 index 0000000000..64aec57be3 --- /dev/null +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/StubABTestsManager.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index a06679e232..9703947954 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -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() diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt b/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt similarity index 84% rename from core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt rename to core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt index 5ea57db8ad..23be99e1fd 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt @@ -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() diff --git a/domain/legacy/src/main/java/com/tangem/domain/analytics/ChangeCardAnalyticsContextUseCase.kt b/domain/legacy/src/main/java/com/tangem/domain/analytics/ChangeCardAnalyticsContextUseCase.kt deleted file mode 100644 index 3aa5e994c9..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/analytics/ChangeCardAnalyticsContextUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.analytics - -import com.tangem.domain.models.scan.ScanResponse - -interface ChangeCardAnalyticsContextUseCase { - - operator fun invoke(scanResponse: ScanResponse) -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index c8506fe4f2..c1abf8b0fe 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -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 = MutableStateFlow(startRoute) init { - analyticsContextProxy.addHotWalletContext() + trackingContextProxy.addHotWalletContext() } override fun onDestroy() { super.onDestroy() - analyticsContextProxy.removeContext() + trackingContextProxy.removeContext() } fun onChildBack() { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 8607a0715b..800b694885 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -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 @@ -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() { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt index 0e1da6c079..54458af212 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -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() @@ -41,12 +41,12 @@ internal class CreateWalletBackupModel @Inject constructor( val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) init { - analyticsContextProxy.addHotWalletContext() + trackingContextProxy.addHotWalletContext() } override fun onDestroy() { super.onDestroy() - analyticsContextProxy.removeContext() + trackingContextProxy.removeContext() } fun onBack() { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index eb31a235f9..71fc8bb713 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -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() @@ -59,12 +59,12 @@ internal class WalletActivationModel @Inject constructor( val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) init { - analyticsContextProxy.addHotWalletContext() + trackingContextProxy.addHotWalletContext() } override fun onDestroy() { super.onDestroy() - analyticsContextProxy.removeContext() + trackingContextProxy.removeContext() } fun onChildBack() { diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 082180535d..b442bd86a7 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -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( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 028cf3611c..695f7f7021 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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" } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 058a4ccb06..af4e41474e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -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(), + ) } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 896952a0e8..a0814f9003 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -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), ), ), } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d69405d8eb..4e3af1e28f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")