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) {
|
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||||
AmplitudeABTestsManager(
|
AmplitudeABTestsManager(
|
||||||
application = application,
|
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,
|
scope = appScope,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,5 @@ interface ABTestsManager {
|
||||||
|
|
||||||
fun removeUserProperties()
|
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.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
internal class AmplitudeABTestsManager(
|
internal class AmplitudeABTestsManager(
|
||||||
val application: Application,
|
val application: Application,
|
||||||
|
|
@ -19,9 +21,13 @@ internal class AmplitudeABTestsManager(
|
||||||
|
|
||||||
private lateinit var client: ExperimentClient
|
private lateinit var client: ExperimentClient
|
||||||
|
|
||||||
|
private val variantsFetched = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
private val logger = TangemLogger.withTag(TAG)
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
if (::client.isInitialized) {
|
if (::client.isInitialized) {
|
||||||
TangemLogger.w("AB Tests manager already initialized, skipping")
|
logger.w("AB Tests manager already initialized, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +46,9 @@ internal class AmplitudeABTestsManager(
|
||||||
val allVariants = client.all()
|
val allVariants = client.all()
|
||||||
logAllVariants(allVariants)
|
logAllVariants(allVariants)
|
||||||
} catch (exception: Exception) {
|
} 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())
|
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
|
return client.variant(key).value ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
private suspend fun awaitVariantsFetched() {
|
||||||
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
|
if (variantsFetched.isCompleted) return
|
||||||
TangemLogger.d("AB Tests: Fetched ${allVariants.size} variants")
|
val completed = withTimeoutOrNull(FETCH_AWAIT_TIMEOUT_MILLIS) {
|
||||||
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
|
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()) {
|
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
||||||
TangemLogger.d("No variants available")
|
val message = buildString {
|
||||||
} else {
|
appendLine("AB Tests: Fetched ${allVariants.size} variants")
|
||||||
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
if (allVariants.isEmpty()) {
|
||||||
TangemLogger.d("[${index + 1}/${allVariants.size}] Key: $key")
|
append("No variants available")
|
||||||
TangemLogger.d(" → Value: ${variant.value ?: "null"}")
|
} else {
|
||||||
TangemLogger.d(" → Payload: ${variant.payload ?: "null"}")
|
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
||||||
TangemLogger.d(" → Key: ${variant.key ?: "null"}")
|
appendLine("[${index + 1}/${allVariants.size}] $key")
|
||||||
TangemLogger.d("-".repeat(SEPARATOR_LENGTH))
|
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 {
|
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
|
// intentionally do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValue(key: String, defaultValue: String): String {
|
override suspend fun getValue(key: String, defaultValue: String): String {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,9 +7,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||||
import com.tangem.features.swap.SwapFeatureToggles
|
import com.tangem.features.swap.SwapFeatureToggles
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
@ -34,7 +32,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
||||||
coVerify(exactly = 0) { swapRepository.getStoredSwapUiMode() }
|
coVerify(exactly = 0) { swapRepository.getStoredSwapUiMode() }
|
||||||
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -46,7 +44,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
val actual = sut.invoke()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
||||||
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -58,7 +56,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
val actual = sut.invoke()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(SwapUIMode.Simple)
|
assertThat(actual).isEqualTo(SwapUIMode.Simple)
|
||||||
verify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -66,24 +64,24 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
||||||
coEvery { swapRepository.getStoredSwapUiMode() } returns null
|
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()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
assertThat(actual).isEqualTo(SwapUIMode.Detailed)
|
||||||
verify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
|
coVerify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN toggle enabled and repository empty and AB returns simple WHEN invoke THEN returns Simple`() = runTest {
|
fun `GIVEN toggle enabled and repository empty and AB returns simple WHEN invoke THEN returns Simple`() = runTest {
|
||||||
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
||||||
coEvery { swapRepository.getStoredSwapUiMode() } returns null
|
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()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(SwapUIMode.Simple)
|
assertThat(actual).isEqualTo(SwapUIMode.Simple)
|
||||||
verify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
|
coVerify(exactly = 1) { abTestsManager.getValue("swap_form_variant", "detailed") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -91,7 +89,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
||||||
coEvery { swapRepository.getStoredSwapUiMode() } returns null
|
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()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
|
|
@ -103,7 +101,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
||||||
coEvery { swapRepository.getStoredSwapUiMode() } returns null
|
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()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
|
|
@ -115,7 +113,7 @@ internal class GetSwapUiModeUseCaseTest {
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
coEvery { swapFeatureToggles.isSwapAbEnabled } returns true
|
||||||
coEvery { swapRepository.getStoredSwapUiMode() } returns null
|
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()
|
val actual = sut.invoke()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ enum class BuildType(
|
||||||
BuildConfigField.LogEnabled(isEnabled = true),
|
BuildConfigField.LogEnabled(isEnabled = true),
|
||||||
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
||||||
BuildConfigField.MockDataSource(isEnabled = false),
|
BuildConfigField.MockDataSource(isEnabled = false),
|
||||||
BuildConfigField.ABTestsEnabled(isEnabled = false),
|
BuildConfigField.ABTestsEnabled(isEnabled = true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ enum class BuildType(
|
||||||
BuildConfigField.LogEnabled(isEnabled = true),
|
BuildConfigField.LogEnabled(isEnabled = true),
|
||||||
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
BuildConfigField.TesterMenuAvailability(isEnabled = true),
|
||||||
BuildConfigField.MockDataSource(isEnabled = false),
|
BuildConfigField.MockDataSource(isEnabled = false),
|
||||||
BuildConfigField.ABTestsEnabled(isEnabled = false),
|
BuildConfigField.ABTestsEnabled(isEnabled = true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ enum class BuildType(
|
||||||
BuildConfigField.LogEnabled(isEnabled = false),
|
BuildConfigField.LogEnabled(isEnabled = false),
|
||||||
BuildConfigField.TesterMenuAvailability(isEnabled = false),
|
BuildConfigField.TesterMenuAvailability(isEnabled = false),
|
||||||
BuildConfigField.MockDataSource(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