Updated on 2026-08-14
This commit is contained in:
parent
0fe533f267
commit
7a7204c6c4
32 changed files with 386 additions and 120 deletions
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue