Updated on 2026-08-14
This commit is contained in:
parent
a7d501adc9
commit
9bf5707cac
6 changed files with 62 additions and 36 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ interface ABTestsManager {
|
|||
|
||||
fun removeUserProperties()
|
||||
|
||||
fun getValue(key: String, defaultValue: String): String
|
||||
suspend fun getValue(key: String, defaultValue: String): String
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue