Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support bootstrapping initial local eval flags #48

Merged
merged 2 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.amplitude.experiment

import com.amplitude.experiment.evaluation.EvaluationEngineImpl
import com.amplitude.experiment.evaluation.EvaluationFlag
import com.amplitude.experiment.evaluation.json
import com.amplitude.experiment.evaluation.topologicalSort
import com.amplitude.experiment.storage.LoadStoreCache
import com.amplitude.experiment.storage.Storage
Expand All @@ -21,6 +22,7 @@ import com.amplitude.experiment.util.merge
import com.amplitude.experiment.util.toEvaluationContext
import com.amplitude.experiment.util.toJson
import com.amplitude.experiment.util.toVariant
import kotlinx.serialization.decodeFromString
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
Expand Down Expand Up @@ -70,6 +72,7 @@ internal class DefaultExperimentClient internal constructor(
init {
this.variants.load()
this.flags.load()
mergeInitialFlagsWithStorage()
}

private val backoffLock = Any()
Expand Down Expand Up @@ -362,6 +365,7 @@ internal class DefaultExperimentClient internal constructor(
this.flags.clear()
this.flags.putAll(flags)
this.flags.store()
mergeInitialFlagsWithStorage()
}
}
}
Expand Down Expand Up @@ -649,6 +653,17 @@ internal class DefaultExperimentClient internal constructor(
}
return defaultVariantAndSource
}

private fun mergeInitialFlagsWithStorage() {
if (config.initialFlags != null) {
val initialFlags: List<EvaluationFlag> = json.decodeFromString(config.initialFlags)
for (flag in initialFlags) {
if (this.flags.get(flag.key) == null) {
this.flags.put(flag.key, flag)
}
}
}
}
}

data class VariantAndSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ExperimentClient {
* @see fetch
* @see variant
*/
fun start(user: ExperimentUser?): Future<ExperimentClient>
fun start(user: ExperimentUser? = null): Future<ExperimentClient>

/**
* Stop the local flag configuration poller.
Expand Down
14 changes: 14 additions & 0 deletions sdk/src/main/java/com/amplitude/experiment/ExperimentConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class ExperimentConfig internal constructor(
@JvmField
val fallbackVariant: Variant = Defaults.FALLBACK_VARIANT,
@JvmField
val initialFlags: String? = Defaults.INITIAL_FLAGS,
@JvmField
val initialVariants: Map<String, Variant> = Defaults.INITIAL_VARIANTS,
@JvmField
val source: Source = Defaults.SOURCE,
Expand Down Expand Up @@ -81,6 +83,11 @@ class ExperimentConfig internal constructor(
*/
val FALLBACK_VARIANT: Variant = Variant()

/**
* null
*/
val INITIAL_FLAGS: String? = null

/**
* Empty Map<String, Variant>
*/
Expand Down Expand Up @@ -165,6 +172,7 @@ class ExperimentConfig internal constructor(
private var debug = Defaults.DEBUG
private var instanceName = Defaults.INSTANCE_NAME
private var fallbackVariant = Defaults.FALLBACK_VARIANT
private var initialFlags = Defaults.INITIAL_FLAGS
private var initialVariants = Defaults.INITIAL_VARIANTS
private var source = Defaults.SOURCE
private var serverUrl = Defaults.SERVER_URL
Expand Down Expand Up @@ -192,6 +200,10 @@ class ExperimentConfig internal constructor(
this.fallbackVariant = fallbackVariant
}

fun initialFlags(initialFlags: String?) = apply {
this.initialFlags = initialFlags
}

fun initialVariants(initialVariants: Map<String, Variant>) = apply {
this.initialVariants = initialVariants
}
Expand Down Expand Up @@ -254,6 +266,7 @@ class ExperimentConfig internal constructor(
debug = debug,
instanceName = instanceName,
fallbackVariant = fallbackVariant,
initialFlags = initialFlags,
initialVariants = initialVariants,
source = source,
serverUrl = serverUrl,
Expand All @@ -277,6 +290,7 @@ class ExperimentConfig internal constructor(
.debug(debug)
.instanceName(instanceName)
.fallbackVariant(fallbackVariant)
.initialFlags(initialFlags)
.initialVariants(initialVariants)
.source(source)
.serverUrl(serverUrl)
Expand Down
48 changes: 48 additions & 0 deletions sdk/src/test/java/com/amplitude/experiment/ExperimentClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1152,4 +1152,52 @@ class ExperimentClientTest {
Assert.assertEquals(array::class, variant.payload!!::class)
Assert.assertEquals(array.toString(), variant.payload.toString())
}

@Test
fun `initial flags`() {
val storage = MockStorage()
// Flag, sdk-ci-test-local is modified to always return off
val initialFlags = """
[
{"key":"sdk-ci-test-local","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"off"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}},
{"key":"sdk-ci-test-local-2","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"on"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}}
]
""".trimIndent()
val client = DefaultExperimentClient(
API_KEY,
ExperimentConfig(
initialFlags = initialFlags,
),
OkHttpClient(),
storage,
Experiment.executorService,
)
val user = ExperimentUser(userId = "user_id", deviceId = "device_id")
client.setUser(user)
var variant = client.variant("sdk-ci-test-local")
Assert.assertEquals("off", variant.key)
var variant2 = client.variant("sdk-ci-test-local-2")
Assert.assertEquals("on", variant2.key)
// Call start to update the flag, overwrites the initial flag to return on
client.start(user).get()
variant = client.variant("sdk-ci-test-local")
Assert.assertEquals("on", variant.key)
variant2 = client.variant("sdk-ci-test-local-2")
Assert.assertEquals("on", variant2.key)
// Initialize a second client with the same storage to simulate an app restart
val client2 = DefaultExperimentClient(
API_KEY,
ExperimentConfig(
initialFlags = initialFlags,
),
OkHttpClient(),
storage,
Experiment.executorService,
)
// Storage flag should take precedent over initial flag
variant = client.variant("sdk-ci-test-local")
Assert.assertEquals("on", variant.key)
variant2 = client.variant("sdk-ci-test-local-2")
Assert.assertEquals("on", variant2.key)
}
}
Loading