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 index ecc4cf0363..5747dedc9e 100644 --- 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 @@ -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 { 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 index 07f9253ef9..e2cef74d35 100644 --- 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 @@ -8,5 +8,5 @@ interface ABTestsManager { fun removeUserProperties() - fun getValue(key: String, defaultValue: String): String + suspend 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 index 7869bdbc27..3e7baebb67 100644 --- 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 @@ -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() + + 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) { - 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) { + 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 } } \ 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 index 64aec57be3..0ec5293034 100644 --- 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 @@ -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 } } \ No newline at end of file diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/GetSwapUiModeUseCaseTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/GetSwapUiModeUseCaseTest.kt index 4d3f65e317..1e3d6a6c1c 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/GetSwapUiModeUseCaseTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/GetSwapUiModeUseCaseTest.kt @@ -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() 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 c15761c0d9..07e7b23df7 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,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), ), ), ;