diff --git a/Confidence/src/main/java/com/spotify/confidence/Confidence.kt b/Confidence/src/main/java/com/spotify/confidence/Confidence.kt index edee2ef5..8bcf5e0a 100644 --- a/Confidence/src/main/java/com/spotify/confidence/Confidence.kt +++ b/Confidence/src/main/java/com/spotify/confidence/Confidence.kt @@ -115,10 +115,12 @@ class Confidence internal constructor( key, default, evaluationContext - ) { flagName, resolveToken -> + ) { flagName, resolveToken, shouldApply -> // this lambda will be invoked inside the evaluation process // and only if the resolve reason is not targeting key error. - apply(flagName, resolveToken) + if (shouldApply) { + apply(flagName, resolveToken) + } } // we are using a custom serializer so that the Json is serialized correctly in the logs val newMap: Map = diff --git a/Confidence/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt b/Confidence/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt index 114d3e63..a7441db0 100644 --- a/Confidence/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt +++ b/Confidence/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt @@ -6,7 +6,7 @@ fun FlagResolution?.getEvaluation( flag: String, defaultValue: T, context: Map, - applyFlag: (String, String) -> Unit = { _, _ -> } + applyFlag: (String, String, Boolean) -> Unit = { _, _, _ -> } ): Evaluation { val parsedKey = FlagKey(flag) if (this == null) { @@ -25,7 +25,7 @@ fun FlagResolution?.getEvaluation( ) if (resolvedFlag.reason != ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR) { - applyFlag(parsedKey.flagName, resolveToken) + applyFlag(parsedKey.flagName, resolveToken, resolvedFlag.shouldApply) } else { return Evaluation( value = defaultValue, diff --git a/Confidence/src/main/java/com/spotify/confidence/client/Types.kt b/Confidence/src/main/java/com/spotify/confidence/client/Types.kt index 501e12b4..bb80878b 100644 --- a/Confidence/src/main/java/com/spotify/confidence/client/Types.kt +++ b/Confidence/src/main/java/com/spotify/confidence/client/Types.kt @@ -37,7 +37,8 @@ data class ResolvedFlag( val flag: String, val variant: String, val value: ConfidenceValueMap = mapOf(), - val reason: ResolveReason + val reason: ResolveReason, + val shouldApply: Boolean ) sealed interface SchemaType { diff --git a/Confidence/src/main/java/com/spotify/confidence/serializers/Serializers.kt b/Confidence/src/main/java/com/spotify/confidence/serializers/Serializers.kt index f88d3a2a..c23137c2 100644 --- a/Confidence/src/main/java/com/spotify/confidence/serializers/Serializers.kt +++ b/Confidence/src/main/java/com/spotify/confidence/serializers/Serializers.kt @@ -86,6 +86,7 @@ internal object NetworkResolvedFlagSerializer : KSerializer { .replace("\"", "") val resolvedReason = Json.decodeFromString(json["reason"].toString()) + val shouldApply = Json.decodeFromString(json["shouldApply"].toString()) val flagSchemaJsonElement = json["flagSchema"] val schemasJson = if (flagSchemaJsonElement != null && flagSchemaJsonElement != JsonNull) { @@ -112,14 +113,16 @@ internal object NetworkResolvedFlagSerializer : KSerializer { flag = flag, variant = variant, reason = resolvedReason, - value = values.map + value = values.map, + shouldApply = shouldApply ) } else { ResolvedFlag( flag = flag, variant = variant, reason = resolvedReason, - value = mutableMapOf() + value = mutableMapOf(), + shouldApply = shouldApply ) } } diff --git a/Confidence/src/test/java/com/spotify/confidence/ConfidenceEvaluationTest.kt b/Confidence/src/test/java/com/spotify/confidence/ConfidenceEvaluationTest.kt index c8dd1aae..972230d8 100644 --- a/Confidence/src/test/java/com/spotify/confidence/ConfidenceEvaluationTest.kt +++ b/Confidence/src/test/java/com/spotify/confidence/ConfidenceEvaluationTest.kt @@ -59,7 +59,19 @@ internal class ConfidenceEvaluationTest { "test-kotlin-flag-1", "flags/test-kotlin-flag-1/variants/variant-1", resolvedValueAsMap, - ResolveReason.RESOLVE_REASON_MATCH + ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = true + ) + ) + ) + private val resolvedFlagsNoApply = Flags( + listOf( + ResolvedFlag( + "test-kotlin-flag-2", + "flags/test-kotlin-flag-2/variants/variant-1", + resolvedValueAsMap, + ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = false ) ) ) @@ -919,7 +931,8 @@ internal class ConfidenceEvaluationTest { "test-kotlin-flag-1", "", mapOf(), - ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR + ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR, + shouldApply = true ) ) ) @@ -965,7 +978,8 @@ internal class ConfidenceEvaluationTest { flag = "test-kotlin-flag-1", variant = "", mapOf(), - reason + reason, + true ) ) ) @@ -1097,6 +1111,39 @@ internal class ConfidenceEvaluationTest { ) TestCase.assertEquals(ErrorCode.PARSE_ERROR, ex.errorCode) } + + @Test + fun testShouldApplyFalse() = runTest { + val testDispatcher = UnconfinedTestDispatcher(testScheduler) + val context = mapOf("targeting_key" to ConfidenceValue.String("foo")) + val flagResolver: FlagResolver = mock() + val mockConfidence = getConfidence( + testDispatcher, + initialContext = context, + flagResolver = flagResolver + ) + whenever( + flagResolver.resolve( + any(), + eq(context) + ) + ).thenReturn( + Result.Success( + FlagResolution( + context, + resolvedFlagsNoApply.list, + "token1" + ) + ) + ) + mockConfidence.fetchAndActivate() + advanceUntilIdle() + val evalString = mockConfidence.getFlag("test-kotlin-flag-2.mystring", "default") + + // Resolve is correct, but no Apply sent + TestCase.assertEquals("red", evalString.value) + verify(flagApplierClient, times(0)).apply(any(), any()) + } } private const val cacheFileData = "{\n" + diff --git a/Confidence/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt b/Confidence/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt index e67c4a58..f9e5ec31 100644 --- a/Confidence/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt +++ b/Confidence/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt @@ -54,7 +54,8 @@ class ConfidenceIntegrationTests { "my-integer" to ConfidenceValue.Integer( storedValue ) - ) + ), + shouldApply = true ) ) diff --git a/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt b/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt index 503598c2..42f3e5d8 100644 --- a/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt +++ b/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt @@ -94,7 +94,8 @@ internal class ConfidenceRemoteClientTests { " }\n" + " }\n" + " },\n" + - " \"reason\": \"RESOLVE_REASON_MATCH\"\n" + + " \"reason\": \"RESOLVE_REASON_MATCH\",\n" + + " \"shouldApply\": true\n" + " }\n" + " ],\n" + " \"resolveToken\": \"token1\"\n" + @@ -132,7 +133,8 @@ internal class ConfidenceRemoteClientTests { ) ) ), - ResolveReason.RESOLVE_REASON_MATCH + ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = true ) ) ) @@ -144,7 +146,7 @@ internal class ConfidenceRemoteClientTests { } @Test - fun testDeserializeResolveResponseNoMatch() = runTest { + fun testDeserializeResolveResponseNoApply() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) val jsonPayload = "{\n" + " \"resolvedFlags\": [\n" + @@ -153,7 +155,8 @@ internal class ConfidenceRemoteClientTests { " \"variant\": \"\",\n" + " \"value\": null,\n" + " \"flagSchema\": null,\n" + - " \"reason\": \"RESOLVE_REASON_NO_SEGMENT_MATCH\"\n" + + " \"reason\": \"RESOLVE_REASON_NO_SEGMENT_MATCH\",\n" + + " \"shouldApply\": false\n" + " }\n" + " ],\n" + " \"resolveToken\": \"token1\"\n" + @@ -179,7 +182,8 @@ internal class ConfidenceRemoteClientTests { ResolvedFlag( flag = "test-kotlin-flag-1", variant = "", - reason = com.spotify.confidence.ResolveReason.RESOLVE_REASON_NO_SEGMENT_MATCH + reason = com.spotify.confidence.ResolveReason.RESOLVE_REASON_NO_SEGMENT_MATCH, + shouldApply = false ) ) ), @@ -207,6 +211,7 @@ internal class ConfidenceRemoteClientTests { " }\n" + " }\n" + " },\n" + + " \"shouldApply\": true,\n" + " \"reason\": \"RESOLVE_REASON_MATCH\"\n" + " }\n" + " ],\n" + @@ -237,7 +242,8 @@ internal class ConfidenceRemoteClientTests { mutableMapOf( "mydouble" to ConfidenceValue.Double(3.0) ), - com.spotify.confidence.ResolveReason.RESOLVE_REASON_MATCH + com.spotify.confidence.ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = true ) ) ), @@ -263,7 +269,8 @@ internal class ConfidenceRemoteClientTests { " }\n" + " }\n" + " },\n" + - " \"reason\": \"RESOLVE_REASON_MATCH\"\n" + + " \"reason\": \"RESOLVE_REASON_MATCH\",\n" + + " \"shouldApply\": true\n" + " }\n" + " ],\n" + " \"resolveToken\": \"token1\"\n" + @@ -308,7 +315,8 @@ internal class ConfidenceRemoteClientTests { " }\n" + " }\n" + " },\n" + - " \"reason\": \"RESOLVE_REASON_MATCH\"\n" + + " \"reason\": \"RESOLVE_REASON_MATCH\",\n" + + " \"shouldApply\": true\n" + " }\n" + " ],\n" + " \"resolveToken\": \"token1\"\n" + @@ -393,7 +401,8 @@ internal class ConfidenceRemoteClientTests { " }\n" + " }\n" + " },\n" + - " \"reason\": \"RESOLVE_REASON_MATCH\"\n" + + " \"reason\": \"RESOLVE_REASON_MATCH\",\n" + + " \"shouldApply\": true\n" + " }\n" + " ],\n" + " \"resolveToken\": \"token1\"\n" + diff --git a/Confidence/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt b/Confidence/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt index d9759e84..4f61b0a2 100644 --- a/Confidence/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt +++ b/Confidence/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt @@ -40,7 +40,8 @@ class StorageFileCacheTests { ), "mynull" to ConfidenceValue.Null ), - ResolveReason.RESOLVE_REASON_MATCH + ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = true ) ) )