From b3c3e69fd57ea88e96f7edaea88760d94904353c Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Thu, 1 Oct 2026 18:28:35 +0200 Subject: [PATCH 1/3] feat: upgrade OpenFeature SDK to 0.9.0 --- Provider/api/Provider.api | 1 + .../openfeature/ConfidenceFeatureProvider.kt | 23 +++++++++++++-- .../openfeature/OpenFeatureTrackMapper.kt | 9 ++++-- .../ConfidenceFeatureProviderTrackTest.kt | 28 +++++++++++++++++++ .../openfeature/ValueMappingsTest.kt | 11 ++++++++ gradle/libs.versions.toml | 2 +- 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/Provider/api/Provider.api b/Provider/api/Provider.api index 5576cb34..aa0e92a9 100644 --- a/Provider/api/Provider.api +++ b/Provider/api/Provider.api @@ -5,6 +5,7 @@ public final class com/spotify/confidence/openfeature/ConfidenceFeatureProvider public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation; public fun getHooks ()Ljava/util/List; public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation; + public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation; public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata; public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation; public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation; diff --git a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt index 66eb15c3..f3c81ea2 100644 --- a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt +++ b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt @@ -126,6 +126,25 @@ class ConfidenceFeatureProvider private constructor( return generateEvaluation(key, defaultValue) } + override fun getLongEvaluation( + key: String, + defaultValue: Long, + context: EvaluationContext? + ): ProviderEvaluation { + val evaluation = generateEvaluation(key, defaultValue) + return ProviderEvaluation( + value = when (val value = evaluation.value) { + is Int -> value.toLong() + is Long -> value + else -> throw OpenFeatureError.ParseError("Flag value is not an integer") + }, + reason = evaluation.reason, + variant = evaluation.variant, + errorCode = evaluation.errorCode, + errorMessage = evaluation.errorMessage + ) + } + override fun getObjectEvaluation( key: String, defaultValue: Value, @@ -207,6 +226,7 @@ internal fun Value.toConfidenceValue(): ConfidenceValue = when (this) { is Value.Boolean -> ConfidenceValue.Boolean(this.boolean) is Value.Double -> ConfidenceValue.Double(this.double) is Value.Integer -> ConfidenceValue.Integer(this.integer) + is Value.Long -> this.long.toConfidenceValue() is Value.List -> { // if types are different, return an empty list if (this.list.map { it.javaClass.simpleName }.groupBy { it }.size > 1) { @@ -271,8 +291,7 @@ private fun OpenFeatureError.toProviderErrorEvent(): OpenFeatureProviderEvents.P eventDetails = OpenFeatureProviderEvents.EventDetails( message = message, errorCode = errorCode() - ), - error = this + ) ) } diff --git a/Provider/src/main/java/com/spotify/confidence/openfeature/OpenFeatureTrackMapper.kt b/Provider/src/main/java/com/spotify/confidence/openfeature/OpenFeatureTrackMapper.kt index a7832e68..1f093291 100644 --- a/Provider/src/main/java/com/spotify/confidence/openfeature/OpenFeatureTrackMapper.kt +++ b/Provider/src/main/java/com/spotify/confidence/openfeature/OpenFeatureTrackMapper.kt @@ -3,6 +3,7 @@ package com.spotify.confidence.openfeature import com.spotify.confidence.ConfidenceValue import dev.openfeature.kotlin.sdk.EvaluationContext import dev.openfeature.kotlin.sdk.TrackingEventDetails +import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError internal fun mergeEventContext( sessionContext: Map, @@ -37,12 +38,16 @@ internal fun TrackingEventDetails?.toTrackingData(): Map ConfidenceValue.Integer(this) is Long -> if (this in Int.MIN_VALUE..Int.MAX_VALUE) { ConfidenceValue.Integer(toInt()) - } else { + } else if (this in -MAX_EXACT_DOUBLE_INTEGER..MAX_EXACT_DOUBLE_INTEGER) { ConfidenceValue.Double(toDouble()) + } else { + throw OpenFeatureError.ParseError("Long value cannot be represented exactly by Confidence") } is Double -> ConfidenceValue.Double(this) is Float -> ConfidenceValue.Double(toDouble()) diff --git a/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderTrackTest.kt b/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderTrackTest.kt index 94c68e03..e3c422ab 100644 --- a/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderTrackTest.kt +++ b/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderTrackTest.kt @@ -2,12 +2,15 @@ package com.spotify.confidence.openfeature import com.spotify.confidence.Confidence import com.spotify.confidence.ConfidenceValue +import com.spotify.confidence.Evaluation +import com.spotify.confidence.ResolveReason import dev.openfeature.kotlin.sdk.ImmutableContext import dev.openfeature.kotlin.sdk.ImmutableStructure import dev.openfeature.kotlin.sdk.TrackingEventDetails import dev.openfeature.kotlin.sdk.Value import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents import dev.openfeature.kotlin.sdk.exceptions.ErrorCode +import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -23,6 +26,31 @@ import org.junit.Test import com.spotify.confidence.Result as ConfidenceResult class ConfidenceFeatureProviderTrackTest { + @Test + fun longEvaluationWidensIntegerFlagValueAndPreservesLongDefault() { + val confidence = mockk(relaxed = true) + every { confidence.getFlag("size", any()) } returns + Evaluation(42, reason = ResolveReason.RESOLVE_REASON_MATCH) + every { confidence.getFlag("missing", any()) } answers + { Evaluation(secondArg(), reason = ResolveReason.ERROR) } + val provider = ConfidenceFeatureProvider.create(confidence) + + assertEquals(42L, provider.getLongEvaluation("size", 0L, null).value) + assertEquals(Long.MAX_VALUE, provider.getLongEvaluation("missing", Long.MAX_VALUE, null).value) + } + + @Test + fun longEvaluationRejectsDoubleFlagValue() { + val confidence = mockk(relaxed = true) + every { confidence.getFlag("size", any()) } returns + Evaluation(42.0, reason = ResolveReason.RESOLVE_REASON_MATCH) + val provider = ConfidenceFeatureProvider.create(confidence) + + org.junit.Assert.assertThrows(OpenFeatureError.ParseError::class.java) { + provider.getLongEvaluation("size", 0L, null) + } + } + @Test fun shutdownStopsConfidence() { val confidence = mockk(relaxed = true) diff --git a/Provider/src/test/java/com/spotify/confidence/openfeature/ValueMappingsTest.kt b/Provider/src/test/java/com/spotify/confidence/openfeature/ValueMappingsTest.kt index f54ad184..1756d187 100644 --- a/Provider/src/test/java/com/spotify/confidence/openfeature/ValueMappingsTest.kt +++ b/Provider/src/test/java/com/spotify/confidence/openfeature/ValueMappingsTest.kt @@ -2,7 +2,9 @@ package com.spotify.confidence.openfeature import com.spotify.confidence.ConfidenceValue import dev.openfeature.kotlin.sdk.Value +import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Test class ValueMappingsTest { @@ -35,6 +37,15 @@ class ValueMappingsTest { assertEquals(42, confidenceValue.asInteger()?.integer) } + @Test + fun openFeatureLongValueToConfidenceValue() { + assertEquals(ConfidenceValue.Integer(42), Value.Long(42).toConfidenceValue()) + assertEquals(ConfidenceValue.Double(3_000_000_000.0), Value.Long(3_000_000_000).toConfidenceValue()) + assertThrows(OpenFeatureError.ParseError::class.java) { + Value.Long(9_007_199_254_740_993L).toConfidenceValue() + } + } + @Test fun openFeatureStructureValueToConfidenceValueStruct() { val value = Value.Structure(mapOf("key" to Value.String("value"))) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fadbea76..7fccc635 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ okHttp = "4.12.0" kotlinxSerialization = "1.6.0" # Provider -openFeatureSDK = "0.8.0" +openFeatureSDK = "0.9.0" # Sample app activityCompose = "1.3.1" From 00584b4e372d8470c47a373e969884eb4d9621f6 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Fri, 2 Oct 2026 07:43:29 +0200 Subject: [PATCH 2/3] fix: handle long values in demo app --- .../src/main/java/com/example/confidencedemoapp/MainVm.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt index 1ac6befb..d4241155 100644 --- a/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt +++ b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt @@ -158,6 +158,7 @@ private fun Value.friendlyString(): String { return when (this) { is Value.String -> this.string is Value.Integer -> this.integer.toString() + is Value.Long -> this.long.toString() is Value.Double -> this.double.toString() is Value.Boolean -> this.boolean.toString() is Value.Instant -> this.instant.toString() From 29d5378281de4a4400a8f408d9be0001239b66f3 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Thu, 8 Oct 2026 09:59:52 +0200 Subject: [PATCH 3/3] fix: preserve object evaluation defaults --- .../openfeature/ConfidenceFeatureProvider.kt | 8 ++- .../ConfidenceFeatureProviderObjectTest.kt | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderObjectTest.kt diff --git a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt index f3c81ea2..7a334095 100644 --- a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt +++ b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt @@ -150,9 +150,13 @@ class ConfidenceFeatureProvider private constructor( defaultValue: Value, context: EvaluationContext? ): ProviderEvaluation { - val evaluation = generateEvaluation(key, defaultValue.toConfidenceValue()) + val evaluation = generateEvaluation(key, defaultValue) return ProviderEvaluation( - value = evaluation.value.toValue(), + value = when (val value = evaluation.value) { + is Value -> value + is ConfidenceValue -> value.toValue() + else -> throw OpenFeatureError.ParseError("Flag value is not an object") + }, reason = evaluation.reason, variant = evaluation.variant, errorCode = evaluation.errorCode, diff --git a/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderObjectTest.kt b/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderObjectTest.kt new file mode 100644 index 00000000..17fb3abd --- /dev/null +++ b/Provider/src/test/java/com/spotify/confidence/openfeature/ConfidenceFeatureProviderObjectTest.kt @@ -0,0 +1,69 @@ +package com.spotify.confidence.openfeature + +import com.spotify.confidence.Confidence +import com.spotify.confidence.ConfidenceValue +import com.spotify.confidence.FlagResolution +import com.spotify.confidence.ResolveReason +import com.spotify.confidence.client.ResolvedFlag +import com.spotify.confidence.getEvaluation +import dev.openfeature.kotlin.sdk.Reason +import dev.openfeature.kotlin.sdk.Value +import dev.openfeature.kotlin.sdk.exceptions.ErrorCode +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Test + +class ConfidenceFeatureProviderObjectTest { + @Test + fun missingFlagPreservesLongDefaults() { + val provider = createProvider(FlagResolution.EMPTY) + + for (count in listOf(42L, Long.MAX_VALUE)) { + val default = Value.Structure(mapOf("count" to Value.Long(count))) + + val evaluation = provider.getObjectEvaluation("missing", default, null) + + assertSame(default, evaluation.value) + assertEquals(Reason.ERROR.name, evaluation.reason) + assertEquals(ErrorCode.FLAG_NOT_FOUND, evaluation.errorCode) + } + } + + @Test + fun matchingFlagResolvesWithLongMaxValueInUnusedDefault() { + val provider = createProvider( + FlagResolution( + context = emptyMap(), + flags = listOf( + ResolvedFlag( + flag = "size", + variant = "variant-1", + value = mapOf("count" to ConfidenceValue.Integer(42)), + reason = ResolveReason.RESOLVE_REASON_MATCH, + shouldApply = false + ) + ), + resolveToken = "token" + ) + ) + val default = Value.Structure(mapOf("count" to Value.Long(Long.MAX_VALUE))) + + val evaluation = provider.getObjectEvaluation("size", default, null) + + assertEquals(Value.Structure(mapOf("count" to Value.Integer(42))), evaluation.value) + assertEquals(Reason.TARGETING_MATCH.name, evaluation.reason) + assertEquals("variant-1", evaluation.variant) + assertNull(evaluation.errorCode) + } + + private fun createProvider(resolution: FlagResolution): ConfidenceFeatureProvider { + val confidence = mockk(relaxed = true) + every { confidence.getFlag(any(), any()) } answers { + resolution.getEvaluation(firstArg(), secondArg(), emptyMap()) + } + return ConfidenceFeatureProvider.create(confidence) + } +}