Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-10 16:28:47 +03:00
parent a7d501adc9
commit 9bf5707cac
6 changed files with 62 additions and 36 deletions

View file

@ -27,7 +27,13 @@ internal object ABTestsManagerModule {
return if (BuildConfig.AB_TESTS_ENABLED) {
AmplitudeABTestsManager(
application = application,
apiKey = environmentConfig.amplitudeApiKey,
apiKey = if (BuildConfig.TESTER_MENU_ENABLED) {
requireNotNull(environmentConfig.amplitudeApiKeyDev) {
"Amplitude api key not found in ${BuildConfig.BUILD_TYPE}"
}
} else {
environmentConfig.amplitudeApiKey
},
scope = appScope,
)
} else {

View file

@ -8,5 +8,5 @@ interface ABTestsManager {
fun removeUserProperties()
fun getValue(key: String, defaultValue: String): String
suspend fun getValue(key: String, defaultValue: String): String
}

View file

@ -9,7 +9,9 @@ import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
internal class AmplitudeABTestsManager(
val application: Application,
@ -19,9 +21,13 @@ internal class AmplitudeABTestsManager(
private lateinit var client: ExperimentClient
private val variantsFetched = CompletableDeferred<Unit>()
private val logger = TangemLogger.withTag(TAG)
override fun init() {
if (::client.isInitialized) {
TangemLogger.w("AB Tests manager already initialized, skipping")
logger.w("AB Tests manager already initialized, skipping")
return
}
@ -40,7 +46,9 @@ internal class AmplitudeABTestsManager(
val allVariants = client.all()
logAllVariants(allVariants)
} catch (exception: Exception) {
TangemLogger.e("Failed to fetch AB test variants", exception)
logger.e("Failed to fetch AB test variants", exception)
} finally {
variantsFetched.complete(Unit)
}
}
}
@ -64,31 +72,45 @@ internal class AmplitudeABTestsManager(
client.setUser(ExperimentUser())
}
override fun getValue(key: String, defaultValue: String): String {
override suspend fun getValue(key: String, defaultValue: String): String {
if (!::client.isInitialized) return defaultValue
awaitVariantsFetched()
return client.variant(key).value ?: defaultValue
}
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
TangemLogger.d("AB Tests: Fetched ${allVariants.size} variants")
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
private suspend fun awaitVariantsFetched() {
if (variantsFetched.isCompleted) return
val completed = withTimeoutOrNull(FETCH_AWAIT_TIMEOUT_MILLIS) {
variantsFetched.await()
}
if (completed == null) {
logger.w("AB Tests variants not fetched within $FETCH_AWAIT_TIMEOUT_MILLIS ms, using default value")
// Prevent repeated blocking on subsequent calls; fetch can still complete in background.
variantsFetched.complete(Unit)
}
}
if (allVariants.isEmpty()) {
TangemLogger.d("No variants available")
} else {
allVariants.entries.forEachIndexed { index, (key, variant) ->
TangemLogger.d("[${index + 1}/${allVariants.size}] Key: $key")
TangemLogger.d(" → Value: ${variant.value ?: "null"}")
TangemLogger.d(" → Payload: ${variant.payload ?: "null"}")
TangemLogger.d(" → Key: ${variant.key ?: "null"}")
TangemLogger.d("-".repeat(SEPARATOR_LENGTH))
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
val message = buildString {
appendLine("AB Tests: Fetched ${allVariants.size} variants")
if (allVariants.isEmpty()) {
append("No variants available")
} else {
allVariants.entries.forEachIndexed { index, (key, variant) ->
appendLine("[${index + 1}/${allVariants.size}] $key")
appendLine(" → value: ${variant.value ?: "null"}")
appendLine(" → key: ${variant.key ?: "null"}")
append(" → payload: ${variant.payload ?: "null"}")
if (index != allVariants.size - 1) appendLine()
}
}
}
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
logger.i(message)
}
private companion object {
const val SEPARATOR_LENGTH = 50
const val TAG = "AmplitudeABTestsManager"
const val FETCH_AWAIT_TIMEOUT_MILLIS = 3_000L
}
}

View file

@ -16,7 +16,7 @@ internal class StubABTestsManager : ABTestsManager {
// intentionally do nothing
}
override fun getValue(key: String, defaultValue: String): String {
override suspend fun getValue(key: String, defaultValue: String): String {
return defaultValue
}
}

View file

@ -7,9 +7,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapUIMode
import com.tangem.features.swap.SwapFeatureToggles
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@ -34,7 +32,7 @@ internal class GetSwapUiModeUseCaseTest {
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
coVerify(exactly = 0) { swapRepository.getStoredSwapUiMode() }
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
}
@Test
@ -46,7 +44,7 @@ internal class GetSwapUiModeUseCaseTest {
val actual = sut.invoke()
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
}
@Test
@ -58,7 +56,7 @@ internal class GetSwapUiModeUseCaseTest {
val actual = sut.invoke()
assertThat(actual).isEqualTo(SwapUIMode.Simple)
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
}
@Test
@ -66,24 +64,24 @@ internal class GetSwapUiModeUseCaseTest {
runTest {
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
coEvery { swapRepository.getStoredSwapUiMode() } returns null
every { abTestsManager.getValue("swap_form_variant", "detailed") } returns "detailed"
coEvery { abTestsManager.getValue("swap_form_variant", "detailed") } returns "detailed"
val actual = sut.invoke()
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
verify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
coVerify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
}
@Test
fun `GIVEN toggle enabled and repository empty and AB returns simple WHEN invoke THEN returns Simple`() = runTest {
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
coEvery { swapRepository.getStoredSwapUiMode() } returns null
every { abTestsManager.getValue("swap_form_variant", "detailed") } returns "simple"
coEvery { abTestsManager.getValue("swap_form_variant", "detailed") } returns "simple"
val actual = sut.invoke()
assertThat(actual).isEqualTo(SwapUIMode.Simple)
verify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
coVerify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
}
@Test
@ -91,7 +89,7 @@ internal class GetSwapUiModeUseCaseTest {
runTest {
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
coEvery { swapRepository.getStoredSwapUiMode() } returns null
every { abTestsManager.getValue("swap_form_variant", "detailed") } returns "SIMPLE"
coEvery { abTestsManager.getValue("swap_form_variant", "detailed") } returns "SIMPLE"
val actual = sut.invoke()
@ -103,7 +101,7 @@ internal class GetSwapUiModeUseCaseTest {
runTest {
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
coEvery { swapRepository.getStoredSwapUiMode() } returns null
every { abTestsManager.getValue("swap_form_variant", "detailed") } returns "something_else"
coEvery { abTestsManager.getValue("swap_form_variant", "detailed") } returns "something_else"
val actual = sut.invoke()
@ -115,7 +113,7 @@ internal class GetSwapUiModeUseCaseTest {
runTest {
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
coEvery { swapRepository.getStoredSwapUiMode() } returns null
every { abTestsManager.getValue("swap_form_variant", "detailed") } returns ""
coEvery { abTestsManager.getValue("swap_form_variant", "detailed") } returns ""
val actual = sut.invoke()

View file

@ -28,7 +28,7 @@ enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
),
),
@ -74,7 +74,7 @@ enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
),
),
@ -114,7 +114,7 @@ enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = false),
BuildConfigField.TesterMenuAvailability(isEnabled = false),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
),
),
;