From 23fa3141f2d5251c9a07200d1463f2a904d12e81 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 22 Oct 2026 15:53:42 +0000 Subject: [PATCH 01/44] FFL-3459: Support flagSelection on Flags precompute requests Add FlagsConfiguration.Builder.flagSelection(tags, keyPatterns). The selection is canonicalized and validated like Compute and ffe-service, sent as flag_selection on the precompute POST, and stored with the persisted state so a cache fetched with another selection is ignored. Report DD-Flag-Selection-Ignored, and treat a valid empty configuration as loaded so a later failed fetch reports Stale. Ref: FFL-3459 --- detekt_custom_safe_calls_third_party.yml | 2 + features/dd-sdk-android-flags/api/apiSurface | 1 + .../api/dd-sdk-android-flags.api | 1 + .../com/datadog/android/flags/FlagsClient.kt | 1 + .../android/flags/FlagsConfiguration.kt | 35 +++- .../android/flags/internal/FlagsFeature.kt | 15 +- .../internal/evaluation/EvaluationsManager.kt | 9 +- .../flags/internal/model/FlagsStateEntry.kt | 4 +- .../model/PrecomputedFlagConstants.kt | 3 + .../net/PrecomputedAssignmentsDownloader.kt | 20 +- .../net/PrecomputedAssignmentsReader.kt | 14 +- .../PrecomputedAssignmentsRequestFactory.kt | 12 +- .../persistence/FlagsPersistenceManager.kt | 5 +- .../persistence/FlagsStateDeserializer.kt | 16 +- .../persistence/FlagsStateSerializer.kt | 10 + .../repository/DefaultFlagsRepository.kt | 15 +- .../internal/repository/FlagsRepository.kt | 4 + .../flags/internal/selection/FlagSelection.kt | 121 +++++++++++ .../selection/FlagSelectionLogging.kt | 46 ++++ .../android/flags/FlagsConfigurationTest.kt | 48 +++++ .../flags/internal/FlagsFeatureTest.kt | 46 ++++ .../evaluation/EvaluationsManagerTest.kt | 128 ++++++++++-- .../PrecomputedAssignmentsDownloaderTest.kt | 28 ++- ...recomputedAssignmentsRequestFactoryTest.kt | 78 ++++++- .../persistence/FlagsStateDeserializerTest.kt | 28 +++ .../persistence/FlagsStateSerializerTest.kt | 49 ++++- .../repository/DefaultFlagsRepositoryTest.kt | 133 +++++++++++- .../repository/FirstFlagsInstallationTest.kt | 6 +- .../FlagSelectionCanonicalizerTest.kt | 197 ++++++++++++++++++ 29 files changed, 1013 insertions(+), 62 deletions(-) create mode 100644 features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt create mode 100644 features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt create mode 100644 features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index 7fc6a3c29d..c1e68bdce8 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -865,6 +865,8 @@ datadog: - "kotlin.ByteArray.constructor(kotlin.Int)" - "kotlin.ByteArray.copyTo(kotlin.Int, kotlin.ByteArray, kotlin.Int, kotlin.Int, com.datadog.android.api.InternalLogger)" - "kotlin.Char.digitToIntOrNull()" + - "kotlin.Char.isHighSurrogate()" + - "kotlin.Char.isLowSurrogate()" - "kotlin.Char.isLowerCase()" - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" diff --git a/features/dd-sdk-android-flags/api/apiSurface b/features/dd-sdk-android-flags/api/apiSurface index 8b95feb419..d2aeef4c48 100644 --- a/features/dd-sdk-android-flags/api/apiSurface +++ b/features/dd-sdk-android-flags/api/apiSurface @@ -33,6 +33,7 @@ data class com.datadog.android.flags.FlagsConfiguration fun evaluationFlushInterval(Long): Builder fun useCustomFlagEndpoint(String): Builder fun initializationTimeout(Long): Builder + fun flagSelection(List, List): Builder fun rumIntegrationEnabled(Boolean): Builder fun gracefulModeEnabled(Boolean): Builder fun build(): FlagsConfiguration diff --git a/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api b/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api index 0c62e195f7..ea6752b8f9 100644 --- a/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api +++ b/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api @@ -66,6 +66,7 @@ public final class com/datadog/android/flags/FlagsConfiguration$Builder { public fun ()V public final fun build ()Lcom/datadog/android/flags/FlagsConfiguration; public final fun evaluationFlushInterval (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder; + public final fun flagSelection (Ljava/util/List;Ljava/util/List;)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun gracefulModeEnabled (Z)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun initializationTimeout (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun rumIntegrationEnabled (Z)Lcom/datadog/android/flags/FlagsConfiguration$Builder; diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt index 76ab698407..da0f70997d 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt @@ -442,6 +442,7 @@ interface FlagsClient { featureSdkCore = featureSdkCore, dataStore = datastore, instanceName = name, + flagSelection = flagsFeature.flagSelection, deliverFirstFlags = { delivery -> val worker = featureSdkCore.createSingleThreadExecutorService(FLAGS_FIRST_FLAGS_EXECUTOR_NAME) try { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 88d2b2405a..f8bf636bae 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -6,6 +6,8 @@ package com.datadog.android.flags +import com.datadog.android.flags.internal.selection.FlagSelection + private const val DEFAULT_INITIALIZATION_TIMEOUT_MS = 5_000L /** @@ -21,10 +23,11 @@ data class FlagsConfiguration internal constructor( internal val evaluationFlushIntervalMs: Long, internal val rumIntegrationEnabled: Boolean, internal val gracefulModeEnabled: Boolean, - internal val initializationTimeoutMs: Long? + internal val initializationTimeoutMs: Long?, + internal val flagSelection: FlagSelection? ) { /** - * Copies this configuration and preserves the initialization timeout. + * Copies this configuration and preserves the initialization timeout and the flag selection. * * This overload keeps the public JVM signature that existed before the initialization timeout was added. */ @@ -46,7 +49,8 @@ data class FlagsConfiguration internal constructor( evaluationFlushIntervalMs = evaluationFlushIntervalMs, rumIntegrationEnabled = rumIntegrationEnabled, gracefulModeEnabled = gracefulModeEnabled, - initializationTimeoutMs = initializationTimeoutMs + initializationTimeoutMs = initializationTimeoutMs, + flagSelection = flagSelection ) /** @@ -62,6 +66,7 @@ data class FlagsConfiguration internal constructor( private var rumIntegrationEnabled: Boolean = true private var gracefulModeEnabled: Boolean = true private var initializationTimeoutMs: Long? = DEFAULT_INITIALIZATION_TIMEOUT_MS + private var flagSelection: FlagSelection? = null /** * Sets whether exposures should be logged to the dedicated exposures intake endpoint. @@ -170,6 +175,27 @@ data class FlagsConfiguration internal constructor( initializationTimeoutMs = timeoutMs.takeIf { it > 0 } } + /** + * Loads only the flags that match the given tags or key patterns. + * + * Tags match exactly and are case-sensitive; a flag matches if it has any of the tags. Key patterns are + * whole-key globs where only `*` is special. Leading and trailing Unicode whitespace is trimmed from every + * value, empty values are dropped, and duplicates are removed. Each list accepts at most 31 values, and each + * tag and key pattern is at most 200 UTF-8 bytes. A selection over a limit is not sent, and the full flag + * configuration is loaded. Flags outside the selection are not loaded, so evaluating them returns the + * default value. + * + * The selection is fixed when the Flags feature is enabled. Calling this method again replaces the + * previous selection. + * + * @param tags The flag tags to load. + * @param keyPatterns The flag key patterns to load. + * @return this [Builder] instance for method chaining. + */ + fun flagSelection(tags: List, keyPatterns: List): Builder = apply { + flagSelection = FlagSelection(tags.toList(), keyPatterns.toList()) + } + /** * Sets whether RUM evaluation logging is enabled. * This adds the result of evaluating a feature flag to the view. @@ -215,7 +241,8 @@ data class FlagsConfiguration internal constructor( evaluationFlushIntervalMs = evaluationFlushIntervalMs, rumIntegrationEnabled = rumIntegrationEnabled, gracefulModeEnabled = gracefulModeEnabled, - initializationTimeoutMs = initializationTimeoutMs + initializationTimeoutMs = initializationTimeoutMs, + flagSelection = flagSelection ) internal companion object { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt index 94ec13af65..a1637449ba 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt @@ -20,6 +20,10 @@ import com.datadog.android.flags.FlagsConfiguration import com.datadog.android.flags.internal.evaluation.InitializationTimeoutScheduler import com.datadog.android.flags.internal.net.ExposuresRequestFactory import com.datadog.android.flags.internal.net.PrecomputedAssignmentsRequestFactory +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionCanonicalizer +import com.datadog.android.flags.internal.selection.FlagSelectionResult +import com.datadog.android.flags.internal.selection.logDroppedFlagSelection import com.datadog.android.flags.internal.storage.ExposureEventRecordWriter import com.datadog.android.flags.internal.storage.NoOpRecordWriter import com.datadog.android.flags.internal.storage.RecordWriter @@ -75,10 +79,16 @@ internal class FlagsFeature( // region Domain Objects + private val flagSelectionResult = FlagSelectionCanonicalizer.canonicalize(flagsConfiguration.flagSelection) + + internal val flagSelection: CanonicalFlagSelection? = + (flagSelectionResult as? FlagSelectionResult.Selected)?.selection + internal val precomputedRequestFactory = PrecomputedAssignmentsRequestFactory( internalLogger = sdkCore.internalLogger, - customFlagEndpoint = flagsConfiguration.customFlagEndpoint + customFlagEndpoint = flagsConfiguration.customFlagEndpoint, + flagSelection = flagSelection ) internal val initializationTimeoutScheduler = InitializationTimeoutScheduler { timeoutMs, action -> @@ -122,6 +132,9 @@ internal class FlagsFeature( } isDebugBuild = (appContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 isInitialized = true + (flagSelectionResult as? FlagSelectionResult.Dropped)?.let { + sdkCore.internalLogger.logDroppedFlagSelection(it.error) + } dataWriter = createDataWriter() processor = ExposureEventsProcessor( writer = dataWriter, diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 05ff5c0b20..5378742a27 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -17,8 +17,10 @@ import com.datadog.android.flags.internal.FlagsStateManager import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.net.NetworkRequestFailedException import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader +import com.datadog.android.flags.internal.net.PrecomputedAssignmentsResponse import com.datadog.android.flags.internal.repository.FlagsRepository import com.datadog.android.flags.internal.repository.net.PrecomputeMapper +import com.datadog.android.flags.internal.selection.logIgnoredFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.FlagsClientState import java.util.concurrent.Executor @@ -146,7 +148,7 @@ internal class EvaluationsManager( hadFlags && flagsRepository.getEvaluationContext() == context ) val response = datadogContext?.let { assignmentsReader.readPrecomputedFlags(context, it) } - val flagsMap = response?.let { precomputeMapper.map(it) } + val flagsMap = response?.let { mapResponse(it) } if (flagsMap != null) { installFlags(context, flagsMap, initializationCompletion, callback) } else { @@ -198,6 +200,11 @@ internal class EvaluationsManager( } } + private fun mapResponse(response: PrecomputedAssignmentsResponse): Map? { + response.flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } + return precomputeMapper.map(response.body) + } + private fun installFlags( context: EvaluationContext, flagsMap: Map, diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt index d61d3f383f..0a40bfaed6 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt @@ -6,10 +6,12 @@ package com.datadog.android.flags.internal.model +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext internal data class FlagsStateEntry( val evaluationContext: EvaluationContext, val flags: Map, - val lastUpdateTimestamp: Long + val lastUpdateTimestamp: Long, + val flagSelection: CanonicalFlagSelection? ) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/PrecomputedFlagConstants.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/PrecomputedFlagConstants.kt index 4a013b0da1..68e6503861 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/PrecomputedFlagConstants.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/PrecomputedFlagConstants.kt @@ -15,6 +15,9 @@ internal enum class JsonKeys(val value: String) { // FlagsStateEntry keys FLAGS("flags"), LAST_UPDATE_TIMESTAMP("lastUpdateTimestamp"), + FLAG_SELECTION("flagSelection"), + TAGS("tags"), + KEY_PATTERNS("keyPatterns"), // PrecomputedFlag keys VARIATION_TYPE("variationType"), diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index 65311982c4..e68f0ef4f5 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -28,14 +28,17 @@ internal class PrecomputedAssignmentsDownloader( ) : PrecomputedAssignmentsReader { @WorkerThread - override fun readPrecomputedFlags(context: EvaluationContext, datadogContext: DatadogContext): String? { + override fun readPrecomputedFlags( + context: EvaluationContext, + datadogContext: DatadogContext + ): PrecomputedAssignmentsResponse? { val request = requestFactory.create(context, datadogContext) ?: return null return executeDownloadRequest(request) } @Suppress("TooGenericExceptionCaught") - private fun executeDownloadRequest(request: Request): String? = try { + private fun executeDownloadRequest(request: Request): PrecomputedAssignmentsResponse? = try { val response = callFactory.newCall(request).execute() handleResponse(response) } catch (e: Throwable) { @@ -48,9 +51,14 @@ internal class PrecomputedAssignmentsDownloader( null } - private fun handleResponse(response: Response): String? = if (response.isSuccessful) { + private fun handleResponse(response: Response): PrecomputedAssignmentsResponse? = if (response.isSuccessful) { @Suppress("UnsafeThirdPartyFunctionCall") // Safe: wrapped in outer try-catch - response.body?.use { it.string() } + response.body?.use { it.string() }?.let { body -> + PrecomputedAssignmentsResponse( + body = body, + flagSelectionIgnoredReason = response.header(HEADER_FLAG_SELECTION_IGNORED) + ) + } } else { internalLogger.log( InternalLogger.Level.ERROR, @@ -70,4 +78,8 @@ internal class PrecomputedAssignmentsDownloader( null } + + private companion object { + const val HEADER_FLAG_SELECTION_IGNORED = "DD-Flag-Selection-Ignored" + } } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt index e254a9c08e..a17478a480 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt @@ -14,5 +14,17 @@ import com.datadog.tools.annotation.NoOpImplementation @NoOpImplementation internal interface PrecomputedAssignmentsReader { @WorkerThread - fun readPrecomputedFlags(context: EvaluationContext, datadogContext: DatadogContext): String? + fun readPrecomputedFlags( + context: EvaluationContext, + datadogContext: DatadogContext + ): PrecomputedAssignmentsResponse? } + +/** + * @param body the raw response body + * @param flagSelectionIgnoredReason the `DD-Flag-Selection-Ignored` header value, or null when absent + */ +internal data class PrecomputedAssignmentsResponse( + val body: String, + val flagSelectionIgnoredReason: String? +) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt index e7414d824c..1347263ba7 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt @@ -10,11 +10,13 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature import com.datadog.android.flags.internal.getFlagsEndpoint +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import okhttp3.Headers import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -23,7 +25,8 @@ import org.json.JSONObject */ internal class PrecomputedAssignmentsRequestFactory( private val internalLogger: InternalLogger, - private val customFlagEndpoint: String? + private val customFlagEndpoint: String?, + private val flagSelection: CanonicalFlagSelection? ) { /** @@ -94,6 +97,7 @@ internal class PrecomputedAssignmentsRequestFactory( .put("env", env) .put("source", source) .put("subject", subject) + flagSelection?.let { attributes.put("flag_selection", buildFlagSelectionPayload(it)) } val data = JSONObject() .put("type", "precompute-assignments-request") .put("attributes", attributes) @@ -134,6 +138,12 @@ internal class PrecomputedAssignmentsRequestFactory( .put("sdk_name", SDK_NAME) .put("sdk_version", datadogContext.sdkVersion) + @Suppress("UnsafeThirdPartyFunctionCall") // call wrapped in try/catch + private fun buildFlagSelectionPayload(selection: CanonicalFlagSelection): JSONObject = + JSONObject() + .put("tags", JSONArray(selection.tags)) + .put("key_patterns", JSONArray(selection.keyPatterns)) + private val DatadogContext.rumApplicationId: String? get() = featuresContext.get(Feature.RUM_FEATURE_NAME) ?.get("application_id") as? String diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt index 9563a6eb8d..c26c6f5310 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt @@ -13,6 +13,7 @@ import com.datadog.android.api.storage.datastore.DataStoreWriteCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext internal class FlagsPersistenceManager( @@ -32,13 +33,15 @@ internal class FlagsPersistenceManager( internal fun saveFlagsState( context: EvaluationContext, flags: Map, + flagSelection: CanonicalFlagSelection?, currentTimestamp: Long, callback: DataStoreWriteCallback? = null ) { val entry = FlagsStateEntry( evaluationContext = context, flags = flags, - lastUpdateTimestamp = currentTimestamp + lastUpdateTimestamp = currentTimestamp, + flagSelection = flagSelection ) dataStore.setValue( diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt index c0216edeec..1803d3a326 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt @@ -11,7 +11,9 @@ import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.JsonKeys import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -32,6 +34,8 @@ internal class FlagsStateDeserializer(private val internalLogger: InternalLogger @Suppress("UnsafeThirdPartyFunctionCall") // JSONObject operations wrapped in try-catch val timestamp = json.getLong(JsonKeys.LAST_UPDATE_TIMESTAMP.value) + val flagSelection = json.optJSONObject(JsonKeys.FLAG_SELECTION.value)?.let { deserializeFlagSelection(it) } + // A nonempty cache with no readable flags is not a valid empty configuration. if (flags.isEmpty() && flagsJson.length() > 0) { null @@ -39,7 +43,8 @@ internal class FlagsStateDeserializer(private val internalLogger: InternalLogger FlagsStateEntry( evaluationContext = evaluationContext, flags = flags, - lastUpdateTimestamp = timestamp + lastUpdateTimestamp = timestamp, + flagSelection = flagSelection ) } } catch (e: JSONException) { @@ -74,6 +79,15 @@ internal class FlagsStateDeserializer(private val internalLogger: InternalLogger return EvaluationContext(targetingKey, attributes) } + @Suppress("UnsafeThirdPartyFunctionCall") // JSONObject operations wrapped in try-catch + private fun deserializeFlagSelection(selectionJson: JSONObject): CanonicalFlagSelection = CanonicalFlagSelection( + tags = selectionJson.getJSONArray(JsonKeys.TAGS.value).toStringList(), + keyPatterns = selectionJson.getJSONArray(JsonKeys.KEY_PATTERNS.value).toStringList() + ) + + @Suppress("UnsafeThirdPartyFunctionCall") // JSONArray operations wrapped in try-catch + private fun JSONArray.toStringList(): List = List(length()) { getString(it) } + @Suppress("UnsafeThirdPartyFunctionCall") // JSONObject operations wrapped in try-catch private fun deserializeAttributes(attributesJson: JSONObject): Map { val attributes = mutableMapOf() diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt index e950b6bdda..7872830827 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt @@ -11,6 +11,8 @@ import com.datadog.android.core.persistence.Serializer import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.JsonKeys import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -37,6 +39,8 @@ internal class FlagsStateSerializer(private val internalLogger: InternalLogger) json.put(JsonKeys.LAST_UPDATE_TIMESTAMP.value, model.lastUpdateTimestamp) + model.flagSelection?.let { json.put(JsonKeys.FLAG_SELECTION.value, serializeFlagSelection(it)) } + json.toString() } catch (e: JSONException) { internalLogger.log( @@ -57,6 +61,12 @@ internal class FlagsStateSerializer(private val internalLogger: InternalLogger) return attributesJson } + @Suppress("UnsafeThirdPartyFunctionCall") // JSONObject operations wrapped in try-catch + private fun serializeFlagSelection(selection: CanonicalFlagSelection): JSONObject = JSONObject().apply { + put(JsonKeys.TAGS.value, JSONArray(selection.tags)) + put(JsonKeys.KEY_PATTERNS.value, JSONArray(selection.keyPatterns)) + } + @Suppress("UnsafeThirdPartyFunctionCall") // JSONObject operations wrapped in try-catch private fun serializePrecomputedFlag(flag: PrecomputedFlag): JSONObject = JSONObject().apply { put(JsonKeys.VARIATION_TYPE.value, flag.variationType) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt index 5687a6f274..6fb6f1d176 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt @@ -12,6 +12,7 @@ import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.api.storage.datastore.DataStoreWriteCallback import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.persistence.FlagsPersistenceManager +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import java.util.concurrent.CountDownLatch @@ -22,6 +23,7 @@ internal class DefaultFlagsRepository( private val featureSdkCore: FeatureSdkCore, private val instanceName: String, private val dataStore: DataStoreHandler, + private val flagSelection: CanonicalFlagSelection?, private val internalLogger: InternalLogger = featureSdkCore.internalLogger, private val persistenceLoadTimeoutMs: Long = PERSISTENCE_LOAD_TIMEOUT_MS, private val deliverFirstFlags: (Runnable) -> Unit = Runnable::run @@ -37,7 +39,9 @@ internal class DefaultFlagsRepository( dataStore = dataStore, instanceName = instanceName, internalLogger = internalLogger - ) { persistedState -> + ) { loadedState -> + // State fetched with a different selection holds a different set of flags. + val persistedState = loadedState?.takeIf { it.flagSelection == flagSelection } val installed = try { persistedState != null && atomicState.compareAndSet( null, @@ -68,6 +72,7 @@ internal class DefaultFlagsRepository( persistenceManager.saveFlagsState( context = context, flags = flags, + flagSelection = flagSelection, currentTimestamp = featureSdkCore.timeProvider.getDeviceTimestampMillis(), object : DataStoreWriteCallback { override fun onSuccess() { @@ -139,13 +144,11 @@ internal class DefaultFlagsRepository( override fun hasFlags(): Boolean { waitForPersistenceLoad() - return atomicState.get()?.flags?.isNotEmpty() ?: false + return atomicState.get() != null } - override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean { - val state = atomicState.get() - return state?.context == context && state.flags.isNotEmpty() - } + override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean = + atomicState.get()?.context == context override fun getPrecomputedFlagWithContext(key: String): Pair? { val state = atomicState.get() ?: return null diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt index 1bcab8e477..a5f2aee06f 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt @@ -29,7 +29,11 @@ internal interface FlagsRepository { // Nonblocking snapshot. Missing flags retain their context from the same atomic state read. fun getPrecomputedFlagWithContext(key: String): Pair? val firstFlags: FirstFlagsLatch + + /** True once a configuration is installed, including a valid configuration with zero flags. */ fun hasFlags(): Boolean + + /** True when the installed configuration is for [context], including a valid configuration with zero flags. */ fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean fun getFlagsSnapshot(): Map } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt new file mode 100644 index 0000000000..23fa30df49 --- /dev/null +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -0,0 +1,121 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +/** + * The flag selection as configured by the application, before canonicalization. + * Elements are nullable because Java callers can pass lists that hold nulls. + */ +internal data class FlagSelection( + val tags: List, + val keyPatterns: List +) + +/** + * A flag selection that was trimmed, deduplicated, sorted in UTF-8 byte order and validated. + */ +internal data class CanonicalFlagSelection( + val tags: List, + val keyPatterns: List +) + +/** + * Reasons a flag selection is dropped. Values match the reasons used by Compute and ffe-service. + */ +internal enum class FlagSelectionError(val reason: String, val limit: String) { + MALFORMED_SELECTION( + "malformed_selection", + "tags and key patterns must be non-null strings without unpaired UTF-16 surrogates" + ), + TOO_MANY_VALUES( + "too_many_values", + "each list accepts at most ${FlagSelectionCanonicalizer.MAX_VALUES} values" + ), + INVALID_KEY_PATTERN( + "invalid_key_pattern", + "each key pattern is at most ${FlagSelectionCanonicalizer.MAX_KEY_PATTERN_BYTES} UTF-8 bytes" + ), + INVALID_TAG( + "invalid_tag", + "each tag is at most ${FlagSelectionCanonicalizer.MAX_TAG_BYTES} UTF-8 bytes" + ) +} + +internal sealed interface FlagSelectionResult { + object None : FlagSelectionResult + data class Selected(val selection: CanonicalFlagSelection) : FlagSelectionResult + data class Dropped(val error: FlagSelectionError) : FlagSelectionResult +} + +/** + * Canonicalizes and validates a flag selection the same way Compute and ffe-service do. + */ +internal object FlagSelectionCanonicalizer { + + internal const val MAX_VALUES = 31 + internal const val MAX_TAG_BYTES = 200 + internal const val MAX_KEY_PATTERN_BYTES = 200 + + // The Unicode White_Space property, which Compute trims with Rust's str::trim. + // Kotlin's trim() differs: it also trims U+001C..U+001F and does not trim U+0085. + private const val WHITE_SPACE = "\u0009\u000A\u000B\u000C\u000D\u0020\u0085\u00A0\u1680" + + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A" + + "\u2028\u2029\u202F\u205F\u3000" + + @Suppress("ReturnCount") + fun canonicalize(selection: FlagSelection?): FlagSelectionResult { + if (selection == null) return FlagSelectionResult.None + val rawTags = selection.tags.wellFormedOrNull() + val rawKeyPatterns = selection.keyPatterns.wellFormedOrNull() + if (rawTags == null || rawKeyPatterns == null) { + return FlagSelectionResult.Dropped(FlagSelectionError.MALFORMED_SELECTION) + } + + val tags = canonicalizeList(rawTags) + val keyPatterns = canonicalizeList(rawKeyPatterns) + + val error = when { + tags.size > MAX_VALUES || keyPatterns.size > MAX_VALUES -> FlagSelectionError.TOO_MANY_VALUES + keyPatterns.any { it.utf8Size() > MAX_KEY_PATTERN_BYTES } -> FlagSelectionError.INVALID_KEY_PATTERN + tags.any { it.utf8Size() > MAX_TAG_BYTES } -> FlagSelectionError.INVALID_TAG + else -> null + } + return when { + error != null -> FlagSelectionResult.Dropped(error) + tags.isEmpty() && keyPatterns.isEmpty() -> FlagSelectionResult.None + else -> FlagSelectionResult.Selected(CanonicalFlagSelection(tags, keyPatterns)) + } + } + + private fun canonicalizeList(values: List): List = values + .asSequence() + .map { value -> value.trim { it in WHITE_SPACE } } + .filter { it.isNotEmpty() } + .sortedBy { it.utf8SortKey() } + .toList() + .distinct() + + // Latin-1 maps each byte to one char, so comparing these strings compares unsigned UTF-8 bytes. + private fun String.utf8SortKey(): String = String(toByteArray(Charsets.UTF_8), Charsets.ISO_8859_1) + + private fun String.utf8Size(): Int = toByteArray(Charsets.UTF_8).size + + private fun List.wellFormedOrNull(): List? = + if (all { it != null && !it.hasUnpairedSurrogate() }) filterIsInstance() else null + + private fun String.hasUnpairedSurrogate(): Boolean { + var index = 0 + var unpaired = false + while (index < length && !unpaired) { + val char = this[index] + val isPair = char.isHighSurrogate() && index + 1 < length && this[index + 1].isLowSurrogate() + unpaired = !isPair && (char.isHighSurrogate() || char.isLowSurrogate()) + index += if (isPair) 2 else 1 + } + return unpaired + } +} diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt new file mode 100644 index 0000000000..b0300b4f9d --- /dev/null +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt @@ -0,0 +1,46 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import com.datadog.android.api.InternalLogger + +// Telemetry carries only the reason, never selection values. +private const val TELEMETRY_REASON_KEY = "reason" + +internal const val FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE = "Flag selection dropped" +internal const val FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE = "Flag selection ignored" + +internal fun InternalLogger.logDroppedFlagSelection(error: FlagSelectionError) { + log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { + "flagSelection was not sent (${error.reason}: ${error.limit}). " + + "Loading the full flag configuration." + } + ) + log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + { FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE }, + additionalProperties = mapOf(TELEMETRY_REASON_KEY to error.reason) + ) +} + +internal fun InternalLogger.logIgnoredFlagSelection(reason: String) { + log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "flagSelection was ignored by the server ($reason). Loaded the full flag configuration." } + ) + log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + { FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE }, + additionalProperties = mapOf(TELEMETRY_REASON_KEY to reason) + ) +} diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt index f0dc4b7136..a8c4e8f2b9 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.flags +import com.datadog.android.flags.internal.selection.FlagSelection import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -30,6 +31,7 @@ internal class FlagsConfigurationTest { assertThat(configuration.customFlagEndpoint).isNull() assertThat(configuration.gracefulModeEnabled).isTrue() assertThat(configuration.initializationTimeoutMs).isEqualTo(5_000L) + assertThat(configuration.flagSelection).isNull() } @Test @@ -181,5 +183,51 @@ internal class FlagsConfigurationTest { assertThat(copiedConfiguration.initializationTimeoutMs).isEqualTo(2_500L) } + @Test + fun `M set the flag selection W flagSelection() { called twice }`() { + // Given + val builder = FlagsConfiguration.Builder() + .flagSelection(tags = listOf("web"), keyPatterns = listOf("web-*")) + + // When + val configuration = builder + .flagSelection(tags = listOf("mobile"), keyPatterns = emptyList()) + .build() + + // Then + assertThat(configuration.flagSelection) + .isEqualTo(FlagSelection(tags = listOf("mobile"), keyPatterns = emptyList())) + } + + @Test + fun `M copy the lists W flagSelection() { caller mutates the lists after the call }`() { + // Given + val tags = mutableListOf("mobile") + val keyPatterns = mutableListOf("checkout-*") + val builder = FlagsConfiguration.Builder().flagSelection(tags, keyPatterns) + + // When + tags += "web" + keyPatterns.clear() + + // Then + assertThat(builder.build().flagSelection) + .isEqualTo(FlagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*"))) + } + + @Test + fun `M preserve the flag selection W copy() { legacy parameters }`() { + // Given + val testedConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*")) + .build() + + // When + val copiedConfiguration = testedConfiguration.copy(trackExposures = false) + + // Then + assertThat(copiedConfiguration.flagSelection).isEqualTo(testedConfiguration.flagSelection) + } + // endregion } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index 0c83863fac..f88f5f6db5 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -12,8 +12,10 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature.Companion.FLAGS_FEATURE_NAME import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.internal.storage.ExposureEventRecordWriter import com.datadog.android.flags.internal.storage.NoOpRecordWriter +import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -104,6 +106,50 @@ internal class FlagsFeatureTest { ).isNotNull() } + @Test + fun `M expose the canonical flag selection W constructor { valid selection }`() { + // When + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = listOf(" web", "mobile", "web"), keyPatterns = listOf("checkout-*")) + .build() + ) + + // Then + assertThat(feature.flagSelection) + .isEqualTo(CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = listOf("checkout-*"))) + } + + @Test + fun `M log the dropped flag selection W onInitialize() { selection over a limit }`() { + // Given + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = List(32) { "tag-$it" }, keyPatterns = emptyList()) + .build() + ) + + // When + feature.onInitialize(mockContext) + + // Then + assertThat(feature.flagSelection).isNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "flagSelection was not sent (too_many_values: each list accepts at most 31 values). " + + "Loading the full flag configuration." + ) + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection dropped", + additionalProperties = mapOf("reason" to "too_many_values") + ) + } + @Test fun `M reset dataWriter to NoOp W onStop`() { // Given diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index bbadb39bad..9309951484 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -21,6 +21,7 @@ import com.datadog.android.flags.internal.FlagsStateManager import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader +import com.datadog.android.flags.internal.net.PrecomputedAssignmentsResponse import com.datadog.android.flags.internal.repository.DefaultFlagsRepository import com.datadog.android.flags.internal.repository.FlagsRepository import com.datadog.android.flags.internal.repository.NoOpFlagsRepository @@ -29,6 +30,7 @@ import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.FlagsClientState import com.datadog.android.flags.utils.forge.ForgeConfigurator import com.datadog.android.internal.utils.DDCoreStateHolder +import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -196,7 +198,8 @@ internal class EvaluationsManagerTest { initializationTimeoutMs = null, initializationTimeoutScheduler = { _, _ -> {} } ) - whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)).thenReturn("response") + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")).thenReturn(emptyMap()) // When @@ -249,7 +252,7 @@ internal class EvaluationsManagerTest { ) whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(mockResponse) + .thenReturn(PrecomputedAssignmentsResponse(mockResponse, null)) whenever(mockPrecomputeMapper.map(mockResponse)).thenReturn(expectedFlags) // When @@ -312,7 +315,7 @@ internal class EvaluationsManagerTest { context, fakeDatadogContext ) - ).thenReturn(invalidResponse) + ).thenReturn(PrecomputedAssignmentsResponse(invalidResponse, null)) whenever(mockPrecomputeMapper.map(invalidResponse)).thenReturn(null) // When @@ -362,7 +365,7 @@ internal class EvaluationsManagerTest { val expectedFlags = mapOf() whenever(mockAssignmentsDownloader.readPrecomputedFlags(publicContext, fakeDatadogContext)) - .thenReturn(mockResponse) + .thenReturn(PrecomputedAssignmentsResponse(mockResponse, null)) whenever(mockPrecomputeMapper.map(mockResponse)).thenReturn(expectedFlags) // When @@ -444,7 +447,7 @@ internal class EvaluationsManagerTest { val flagsMap = emptyMap() whenever(mockAssignmentsDownloader.readPrecomputedFlags(publicContext, fakeDatadogContext)) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(flagsMap) // When @@ -505,7 +508,7 @@ internal class EvaluationsManagerTest { val flagsMap = emptyMap() whenever(mockAssignmentsDownloader.readPrecomputedFlags(publicContext, fakeDatadogContext)) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(flagsMap) // When/Then - should not throw @@ -525,7 +528,7 @@ internal class EvaluationsManagerTest { null } whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -659,7 +662,7 @@ internal class EvaluationsManagerTest { null } whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), eq(fakeDatadogContext))) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -808,7 +811,7 @@ internal class EvaluationsManagerTest { null }.whenever(mockFlagsStateManager).updateState(any()) whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -851,7 +854,7 @@ internal class EvaluationsManagerTest { var timeoutAction: (() -> Unit)? = null var cancellationCount = 0 whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -878,7 +881,7 @@ internal class EvaluationsManagerTest { val mockCallback = mock() var timeoutAction: (() -> Unit)? = null whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenAnswer { checkNotNull(timeoutAction).invoke() emptyMap() @@ -907,7 +910,7 @@ internal class EvaluationsManagerTest { val mockCallback = mock() var scheduleCount = 0 whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), eq(fakeDatadogContext))) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -932,7 +935,7 @@ internal class EvaluationsManagerTest { val mockCallback = mock() var scheduleCount = 0 whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), eq(fakeDatadogContext))) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val testedManager = createManager( initializationTimeoutMs = null, @@ -987,7 +990,7 @@ internal class EvaluationsManagerTest { val mockCallback = mock() var timeoutAction: (() -> Unit)? = null whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -1111,7 +1114,7 @@ internal class EvaluationsManagerTest { } whenever(mockAssignmentsDownloader.readPrecomputedFlags(publicContext, fakeDatadogContext)) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(flagsMap) // When @@ -1126,7 +1129,7 @@ internal class EvaluationsManagerTest { // Given whenever(mockSdkCore.internalLogger).thenReturn(mockInternalLogger) whenever(mockSdkCore.timeProvider).thenReturn(mock()) - val repository = DefaultFlagsRepository(mockSdkCore, "late-success", mock(), persistenceLoadTimeoutMs = 1) + val repository = DefaultFlagsRepository(mockSdkCore, "late-success", mock(), null, persistenceLoadTimeoutMs = 1) val state = FlagsStateManager( DDCoreStateHolder.create(FlagsClientState.NotReady, FlagsStateListener::onStateChanged) ) @@ -1148,7 +1151,8 @@ internal class EvaluationsManagerTest { val callback = mock() val firstKeys = mutableListOf>() repository.firstFlags.whenComplete { firstKeys.add(it) } - whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)).thenReturn("response") + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")).thenReturn(mapOf("enabled" to mock())) // When @@ -1180,7 +1184,7 @@ internal class EvaluationsManagerTest { whenever(mockSdkCore.timeProvider).thenReturn(mock()) doThrow(storageError).whenever(store) .setValue(any(), any(), any(), anyOrNull(), any()) - val repository = DefaultFlagsRepository(mockSdkCore, "throwing", store, persistenceLoadTimeoutMs = 1) + val repository = DefaultFlagsRepository(mockSdkCore, "throwing", store, null, persistenceLoadTimeoutMs = 1) val state = FlagsStateManager( DDCoreStateHolder.create(FlagsClientState.NotReady, FlagsStateListener::onStateChanged) ) @@ -1206,7 +1210,8 @@ internal class EvaluationsManagerTest { firstFlagsCount++ manager.updateEvaluationsForContext(EvaluationContext("newer")) } - whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)).thenReturn("response") + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")).thenReturn(emptyMap()) // When @@ -1241,7 +1246,8 @@ internal class EvaluationsManagerTest { val persistedEntry = FlagsStateEntry( evaluationContext = context, flags = mapOf("cached-flag" to flag), - lastUpdateTimestamp = 0L + lastUpdateTimestamp = 0L, + flagSelection = null ) // Configure DataStore to fire callback synchronously during DefaultFlagsRepository @@ -1264,6 +1270,7 @@ internal class EvaluationsManagerTest { val realRepository = DefaultFlagsRepository( featureSdkCore = mockSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "integration-test" ) @@ -1295,6 +1302,87 @@ internal class EvaluationsManagerTest { // endregion + @Test + fun `M log and install the configuration W updateEvaluationsForContext() { flag selection ignored }`( + @StringForgery(regex = "[a-z_]+") fakeIgnoredReason: String + ) { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, fakeIgnoredReason)) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "flagSelection was ignored by the server ($fakeIgnoredReason). Loaded the full flag configuration." + ) + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection ignored", + additionalProperties = mapOf("reason" to fakeIgnoredReason) + ) + verify(mockFlagsRepository).setFlagsAndContext(eq(context), eq(emptyMap()), any()) + verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) + } + + @Test + fun `M notify STALE W updateEvaluationsForContext() { failed fetch after an empty configuration }`() { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + val mockDataStore = mock() + whenever( + mockDataStore.value( + key = any(), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + ).doAnswer { + it.getArgument>(2).onFailure() + null + } + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.timeProvider) doReturn mock() + val realRepository = DefaultFlagsRepository( + featureSdkCore = mockSdkCore, + dataStore = mockDataStore, + flagSelection = null, + instanceName = "empty-configuration-test" + ) + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null), null) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(emptyMap()) + val testedManager = EvaluationsManager( + sdkCore = mockSdkCore, + executorService = mockExecutorService, + internalLogger = mockInternalLogger, + flagsRepository = realRepository, + assignmentsReader = mockAssignmentsDownloader, + precomputeMapper = mockPrecomputeMapper, + flagStateManager = mockFlagsStateManager, + initializationTimeoutMs = null, + initializationTimeoutScheduler = { _, _ -> {} } + ) + + // When + testedManager.updateEvaluationsForContext(context) + testedManager.updateEvaluationsForContext(context) + + // Then + inOrder(mockFlagsStateManager) { + verify(mockFlagsStateManager).updateState(FlagsClientState.Reconciling) + verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) + verify(mockFlagsStateManager).updateState(FlagsClientState.Reconciling) + verify(mockFlagsStateManager).updateState(FlagsClientState.Stale) + } + } + private fun createManager( flagStateManager: FlagsStateManager = mockFlagsStateManager, initializationTimeoutMs: Long? = null, diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index 31a0c48a4a..53d932a889 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -115,10 +115,36 @@ internal class PrecomputedAssignmentsDownloaderTest { val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext) // Then - assertThat(result).isEqualTo(fakeResponseBody) + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) verify(mockRequestFactory).create(fakeEvaluationContext, fakeDatadogContext) } + @Test + fun `M return flag selection ignored reason W readPrecomputedFlags() { DD-Flag-Selection-Ignored header }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "[a-z_]+") fakeIgnoredReason: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + val fakeRequest = Request.Builder() + .url(fakeUrl) + .build() + val fakeResponse = createSuccessfulResponse(fakeResponseBody, fakeUrl) + .newBuilder() + .header("DD-Flag-Selection-Ignored", fakeIgnoredReason) + .build() + whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext)) + .doReturn(fakeRequest) + whenever(mockCallFactory.newCall(fakeRequest)).doReturn(mockCall) + whenever(mockCall.execute()).doReturn(fakeResponse) + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) + } + @Test fun `M call factory with correct parameters W readPrecomputedFlags()`( @StringForgery fakeResponseBody: String, diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt index 3a427c85f7..69a573a9bc 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt @@ -10,6 +10,7 @@ import com.datadog.android.DatadogSite import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.utils.forge.ForgeConfigurator import fr.xgouchet.elmyr.Forge @@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -59,7 +61,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { mapOf(Feature.RUM_FEATURE_NAME to mapOf("application_id" to fakeRumApplicationId.toString())) ) - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, null) } // region create() - Success cases @@ -120,7 +122,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { ) { // Given val fakeCustomEndpoint = "https://custom-proxy.example.com/flags/assignments" - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, fakeCustomEndpoint) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, fakeCustomEndpoint, null) val context = EvaluationContext( targetingKey = fakeTargetingKey, attributes = emptyMap() @@ -253,6 +255,78 @@ internal class PrecomputedAssignmentsRequestFactoryTest { assertThat(targetingAttributes.getString("attr4")).isEqualTo("value4") } + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun `M add flag_selection to attributes W create() { flag selection configured }`( + useCustomEndpoint: Boolean, + @StringForgery fakeTargetingKey: String + ) { + // Given + val fakeCustomEndpoint = "https://custom-proxy.example.com/flags/assignments".takeIf { useCustomEndpoint } + testedFactory = PrecomputedAssignmentsRequestFactory( + mockInternalLogger, + fakeCustomEndpoint, + CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = listOf("checkout-*")) + ) + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext) + + // Then + checkNotNull(request) + val flagSelection = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + .getJSONObject("flag_selection") + assertThat(flagSelection.keys().asSequence().toList()).containsExactlyInAnyOrder("tags", "key_patterns") + assertThat(flagSelection.getJSONArray("tags").toString()).isEqualTo("""["mobile","web"]""") + assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + } + + @Test + fun `M send both lists W create() { flag selection with key patterns only }`( + @StringForgery fakeTargetingKey: String + ) { + // Given + testedFactory = PrecomputedAssignmentsRequestFactory( + mockInternalLogger, + null, + CanonicalFlagSelection(tags = emptyList(), keyPatterns = listOf("checkout-*")) + ) + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext) + + // Then + checkNotNull(request) + val flagSelection = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + .getJSONObject("flag_selection") + assertThat(flagSelection.getJSONArray("tags").length()).isZero() + assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + } + + @Test + fun `M not add flag_selection W create() { no flag selection }`( + @StringForgery fakeTargetingKey: String + ) { + // Given + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext) + + // Then + checkNotNull(request) + val attributes = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + assertThat(attributes.keys().asSequence().toList()).containsExactlyInAnyOrder("env", "source", "subject") + } + // endregion // region create() - Error cases diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt index f31f70f6f1..69a3ecf7f3 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt @@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import org.junit.jupiter.api.BeforeEach @@ -319,6 +320,33 @@ internal class FlagsStateDeserializerTest { assertThat(result.evaluationContext.attributes["valid_boolean"]).isEqualTo(validBoolean) } + @Test + fun `M return no flag selection W deserialize() { state persisted without a flag selection }`(forge: Forge) { + // Given + val json = buildStateJson(forge.anAlphabeticalString(), JSONObject().put("flag1", buildFlagJson())) + + // When + val result = testedDeserializer.deserialize(json) + + // Then + checkNotNull(result) + assertThat(result.flagSelection).isNull() + } + + @Test + fun `M return null W deserialize() { malformed flag selection }`(forge: Forge) { + // Given + val json = JSONObject(buildStateJson(forge.anAlphabeticalString(), JSONObject())) + .put("flagSelection", JSONObject().put("tags", JSONArray())) + .toString() + + // When + val result = testedDeserializer.deserialize(json) + + // Then + assertThat(result).isNull() + } + private fun buildFlagJson(): JSONObject = JSONObject().apply { put("variationType", "boolean") put("variationValue", "true") diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt index 0173a21741..9935ce977c 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt @@ -9,6 +9,7 @@ package com.datadog.android.flags.internal.persistence import com.datadog.android.api.InternalLogger import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.LongForgery @@ -69,7 +70,7 @@ internal class FlagsStateSerializerTest { ) ) - val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp) + val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp, null) // When val serialized = testedSerializer.serialize(flagsState) @@ -110,7 +111,7 @@ internal class FlagsStateSerializerTest { serialId = null ) ) - val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp) + val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp, null) // When val serialized = testedSerializer.serialize(flagsState) @@ -126,7 +127,7 @@ internal class FlagsStateSerializerTest { // Given val targetingKey = forge.anAlphabeticalString() val evaluationContext = EvaluationContext(targetingKey, emptyMap()) - val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp) + val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp, null) // When val serialized = testedSerializer.serialize(flagsState) @@ -143,7 +144,7 @@ internal class FlagsStateSerializerTest { // Given // Create a state that could potentially cause serialization issues val evaluationContext = EvaluationContext("key", emptyMap()) - val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp) + val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp, null) // When val serialized = testedSerializer.serialize(flagsState) @@ -154,4 +155,44 @@ internal class FlagsStateSerializerTest { SerializedFlagsStateAssert.assertThatSerializedFlagsState(serialized) .hasEmptyFlags() } + + @Test + fun `M round trip the flag selection W serialize() + deserialize()`(forge: Forge) { + // Given + val flagSelection = CanonicalFlagSelection( + tags = listOf("mobile", "\u00E9t\u00E9"), + keyPatterns = listOf("checkout-*") + ) + val flagsState = FlagsStateEntry( + EvaluationContext(forge.anAlphabeticalString(), emptyMap()), + emptyMap(), + fakeTimestamp, + flagSelection + ) + + // When + val serialized = testedSerializer.serialize(flagsState) + val deserialized = FlagsStateDeserializer(mockInternalLogger).deserialize(serialized) + + // Then + checkNotNull(deserialized) + assertThat(deserialized.flagSelection).isEqualTo(flagSelection) + } + + @Test + fun `M omit the flag selection W serialize() { no flag selection }`(forge: Forge) { + // Given + val flagsState = FlagsStateEntry( + EvaluationContext(forge.anAlphabeticalString(), emptyMap()), + emptyMap(), + fakeTimestamp, + null + ) + + // When + val serialized = testedSerializer.serialize(flagsState) + + // Then + assertThat(JSONObject(serialized).has("flagSelection")).isFalse() + } } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 6ccdfbe060..21731fe4b7 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -13,6 +13,7 @@ import com.datadog.android.api.storage.datastore.DataStoreReadCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import com.datadog.android.flags.utils.forge.ForgeConfigurator @@ -88,6 +89,7 @@ internal class DefaultFlagsRepositoryTest { testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "default" ) @@ -140,6 +142,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = forge.anAlphabeticalString(), persistenceLoadTimeoutMs = TimeUnit.SECONDS.toMillis(10) ) @@ -201,6 +204,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "loading", persistenceLoadTimeoutMs = TimeUnit.SECONDS.toMillis(10) ) @@ -241,6 +245,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "async" ) val asyncThread = Thread { @@ -273,6 +278,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "timeout", persistenceLoadTimeoutMs = 1L ) @@ -323,7 +329,7 @@ internal class DefaultFlagsRepositoryTest { }.toMap() val callback = preparePersistenceLoad(forge) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) val snapshot = testedRepository.getFlagsSnapshot() flags.forEach { (key, originalFlag) -> @@ -350,7 +356,7 @@ internal class DefaultFlagsRepositoryTest { anAlphabeticalString() to getForgery() }.toMap() val callback = preparePersistenceLoad(forge) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) val cachedSnapshot = testedRepository.getFlagsSnapshot() testedRepository.setFlagsAndContext(context, flags) @@ -379,7 +385,7 @@ internal class DefaultFlagsRepositoryTest { val networkFlags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) testedRepository.setFlagsAndContext(networkContext, networkFlags) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) assertThat(testedRepository.getFlagsSnapshot()).isSameAs(networkFlags) assertThat(testedRepository.getEvaluationContext()).isEqualTo(networkContext) @@ -392,12 +398,12 @@ internal class DefaultFlagsRepositoryTest { val callback = preparePersistenceLoad(forge) // When - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, emptyMap(), forge.aLong()))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, emptyMap(), forge.aLong(), null))) // Then assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) assertThat(testedRepository.getFlagsSnapshot()).isEmpty() - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasFlags()).isTrue() verify(mockDataStore, never()).setValue( key = any(), data = any(), @@ -415,7 +421,7 @@ internal class DefaultFlagsRepositoryTest { val networkContext = diskContext.copy(targetingKey = diskContext.targetingKey + "-network") val diskFlags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) val callback = preparePersistenceLoad(forge) - val diskContent = DataStoreContent(forge.anInt(), FlagsStateEntry(diskContext, diskFlags, forge.aLong())) + val diskContent = DataStoreContent(forge.anInt(), FlagsStateEntry(diskContext, diskFlags, forge.aLong(), null)) // When if (diskFirst) { @@ -442,13 +448,114 @@ internal class DefaultFlagsRepositoryTest { assertThat(entry.firstValue.flags).isEmpty() } - private fun preparePersistenceLoad(forge: Forge): DataStoreReadCallback { + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun `M ignore persisted state W initial disk read completes { different flag selection }`( + configureSelection: Boolean, + forge: Forge + ) { + // Given + val configuredSelection = CanonicalFlagSelection(listOf("mobile"), emptyList()).takeIf { configureSelection } + val persistedSelection = CanonicalFlagSelection(listOf("web"), emptyList()) + val flags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) + val callback = preparePersistenceLoad(forge, configuredSelection) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry(forge.getForgery(), flags, forge.aLong(), persistedSelection) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isNull() + assertThat(testedRepository.hasFlags()).isFalse() + } + + @Test + fun `M install persisted state W initial disk read completes { same flag selection }`(forge: Forge) { + // Given + val context = forge.getForgery() + val flagKey = forge.anAlphabeticalString() + val callback = preparePersistenceLoad(forge, CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*"))) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry( + context, + mapOf(flagKey to forge.getForgery()), + forge.aLong(), + CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*")) + ) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) + assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() + } + + @Test + fun `M persist the configured flag selection W setFlagsAndContext()`(forge: Forge) { + // Given + val flagSelection = CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*")) + preparePersistenceLoad(forge, flagSelection) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + val entry = argumentCaptor() + verify(mockDataStore).setValue( + key = any(), + data = entry.capture(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + assertThat(entry.firstValue.flagSelection).isEqualTo(flagSelection) + } + + @Test + fun `M return true W hasLoadedFlagsForContext() { empty flags map for the same context }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, emptyMap()) + + // When / Then + assertThat(testedRepository.hasLoadedFlagsForContext(context)).isTrue() + } + + @Test + fun `M return false W hasLoadedFlagsForContext() { different context }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, singleFlagMap) + + // When / Then + assertThat( + testedRepository.hasLoadedFlagsForContext(context.copy(targetingKey = context.targetingKey + "-other")) + ).isFalse() + } + + private fun preparePersistenceLoad( + forge: Forge, + flagSelection: CanonicalFlagSelection? = null + ): DataStoreReadCallback { lateinit var callback: DataStoreReadCallback doAnswer { callback = it.getArgument(2) null }.whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) - testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, forge.anAlphabeticalString(), mockDataStore) + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + forge.anAlphabeticalString(), + mockDataStore, + flagSelection + ) return callback } @@ -461,7 +568,7 @@ internal class DefaultFlagsRepositoryTest { } @Test - fun `M return false W hasFlags() { empty flags map }`(forge: Forge) { + fun `M return true W hasFlags() { empty flags map }`(forge: Forge) { // Given testedRepository.setFlagsAndContext( EvaluationContext(forge.anAlphabeticalString(), emptyMap()), @@ -469,7 +576,7 @@ internal class DefaultFlagsRepositoryTest { ) // When + Then - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasFlags()).isTrue() } @Test @@ -496,7 +603,8 @@ internal class DefaultFlagsRepositoryTest { val persistedEntry = FlagsStateEntry( flags = singleFlagMap, evaluationContext = testContext, - lastUpdateTimestamp = 0L + lastUpdateTimestamp = 0L, + flagSelection = null ) doAnswer { it.getArgument>(2) @@ -511,6 +619,7 @@ internal class DefaultFlagsRepositoryTest { val repository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "with-flags" ) @@ -534,6 +643,7 @@ internal class DefaultFlagsRepositoryTest { val repository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "no-data" ) @@ -556,6 +666,7 @@ internal class DefaultFlagsRepositoryTest { val timeoutRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "timeout", persistenceLoadTimeoutMs = 1L ) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt index 4ccb2e44eb..68728edf74 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt @@ -252,7 +252,7 @@ internal class FirstFlagsInstallationTest { try { // When val loading = executor.submit { - disk.onSuccess(DataStoreContent(1, FlagsStateEntry(context, flags, 0))) + disk.onSuccess(DataStoreContent(1, FlagsStateEntry(context, flags, 0, null))) } // Then @@ -365,13 +365,13 @@ internal class FirstFlagsInstallationTest { disk = it.getArgument(2) null }.whenever(store).value(any(), anyOrNull(), any(), any()) - return DefaultFlagsRepository(core, "first", store, persistenceLoadTimeoutMs = timeoutMs).also { + return DefaultFlagsRepository(core, "first", store, null, persistenceLoadTimeoutMs = timeoutMs).also { it.firstFlags.whenComplete { keys -> events.add(keys) } } } private fun restored(keys: List): DataStoreContent = DataStoreContent( 1, - FlagsStateEntry(context, keys.associateWith { mock() }, 0) + FlagsStateEntry(context, keys.associateWith { mock() }, 0, null) ) } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt new file mode 100644 index 0000000000..6dac8ad484 --- /dev/null +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -0,0 +1,197 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class FlagSelectionCanonicalizerTest { + + @Test + fun `M return None W canonicalize() { no selection }`() { + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(null)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M return None W canonicalize() { lists empty after canonicalization }`() { + // Given + val selection = FlagSelection(tags = listOf("", " \t"), keyPatterns = listOf("\u3000")) + + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(selection)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M trim the Unicode White_Space set W canonicalize()`() { + // Given + val whiteSpace = "\t\n\u000B\u000C\r \u0085\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000" + val selection = FlagSelection( + tags = listOf("${whiteSpace}mobile$whiteSpace"), + keyPatterns = listOf("\uFEFFcheckout\uFEFF", "\u001Cpayment\u001C") + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection( + tags = listOf("mobile"), + keyPatterns = listOf("\u001Cpayment\u001C", "\uFEFFcheckout\uFEFF") + ) + ) + ) + } + + @Test + fun `M drop empty values and dedupe after trimming W canonicalize()`() { + // Given + val selection = FlagSelection( + tags = listOf("web", " mobile", "", "mobile ", "web"), + keyPatterns = emptyList() + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = emptyList()) + ) + ) + } + + @Test + fun `M sort in UTF-8 byte order W canonicalize() { UTF-16 order differs }`() { + // Given + val fullwidthTilde = "\uFF5E" + val grinningFace = "\uD83D\uDE00" + val selection = FlagSelection( + tags = listOf(grinningFace, fullwidthTilde, "a"), + keyPatterns = listOf("b*", "B*", "\u00E9*") + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection( + tags = listOf("a", fullwidthTilde, grinningFace), + keyPatterns = listOf("B*", "b*", "\u00E9*") + ) + ) + ) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("acceptedSelections") + fun `M keep the selection W canonicalize() { at a limit }`( + description: String, + selection: FlagSelection + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(description).isInstanceOf(FlagSelectionResult.Selected::class.java) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("droppedSelections") + fun `M drop the selection W canonicalize() { invalid selection }`( + description: String, + selection: FlagSelection, + expectedError: FlagSelectionError + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(description).isEqualTo(FlagSelectionResult.Dropped(expectedError)) + } + + companion object { + private const val GRINNING_FACE = "\uD83D\uDE00" + + private fun values(count: Int) = List(count) { "value-$it" } + + private fun tags(vararg tags: String?) = FlagSelection(tags = tags.toList(), keyPatterns = emptyList()) + + private fun keyPatterns(vararg keyPatterns: String?) = + FlagSelection(tags = emptyList(), keyPatterns = keyPatterns.toList()) + + @JvmStatic + fun acceptedSelections() = listOf( + Arguments.of("31 tags", FlagSelection(values(31), emptyList())), + Arguments.of("31 key patterns", FlagSelection(emptyList(), values(31))), + Arguments.of("32 tags with one duplicate", FlagSelection(values(31) + "value-0", emptyList())), + Arguments.of("200 one-byte characters", tags("a".repeat(200))), + Arguments.of("100 two-byte characters", tags("\u00E9".repeat(100))), + Arguments.of("66 three-byte characters and 2 bytes", tags("\u20AC".repeat(66) + "ab")), + Arguments.of("50 four-byte characters", keyPatterns(GRINNING_FACE.repeat(50))), + Arguments.of("200 bytes after trimming", keyPatterns(" ${"a".repeat(200)}\u3000")) + ) + + @JvmStatic + fun droppedSelections() = listOf( + Arguments.of("32 tags", FlagSelection(values(32), emptyList()), FlagSelectionError.TOO_MANY_VALUES), + Arguments.of( + "32 key patterns", + FlagSelection(emptyList(), values(32)), + FlagSelectionError.TOO_MANY_VALUES + ), + Arguments.of("201-byte tag", tags("a".repeat(201)), FlagSelectionError.INVALID_TAG), + Arguments.of( + "201-byte tag with two-byte characters", + tags("\u00E9".repeat(100) + "a"), + FlagSelectionError.INVALID_TAG + ), + Arguments.of( + "201-byte tag with three-byte characters", + tags("\u20AC".repeat(67)), + FlagSelectionError.INVALID_TAG + ), + Arguments.of( + "201-byte key pattern with four-byte characters", + keyPatterns(GRINNING_FACE.repeat(50) + "*"), + FlagSelectionError.INVALID_KEY_PATTERN + ), + Arguments.of("lone high surrogate", tags("a\uD83D"), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of("lone low surrogate", keyPatterns("\uDE00a"), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of( + "reversed surrogate pair", + tags("\uDE00\uD83D"), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of("null tag", tags("mobile", null), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of("null key pattern", keyPatterns(null), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of( + "malformed before too many values", + FlagSelection(values(32) + null, emptyList()), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "too many values before invalid key pattern", + FlagSelection(values(32), listOf("a".repeat(201))), + FlagSelectionError.TOO_MANY_VALUES + ), + Arguments.of( + "invalid key pattern before invalid tag", + FlagSelection(listOf("a".repeat(201)), listOf("a".repeat(201))), + FlagSelectionError.INVALID_KEY_PATTERN + ) + ) + } +} From 845e923bd01431506fb04a1055361fb917b08190 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 22 Oct 2026 15:58:26 +0000 Subject: [PATCH 02/44] FFL-3459: Tighten flag selection internals Ref: FFL-3459 --- .../flags/internal/net/PrecomputedAssignmentsReader.kt | 3 +-- .../android/flags/internal/selection/FlagSelectionLogging.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt index a17478a480..f9d9f0c2b6 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt @@ -21,8 +21,7 @@ internal interface PrecomputedAssignmentsReader { } /** - * @param body the raw response body - * @param flagSelectionIgnoredReason the `DD-Flag-Selection-Ignored` header value, or null when absent + * [flagSelectionIgnoredReason] is the `DD-Flag-Selection-Ignored` header value, or null when absent. */ internal data class PrecomputedAssignmentsResponse( val body: String, diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt index b0300b4f9d..9c84618cb5 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt @@ -11,8 +11,8 @@ import com.datadog.android.api.InternalLogger // Telemetry carries only the reason, never selection values. private const val TELEMETRY_REASON_KEY = "reason" -internal const val FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE = "Flag selection dropped" -internal const val FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE = "Flag selection ignored" +private const val FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE = "Flag selection dropped" +private const val FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE = "Flag selection ignored" internal fun InternalLogger.logDroppedFlagSelection(error: FlagSelectionError) { log( From d61bd6ec1a3f5b3e0578f27178a4b4dc82937cbd Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Mon, 26 Oct 2026 17:33:00 +0000 Subject: [PATCH 03/44] FFL-3459: Read the ignored header only when a selection was sent The downloader reports DD-Flag-Selection-Ignored only for requests that carried flag_selection, and passes the value through verbatim. Ref: FFL-3459 --- .../net/PrecomputedAssignmentsDownloader.kt | 6 ++- .../PrecomputedAssignmentsRequestFactory.kt | 2 + .../evaluation/EvaluationsManagerTest.kt | 33 +++++++++++-- .../PrecomputedAssignmentsDownloaderTest.kt | 49 ++++++++++++++----- ...recomputedAssignmentsRequestFactoryTest.kt | 2 + 5 files changed, 75 insertions(+), 17 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index e68f0ef4f5..d613954828 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -56,7 +56,11 @@ internal class PrecomputedAssignmentsDownloader( response.body?.use { it.string() }?.let { body -> PrecomputedAssignmentsResponse( body = body, - flagSelectionIgnoredReason = response.header(HEADER_FLAG_SELECTION_IGNORED) + flagSelectionIgnoredReason = if (requestFactory.sendsFlagSelection) { + response.header(HEADER_FLAG_SELECTION_IGNORED) + } else { + null + } ) } } else { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt index dd45945bdf..52dd72792e 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt @@ -31,6 +31,8 @@ internal class PrecomputedAssignmentsRequestFactory( private val flagSelection: CanonicalFlagSelection? ) { + val sendsFlagSelection: Boolean = flagSelection != null + /** * Creates an OkHttp Request for fetching precomputed flag assignments. * diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 44e4aa437e..79168f2da0 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -52,6 +52,7 @@ import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow @@ -1397,9 +1398,10 @@ internal class EvaluationsManagerTest { // endregion - @Test - fun `M log and install the configuration W updateEvaluationsForContext() { flag selection ignored }`( - @StringForgery(regex = "[a-z_]+") fakeIgnoredReason: String + @ParameterizedTest + @ValueSource(strings = ["too_many_values", "tag_load_failed", "Some Future-Reason"]) + fun `M log the reason verbatim and install W updateEvaluationsForContext() { flag selection ignored }`( + fakeIgnoredReason: String ) { // Given val context = EvaluationContext(fakeTargetingKey, emptyMap()) @@ -1426,6 +1428,31 @@ internal class EvaluationsManagerTest { verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) } + @Test + fun `M not log a flag selection message W updateEvaluationsForContext() { no ignored reason }`() { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever(mockAssignmentsDownloader.readPrecomputedFlags(context, fakeDatadogContext)) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + val messages = argumentCaptor<() -> String>() + verify(mockInternalLogger, atLeast(0)).log( + any(), + any(), + messages.capture(), + anyOrNull(), + any(), + anyOrNull() + ) + assertThat(messages.allValues.map { it() }).noneMatch { "flagSelection" in it || "Flag selection" in it } + verify(mockFlagsRepository).setFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull(), any()) + } + @Test fun `M notify STALE W updateEvaluationsForContext() { failed fetch after an empty configuration }`() { // Given diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index 4f10ed8f72..691d7ce0ea 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -28,6 +28,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -110,30 +112,40 @@ internal class PrecomputedAssignmentsDownloaderTest { verify(mockRequestFactory).create(fakeEvaluationContext, fakeDatadogContext) } + @ParameterizedTest + @ValueSource(strings = ["too_many_values", "tag_load_failed", "Some Future-Reason"]) + fun `M return the header value verbatim W readPrecomputedFlags() { selection sent, ignored header }`( + fakeIgnoredReason: String, + forge: Forge + ) { + // Given + val fakeResponseBody = forge.anAlphabeticalString() + val fakeUrl = "https://${forge.anAlphabeticalString()}.com/precompute" + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = fakeIgnoredReason) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) + } + @Test - fun `M return flag selection ignored reason W readPrecomputedFlags() { DD-Flag-Selection-Ignored header }`( + fun `M return no ignored reason W readPrecomputedFlags() { no selection sent, ignored header }`( @StringForgery fakeResponseBody: String, @StringForgery(regex = "[a-z_]+") fakeIgnoredReason: String, @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String ) { // Given - val fakeRequest = Request.Builder() - .url(fakeUrl) - .build() - val fakeResponse = createSuccessfulResponse(fakeResponseBody, fakeUrl) - .newBuilder() - .header("DD-Flag-Selection-Ignored", fakeIgnoredReason) - .build() - whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext)) - .doReturn(fakeRequest) - whenever(mockCallFactory.newCall(fakeRequest)).doReturn(mockCall) - whenever(mockCall.execute()).doReturn(fakeResponse) + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = fakeIgnoredReason) + whenever(mockRequestFactory.sendsFlagSelection) doReturn false // When val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext) // Then - assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) } @Test @@ -419,6 +431,17 @@ internal class PrecomputedAssignmentsDownloaderTest { // region Helper methods + private fun stubSuccessfulCall(body: String, url: String, ignoredReason: String) { + val fakeRequest = Request.Builder().url(url).build() + val fakeResponse = createSuccessfulResponse(body, url) + .newBuilder() + .header("DD-Flag-Selection-Ignored", ignoredReason) + .build() + whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext)) doReturn fakeRequest + whenever(mockCallFactory.newCall(fakeRequest)) doReturn mockCall + whenever(mockCall.execute()) doReturn fakeResponse + } + private fun createSuccessfulResponse(body: String, url: String): Response = Response.Builder() .request(Request.Builder().url(url).build()) .protocol(Protocol.HTTP_1_1) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt index 02f154ca3f..74ca4a69c3 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt @@ -321,6 +321,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { assertThat(flagSelection.keys().asSequence().toList()).containsExactlyInAnyOrder("tags", "key_patterns") assertThat(flagSelection.getJSONArray("tags").toString()).isEqualTo("""["mobile","web"]""") assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + assertThat(testedFactory.sendsFlagSelection).isTrue() } @Test @@ -364,6 +365,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { .getJSONObject("data") .getJSONObject("attributes") assertThat(attributes.keys().asSequence().toList()).containsExactlyInAnyOrder("env", "source", "subject") + assertThat(testedFactory.sendsFlagSelection).isFalse() } // endregion From 9e783442f4bf1fb68817c60c72d123eaeb44dd91 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Mon, 26 Oct 2026 17:36:28 +0000 Subject: [PATCH 04/44] FFL-3459: Accept null lists in flagSelection Builder.flagSelection takes List? for tags and key patterns. A null list is treated as absent, matching the browser SDK; null elements inside a list remain malformed_selection. Ref: FFL-3459 --- features/dd-sdk-android-flags/api/apiSurface | 2 +- .../android/flags/FlagsConfiguration.kt | 4 +-- .../flags/internal/selection/FlagSelection.kt | 10 +++---- .../android/flags/FlagsConfigurationTest.kt | 12 ++++++++ .../FlagSelectionCanonicalizerTest.kt | 28 +++++++++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/features/dd-sdk-android-flags/api/apiSurface b/features/dd-sdk-android-flags/api/apiSurface index 382cb309dc..45b9e1779c 100644 --- a/features/dd-sdk-android-flags/api/apiSurface +++ b/features/dd-sdk-android-flags/api/apiSurface @@ -33,7 +33,7 @@ data class com.datadog.android.flags.FlagsConfiguration fun evaluationFlushInterval(Long): Builder fun useCustomFlagEndpoint(String): Builder fun initializationTimeout(Long): Builder - fun flagSelection(List, List): Builder + fun flagSelection(List?, List?): Builder fun rumIntegrationEnabled(Boolean): Builder fun gracefulModeEnabled(Boolean): Builder fun build(): FlagsConfiguration diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index f8bf636bae..1f4c06962d 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -192,8 +192,8 @@ data class FlagsConfiguration internal constructor( * @param keyPatterns The flag key patterns to load. * @return this [Builder] instance for method chaining. */ - fun flagSelection(tags: List, keyPatterns: List): Builder = apply { - flagSelection = FlagSelection(tags.toList(), keyPatterns.toList()) + fun flagSelection(tags: List?, keyPatterns: List?): Builder = apply { + flagSelection = FlagSelection(tags?.toList(), keyPatterns?.toList()) } /** diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index 23fa30df49..e81868c8ac 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -8,11 +8,11 @@ package com.datadog.android.flags.internal.selection /** * The flag selection as configured by the application, before canonicalization. - * Elements are nullable because Java callers can pass lists that hold nulls. + * Lists and elements are nullable because Java callers can pass nulls. A null list means the list is absent. */ internal data class FlagSelection( - val tags: List, - val keyPatterns: List + val tags: List?, + val keyPatterns: List? ) /** @@ -69,8 +69,8 @@ internal object FlagSelectionCanonicalizer { @Suppress("ReturnCount") fun canonicalize(selection: FlagSelection?): FlagSelectionResult { if (selection == null) return FlagSelectionResult.None - val rawTags = selection.tags.wellFormedOrNull() - val rawKeyPatterns = selection.keyPatterns.wellFormedOrNull() + val rawTags = selection.tags.orEmpty().wellFormedOrNull() + val rawKeyPatterns = selection.keyPatterns.orEmpty().wellFormedOrNull() if (rawTags == null || rawKeyPatterns == null) { return FlagSelectionResult.Dropped(FlagSelectionError.MALFORMED_SELECTION) } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt index a8c4e8f2b9..b85c6338bb 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt @@ -215,6 +215,18 @@ internal class FlagsConfigurationTest { .isEqualTo(FlagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*"))) } + @Test + fun `M keep null lists as absent W flagSelection() { null lists }`() { + // When + val configuration = FlagsConfiguration.Builder() + .flagSelection(tags = null, keyPatterns = listOf("checkout-*")) + .build() + + // Then + assertThat(configuration.flagSelection) + .isEqualTo(FlagSelection(tags = null, keyPatterns = listOf("checkout-*"))) + } + @Test fun `M preserve the flag selection W copy() { legacy parameters }`() { // Given diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index 6dac8ad484..e64d8005b4 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -29,6 +29,34 @@ internal class FlagSelectionCanonicalizerTest { assertThat(FlagSelectionCanonicalizer.canonicalize(selection)).isEqualTo(FlagSelectionResult.None) } + @Test + fun `M return None W canonicalize() { both lists null }`() { + // Given + val selection = FlagSelection(tags = null, keyPatterns = null) + + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(selection)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M treat a null list as empty W canonicalize() { one list null }`() { + // Given + val tagsOnly = FlagSelection(tags = listOf("mobile"), keyPatterns = null) + val keyPatternsOnly = FlagSelection(tags = null, keyPatterns = listOf("checkout-*")) + + // When + val tagsOnlyResult = FlagSelectionCanonicalizer.canonicalize(tagsOnly) + val keyPatternsOnlyResult = FlagSelectionCanonicalizer.canonicalize(keyPatternsOnly) + + // Then + assertThat(tagsOnlyResult).isEqualTo( + FlagSelectionResult.Selected(CanonicalFlagSelection(tags = listOf("mobile"), keyPatterns = emptyList())) + ) + assertThat(keyPatternsOnlyResult).isEqualTo( + FlagSelectionResult.Selected(CanonicalFlagSelection(tags = emptyList(), keyPatterns = listOf("checkout-*"))) + ) + } + @Test fun `M trim the Unicode White_Space set W canonicalize()`() { // Given From e5562f4ed1aaf9804c1a8eae96a1a3ee545d13d0 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Mon, 26 Oct 2026 17:37:05 +0000 Subject: [PATCH 05/44] FFL-3459: Document flagSelection limits and server behavior State that limits count values after dedupe, that flags outside the selection return the default with FLAG_NOT_FOUND, and when the server returns the full configuration instead. Ref: FFL-3459 --- .../android/flags/FlagsConfiguration.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 1f4c06962d..7904ffa36c 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -7,6 +7,7 @@ package com.datadog.android.flags import com.datadog.android.flags.internal.selection.FlagSelection +import com.datadog.android.flags.model.ErrorCode private const val DEFAULT_INITIALIZATION_TIMEOUT_MS = 5_000L @@ -180,16 +181,22 @@ data class FlagsConfiguration internal constructor( * * Tags match exactly and are case-sensitive; a flag matches if it has any of the tags. Key patterns are * whole-key globs where only `*` is special. Leading and trailing Unicode whitespace is trimmed from every - * value, empty values are dropped, and duplicates are removed. Each list accepts at most 31 values, and each - * tag and key pattern is at most 200 UTF-8 bytes. A selection over a limit is not sent, and the full flag - * configuration is loaded. Flags outside the selection are not loaded, so evaluating them returns the - * default value. + * value, empty values are dropped, and duplicates are removed. A null list is treated as absent. + * + * Each list accepts at most 31 values after duplicates are removed, and each tag and key pattern is at most + * 200 UTF-8 bytes. A selection over a limit, or one that contains a null value, is not sent, and the full + * flag configuration is loaded. The server may also ignore a very large selection (`too_many_values`), in + * which case the full flag configuration is loaded. Servers that do not support flag selection, or have it + * disabled, return the full flag configuration without a warning. + * + * Flags outside the selection are not loaded, so evaluating them returns the default value with + * [ErrorCode.FLAG_NOT_FOUND]. * * The selection is fixed when the Flags feature is enabled. Calling this method again replaces the * previous selection. * - * @param tags The flag tags to load. - * @param keyPatterns The flag key patterns to load. + * @param tags The flag tags to load, or null for none. + * @param keyPatterns The flag key patterns to load, or null for none. * @return this [Builder] instance for method chaining. */ fun flagSelection(tags: List?, keyPatterns: List?): Builder = apply { From 6766ce916a04593a659ff7db635dfe8a8cfa9a4f Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Mon, 26 Oct 2026 17:38:16 +0000 Subject: [PATCH 06/44] FFL-3459: Cover the shared flag selection vectors in unit tests Add hand-written canonicalizer cases for the selection vectors with sdk expectations that the existing tests did not cover. Ref: FFL-3459 --- .../FlagSelectionCanonicalizerTest.kt | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index e64d8005b4..03bd713f74 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.util.Locale internal class FlagSelectionCanonicalizerTest { @@ -150,9 +151,28 @@ internal class FlagSelectionCanonicalizerTest { assertThat(result).describedAs(description).isEqualTo(FlagSelectionResult.Dropped(expectedError)) } + @ParameterizedTest(name = "{0}") + @MethodSource("sharedVectorSelections") + fun `M match the shared selection vector W canonicalize()`( + vectorName: String, + selection: FlagSelection, + expectedResult: FlagSelectionResult + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(vectorName).isEqualTo(expectedResult) + } + companion object { private const val GRINNING_FACE = "\uD83D\uDE00" + private fun numbered(prefix: String, count: Int) = List(count) { "$prefix%02d".format(Locale.US, it + 1) } + + private fun selected(tags: List, keyPatterns: List) = + FlagSelectionResult.Selected(CanonicalFlagSelection(tags, keyPatterns)) + private fun values(count: Int) = List(count) { "value-$it" } private fun tags(vararg tags: String?) = FlagSelection(tags = tags.toList(), keyPatterns = emptyList()) @@ -172,6 +192,97 @@ internal class FlagSelectionCanonicalizerTest { Arguments.of("200 bytes after trimming", keyPatterns(" ${"a".repeat(200)}\u3000")) ) + // Cases with `sdk` expectations in DataDog/ffe-system-test-data@e8777633 + // flag-selection/selection-vectors.json that the tests above do not already cover. + @JvmStatic + fun sharedVectorSelections() = listOf( + Arguments.of( + "leading star", + keyPatterns("*-v2"), + selected(emptyList(), listOf("*-v2")) + ), + Arguments.of( + "star matches an empty run and multiple stars are allowed", + keyPatterns("*search*auto*complete*"), + selected(emptyList(), listOf("*search*auto*complete*")) + ), + Arguments.of( + "lone star matches every key", + keyPatterns("*"), + selected(emptyList(), listOf("*")) + ), + Arguments.of( + "question mark and brackets are literal", + keyPatterns("search.ranking-v?", "[a]*", "{a,b}"), + selected(emptyList(), listOf("[a]*", "search.ranking-v?", "{a,b}")) + ), + Arguments.of( + "braces are literal even when an alternative names a flag", + keyPatterns("{mobile.dark-mode,missing}"), + selected(emptyList(), listOf("{mobile.dark-mode,missing}")) + ), + Arguments.of( + "tags are never globs", + tags("team:*"), + selected(listOf("team:*"), emptyList()) + ), + Arguments.of( + "tags match case-sensitively", + tags("Team:Payments", "MOBILE"), + selected(listOf("MOBILE", "Team:Payments"), emptyList()) + ), + Arguments.of( + "characters outside Unicode White_Space are not trimmed", + tags("\uFEFFmobile", "mobile\u200B"), + selected(listOf("mobile\u200B", "\uFEFFmobile"), emptyList()) + ), + Arguments.of( + "values sort by UTF-8 bytes", + tags(GRINNING_FACE, "\uFF5E", "a", "Z"), + selected(listOf("Z", "a", "\uFF5E", GRINNING_FACE), emptyList()) + ), + Arguments.of( + "31 values in each list are allowed", + FlagSelection(numbered("t", 31), numbered("p", 31)), + selected(numbered("t", 31), numbered("p", 31)) + ), + Arguments.of( + "duplicates do not count toward the limit", + FlagSelection(numbered("t", 31) + "t01" + " t02 ", emptyList()), + selected(numbered("t", 31), emptyList()) + ), + Arguments.of( + "key pattern length counts UTF-8 bytes: 100 two-byte characters are allowed", + keyPatterns("\u00E9".repeat(100)), + selected(emptyList(), listOf("\u00E9".repeat(100))) + ), + Arguments.of( + "tag length is measured after trimming", + tags(" ${"a".repeat(200)} "), + selected(listOf("a".repeat(200)), emptyList()) + ), + Arguments.of( + "32 values in one list are rejected even when the other is within the limit", + FlagSelection(numbered("t", 31), numbered("p", 32)), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "key pattern length counts UTF-8 bytes: 201 bytes are invalid", + keyPatterns("x" + "\u00E9".repeat(100)), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ), + Arguments.of( + "a long tag invalidates the selection even when other tags match", + tags("mobile", "a".repeat(201)), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) + ), + Arguments.of( + "too_many_values takes precedence over invalid_tag", + FlagSelection(numbered("t", 31) + "a".repeat(201), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ) + ) + @JvmStatic fun droppedSelections() = listOf( Arguments.of("32 tags", FlagSelection(values(32), emptyList()), FlagSelectionError.TOO_MANY_VALUES), From 61f6476eeaa59fc7699dddc54ad03ea673f304b3 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Tue, 27 Oct 2026 19:14:05 +0000 Subject: [PATCH 07/44] FFL-3459: Ignore an empty flag selection ignored header Treat an empty DD-Flag-Selection-Ignored value as absent, as the browser SDK does, so it no longer logs a server ignore with an empty reason. Clarify in the flagSelection KDoc that the byte limit applies after trimming, that unpaired UTF-16 surrogates are malformed, and that FLAG_NOT_FOUND applies when the server applies the selection. Ref: FFL-3459 --- .../datadog/android/flags/FlagsConfiguration.kt | 12 ++++++------ .../net/PrecomputedAssignmentsDownloader.kt | 2 +- .../net/PrecomputedAssignmentsDownloaderTest.kt | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 7904ffa36c..5c526f0c7b 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -184,13 +184,13 @@ data class FlagsConfiguration internal constructor( * value, empty values are dropped, and duplicates are removed. A null list is treated as absent. * * Each list accepts at most 31 values after duplicates are removed, and each tag and key pattern is at most - * 200 UTF-8 bytes. A selection over a limit, or one that contains a null value, is not sent, and the full - * flag configuration is loaded. The server may also ignore a very large selection (`too_many_values`), in - * which case the full flag configuration is loaded. Servers that do not support flag selection, or have it - * disabled, return the full flag configuration without a warning. + * 200 UTF-8 bytes after trimming. A selection over a limit, or one that contains a null value or an unpaired + * UTF-16 surrogate, is not sent, and the full flag configuration is loaded. The server may also ignore a very + * large selection (`too_many_values`), in which case the full flag configuration is loaded. Servers that do + * not support flag selection, or have it disabled, return the full flag configuration without a warning. * - * Flags outside the selection are not loaded, so evaluating them returns the default value with - * [ErrorCode.FLAG_NOT_FOUND]. + * When the server applies the selection, flags outside it are not loaded, so evaluating them returns the + * default value with [ErrorCode.FLAG_NOT_FOUND]. * * The selection is fixed when the Flags feature is enabled. Calling this method again replaces the * previous selection. diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index d613954828..3e1ee7b9e4 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -57,7 +57,7 @@ internal class PrecomputedAssignmentsDownloader( PrecomputedAssignmentsResponse( body = body, flagSelectionIgnoredReason = if (requestFactory.sendsFlagSelection) { - response.header(HEADER_FLAG_SELECTION_IGNORED) + response.header(HEADER_FLAG_SELECTION_IGNORED)?.takeIf { it.isNotEmpty() } } else { null } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index 691d7ce0ea..92276f60e1 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -131,6 +131,22 @@ internal class PrecomputedAssignmentsDownloaderTest { assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) } + @Test + fun `M return no ignored reason W readPrecomputedFlags() { selection sent, empty ignored header }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = "") + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) + } + @Test fun `M return no ignored reason W readPrecomputedFlags() { no selection sent, ignored header }`( @StringForgery fakeResponseBody: String, From e9a63066920a1c82b64718e1e608f7b008a6e39d Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Tue, 27 Oct 2026 19:14:06 +0000 Subject: [PATCH 08/44] FFL-3459: Cover selection wiring, legacy caches and the trim set Verify that FlagsClient passes the configured selection to the repository, so a persisted state saved under that selection is served. Verify that a persisted state saved without a selection is discarded once a selection is configured. Extend the trim test to every Unicode White_Space character. Ref: FFL-3459 --- .../datadog/android/flags/FlagsClientTest.kt | 77 +++++++++++++++++++ .../repository/DefaultFlagsRepositoryTest.kt | 18 +++++ .../FlagSelectionCanonicalizerTest.kt | 3 +- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt index d43dd3df32..77593ab50a 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt @@ -11,13 +11,22 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.Feature.Companion.FLAGS_FEATURE_NAME import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.flags.internal.DatadogFlagsClient import com.datadog.android.flags.internal.FlagsFeature import com.datadog.android.flags.internal.NoOpFlagsClient +import com.datadog.android.flags.internal.model.FlagsStateEntry +import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Call import org.assertj.core.api.Assertions.assertThat +import org.json.JSONObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -26,12 +35,16 @@ import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.concurrent.ExecutorService @ExtendWith(MockitoExtension::class, ForgeExtension::class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -118,6 +131,70 @@ internal class FlagsClientTest { flagsFeature?.clearClients() } + // region Flag Selection Wiring + + @Test + fun `M key persisted flags by the configured selection W createInternal() { flag selection }`( + @StringForgery fakeFlagKey: String, + @StringForgery fakeClientName: String + ) { + // Given + val mockCore = mock() + val mockDataStore = mock() + val mockFeatureScope = mock() + whenever(mockCore.internalLogger).thenReturn(mockInternalLogger) + whenever(mockCore.createSingleThreadExecutorService(any())).thenReturn(mock()) + whenever(mockCore.createOkHttpCallFactory(any())).thenReturn(mock()) + whenever(mockFeatureScope.dataStore).thenReturn(mockDataStore) + whenever(mockCore.getFeature(FLAGS_FEATURE_NAME)).thenReturn(mockFeatureScope) + lateinit var readCallback: DataStoreReadCallback + doAnswer { + readCallback = it.getArgument(2) + null + }.whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) + val configuration = FlagsConfiguration.Builder() + .trackEvaluations(false) + .flagSelection(tags = listOf("mobile"), keyPatterns = null) + .build() + val client = FlagsClient.createInternal( + configuration = configuration, + featureSdkCore = mockCore, + flagsFeature = FlagsFeature(mockCore, configuration), + evaluationsFeature = null, + name = fakeClientName, + evaluationTelemetryOptOutParameterEnabled = false + ) as DatadogFlagsClient + + // When + readCallback.onSuccess( + DataStoreContent( + 1, + FlagsStateEntry( + evaluationContext = EvaluationContext("user"), + flags = mapOf( + fakeFlagKey to + PrecomputedFlag( + "boolean", + "true", + false, + "allocation", + "on", + JSONObject(), + "TARGETING_MATCH" + ) + ), + lastUpdateTimestamp = 0L, + flagSelection = CanonicalFlagSelection(tags = listOf("mobile"), keyPatterns = emptyList()) + ) + ) + ) + + // Then + assertThat(client.getFlagAssignmentsSnapshot()).containsOnlyKeys(fakeFlagKey) + } + + // endregion + // region Static Flag Resolution Methods @Test diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 9867b34ed0..afc7542667 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -486,6 +486,24 @@ internal class DefaultFlagsRepositoryTest { assertThat(testedRepository.hasFlags()).isFalse() } + @Test + fun `M ignore persisted state W initial disk read completes { state saved without a flag selection }`( + forge: Forge + ) { + // Given + val flags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) + val callback = preparePersistenceLoad(forge, CanonicalFlagSelection(listOf("mobile"), emptyList())) + + // When + callback.onSuccess( + DataStoreContent(forge.anInt(), FlagsStateEntry(forge.getForgery(), flags, forge.aLong())) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isNull() + assertThat(testedRepository.hasFlags()).isFalse() + } + @Test fun `M install persisted state W initial disk read completes { same flag selection }`(forge: Forge) { // Given diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index 03bd713f74..84185cc71d 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -61,7 +61,8 @@ internal class FlagSelectionCanonicalizerTest { @Test fun `M trim the Unicode White_Space set W canonicalize()`() { // Given - val whiteSpace = "\t\n\u000B\u000C\r \u0085\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000" + val whiteSpace = "\t\n\u000B\u000C\r \u0085\u00A0\u1680" + + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000" val selection = FlagSelection( tags = listOf("${whiteSpace}mobile$whiteSpace"), keyPatterns = listOf("\uFEFFcheckout\uFEFF", "\u001Cpayment\u001C") From 2d0533426910e690871870a2b11f7110e1f6cd6d Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Tue, 27 Oct 2026 19:14:07 +0000 Subject: [PATCH 09/44] FFL-3459: Simplify flag selection internals and tests Dedupe selection values before sorting, compute each UTF-8 sort key once, and skip sorting a list that is already over the value limit. Pass the response mapper by reference. Drop positional null arguments in tests that only restated the default obfuscation value. Ref: FFL-3459 --- .../internal/evaluation/EvaluationsManager.kt | 2 +- .../flags/internal/selection/FlagSelection.kt | 21 ++++++++++++------- .../persistence/FlagsStateSerializerTest.kt | 8 +++---- .../repository/DefaultFlagsRepositoryTest.kt | 10 ++++----- .../repository/FirstFlagsInstallationTest.kt | 4 ++-- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 75874efdcd..4b28d13725 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -150,7 +150,7 @@ internal class EvaluationsManager( hadFlags && flagsRepository.getEvaluationContext() == context ) val response = datadogContext?.let { assignmentsReader.readPrecomputedFlags(context, it) } - val assignments = response?.let { mapResponse(it) } + val assignments = response?.let(::mapResponse) ?.takeIf { it.obfuscation == null || supportsObfuscation } if (assignments != null) { installFlags(context, assignments, initializationCompletion, callback) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index e81868c8ac..988aec5e7e 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -91,13 +91,20 @@ internal object FlagSelectionCanonicalizer { } } - private fun canonicalizeList(values: List): List = values - .asSequence() - .map { value -> value.trim { it in WHITE_SPACE } } - .filter { it.isNotEmpty() } - .sortedBy { it.utf8SortKey() } - .toList() - .distinct() + private fun canonicalizeList(values: List): List { + val distinct = values + .asSequence() + .map { value -> value.trim { it in WHITE_SPACE } } + .filter { it.isNotEmpty() } + .toList() + .distinct() + // An over-limit list is dropped as too_many_values, so its order does not matter. + if (distinct.size > MAX_VALUES) return distinct + return distinct + .map { it to it.utf8SortKey() } + .sortedBy { it.second } + .map { it.first } + } // Latin-1 maps each byte to one char, so comparing these strings compares unsigned UTF-8 bytes. private fun String.utf8SortKey(): String = String(toByteArray(Charsets.UTF_8), Charsets.ISO_8859_1) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt index 4ae942584e..106ebfae07 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt @@ -104,7 +104,7 @@ internal class FlagsStateSerializerTest { ) ) - val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp, null) + val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp) // When val serialized = testedSerializer.serialize(flagsState) @@ -145,7 +145,7 @@ internal class FlagsStateSerializerTest { serialId = null ) ) - val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp, null) + val flagsState = FlagsStateEntry(evaluationContext, flags, fakeTimestamp) // When val serialized = testedSerializer.serialize(flagsState) @@ -161,7 +161,7 @@ internal class FlagsStateSerializerTest { // Given val targetingKey = forge.anAlphabeticalString() val evaluationContext = EvaluationContext(targetingKey, emptyMap()) - val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp, null) + val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp) // When val serialized = testedSerializer.serialize(flagsState) @@ -178,7 +178,7 @@ internal class FlagsStateSerializerTest { // Given // Create a state that could potentially cause serialization issues val evaluationContext = EvaluationContext("key", emptyMap()) - val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp, null) + val flagsState = FlagsStateEntry(evaluationContext, emptyMap(), fakeTimestamp) // When val serialized = testedSerializer.serialize(flagsState) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index afc7542667..7b624d4c6d 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -342,7 +342,7 @@ internal class DefaultFlagsRepositoryTest { }.toMap() val callback = preparePersistenceLoad(forge) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) val snapshot = testedRepository.getFlagsSnapshot() flags.forEach { (key, originalFlag) -> @@ -369,7 +369,7 @@ internal class DefaultFlagsRepositoryTest { anAlphabeticalString() to getForgery() }.toMap() val callback = preparePersistenceLoad(forge) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) val cachedSnapshot = testedRepository.getFlagsSnapshot() testedRepository.setFlagsAndContext(context, flags) @@ -398,7 +398,7 @@ internal class DefaultFlagsRepositoryTest { val networkFlags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) testedRepository.setFlagsAndContext(networkContext, networkFlags) - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong(), null))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, flags, forge.aLong()))) assertThat(testedRepository.getFlagsSnapshot()).isSameAs(networkFlags) assertThat(testedRepository.getEvaluationContext()).isEqualTo(networkContext) @@ -411,7 +411,7 @@ internal class DefaultFlagsRepositoryTest { val callback = preparePersistenceLoad(forge) // When - callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, emptyMap(), forge.aLong(), null))) + callback.onSuccess(DataStoreContent(forge.anInt(), FlagsStateEntry(context, emptyMap(), forge.aLong()))) // Then assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) @@ -434,7 +434,7 @@ internal class DefaultFlagsRepositoryTest { val networkContext = diskContext.copy(targetingKey = diskContext.targetingKey + "-network") val diskFlags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) val callback = preparePersistenceLoad(forge) - val diskContent = DataStoreContent(forge.anInt(), FlagsStateEntry(diskContext, diskFlags, forge.aLong(), null)) + val diskContent = DataStoreContent(forge.anInt(), FlagsStateEntry(diskContext, diskFlags, forge.aLong())) // When if (diskFirst) { diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt index 6c3ab53690..b9f4ef0839 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt @@ -252,7 +252,7 @@ internal class FirstFlagsInstallationTest { try { // When val loading = executor.submit { - disk.onSuccess(DataStoreContent(1, FlagsStateEntry(context, flags, 0, null))) + disk.onSuccess(DataStoreContent(1, FlagsStateEntry(context, flags, 0))) } // Then @@ -372,6 +372,6 @@ internal class FirstFlagsInstallationTest { private fun restored(keys: List): DataStoreContent = DataStoreContent( 1, - FlagsStateEntry(context, keys.associateWith { mock() }, 0, null) + FlagsStateEntry(context, keys.associateWith { mock() }, 0) ) } From ec0039c6552ac32ae3666edd5914dadd1d0ec978 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:46:09 -0400 Subject: [PATCH 10/44] FFL-3459: Test the ignored reason with an unparseable body The reason is reported before the body is parsed, so it is logged even when the configuration is not installed. --- .../evaluation/EvaluationsManagerTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index b1b0c5afaf..a63c5a8eb0 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -1679,6 +1679,40 @@ internal class EvaluationsManagerTest { verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) } + @Test + fun `M log the ignored reason and not install W updateEvaluationsForContext() { ignored reason, invalid body }`( + @StringForgery fakeInvalidBody: String + ) { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ).thenReturn(PrecomputedAssignmentsResponse(fakeInvalidBody, "too_many_values")) + whenever(mockPrecomputeMapper.map(fakeInvalidBody)).thenReturn(null) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "flagSelection was ignored by the server (too_many_values). Loaded the full flag configuration." + ) + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection ignored", + additionalProperties = mapOf("reason" to "too_many_values") + ) + verify(mockFlagsRepository, never()).setFlagsAndContext(any(), any(), anyOrNull(), any()) + verify(mockFlagsStateManager).updateState(isA()) + } + @Test fun `M not log a flag selection message W updateEvaluationsForContext() { no ignored reason }`() { // Given From a8cfcab920c5a50d888b498b12cbe51027e65af3 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:46:10 -0400 Subject: [PATCH 11/44] FFL-3459: Assert the selection reaches the request factory --- .../com/datadog/android/flags/internal/FlagsFeatureTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index 8709c2bf02..c7c11f2e99 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -115,6 +115,7 @@ internal class FlagsFeatureTest { // Then assertThat(feature.flagSelection) .isEqualTo(CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = listOf("checkout-*"))) + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).isTrue() } @Test @@ -132,6 +133,7 @@ internal class FlagsFeatureTest { // Then assertThat(feature.flagSelection).isNull() + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).isFalse() mockInternalLogger.verifyLog( InternalLogger.Level.ERROR, InternalLogger.Target.USER, From fce86258710029e5d01034df5467a0e078138588 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:46:11 -0400 Subject: [PATCH 12/44] FFL-3459: Describe flag selection rules without backend names --- .../android/flags/internal/selection/FlagSelection.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index 988aec5e7e..998378766b 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -24,7 +24,7 @@ internal data class CanonicalFlagSelection( ) /** - * Reasons a flag selection is dropped. Values match the reasons used by Compute and ffe-service. + * Reasons a flag selection is dropped. The values are the reason codes that the server and the other Datadog SDKs use. */ internal enum class FlagSelectionError(val reason: String, val limit: String) { MALFORMED_SELECTION( @@ -52,7 +52,7 @@ internal sealed interface FlagSelectionResult { } /** - * Canonicalizes and validates a flag selection the same way Compute and ffe-service do. + * Canonicalizes and validates a flag selection with the same rules as the server. */ internal object FlagSelectionCanonicalizer { @@ -60,7 +60,7 @@ internal object FlagSelectionCanonicalizer { internal const val MAX_TAG_BYTES = 200 internal const val MAX_KEY_PATTERN_BYTES = 200 - // The Unicode White_Space property, which Compute trims with Rust's str::trim. + // The Unicode White_Space property, which the server trims. // Kotlin's trim() differs: it also trims U+001C..U+001F and does not trim U+0085. private const val WHITE_SPACE = "\u0009\u000A\u000B\u000C\u000D\u0020\u0085\u00A0\u1680" + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A" + From 1cc677143792acd7959c1757c520d91a06ac336c Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:48:13 -0400 Subject: [PATCH 13/44] FFL-3459: Document flagSelection in the module README --- features/dd-sdk-android-flags/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index 47e6a7fca5..d2b279ea7a 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -93,6 +93,25 @@ val flagsConfig = FlagsConfiguration.Builder() .build() ``` +#### Load only selected flags + +To load only the flags that your app uses, set a flag selection. The SDK sends it with the flag request, and the server returns only the flags that have any of the tags or whose key matches any of the key patterns: + +```kotlin +val flagsConfig = FlagsConfiguration.Builder() + .flagSelection( + tags = listOf("mobile"), + keyPatterns = listOf("checkout-*") + ) + .build() +``` + +- Tags match exactly and are case-sensitive. A key pattern matches the whole flag key, and only `*` is a wildcard. +- Each list accepts at most 31 values, and each value is at most 200 UTF-8 bytes. The SDK does not send a selection that is over a limit. It logs an error and loads the full flag configuration. +- When the server applies the selection, evaluating a flag outside it returns the default value with `ErrorCode.FLAG_NOT_FOUND`. +- The selection is fixed when you call `Flags.enable`. +- With a custom flag endpoint, the proxy must forward the `DD-Flag-Selection-Ignored` response header. Otherwise, the SDK cannot report a selection that the server ignored. + ## Use the Feature Flags SDK ### Create a Flags client From d365cbb9a29ab7ce23f9569c5d5afbf4258099c9 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:53:31 -0400 Subject: [PATCH 14/44] FFL-3459: Run every shared selection vector case by name List all 38 cases that have an sdk expectation in the shared vectors, in file order, so each one maps to one test row. --- .../FlagSelectionCanonicalizerTest.kt | 152 +++++++++++++++--- 1 file changed, 129 insertions(+), 23 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index 84185cc71d..3c8617f9b8 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -193,94 +193,200 @@ internal class FlagSelectionCanonicalizerTest { Arguments.of("200 bytes after trimming", keyPatterns(" ${"a".repeat(200)}\u3000")) ) - // Cases with `sdk` expectations in DataDog/ffe-system-test-data@e8777633 - // flag-selection/selection-vectors.json that the tests above do not already cover. + // Every case with an `sdk` expectation in DataDog/ffe-system-test-data@e8777633 + // flag-selection/selection-vectors.json, in file order and under the same name. An empty + // expected selection is no selection on Android. @JvmStatic fun sharedVectorSelections() = listOf( Arguments.of( - "leading star", - keyPatterns("*-v2"), - selected(emptyList(), listOf("*-v2")) + "no parameters selects every flag", + FlagSelection(emptyList(), emptyList()), + FlagSelectionResult.None + ), + Arguments.of( + "tag selects flags having any of the tags", + FlagSelection(listOf("team:search", "mobile"), emptyList()), + selected(listOf("mobile", "team:search"), emptyList()) + ), + Arguments.of( + "tags and key patterns are combined with OR", + FlagSelection(listOf("mobile"), listOf("checkout.*")), + selected(listOf("mobile"), listOf("checkout.*")) + ), + Arguments.of( + "key pattern must match the whole key", + FlagSelection(emptyList(), listOf("checkout")), + selected(emptyList(), listOf("checkout")) + ), + Arguments.of( + "key pattern without a star selects exactly that key", + FlagSelection(emptyList(), listOf("checkout.new-flow")), + selected(emptyList(), listOf("checkout.new-flow")) ), Arguments.of( "star matches an empty run and multiple stars are allowed", - keyPatterns("*search*auto*complete*"), + FlagSelection(emptyList(), listOf("*search*auto*complete*")), selected(emptyList(), listOf("*search*auto*complete*")) ), + Arguments.of( + "leading star", + FlagSelection(emptyList(), listOf("*-v2")), + selected(emptyList(), listOf("*-v2")) + ), Arguments.of( "lone star matches every key", - keyPatterns("*"), + FlagSelection(emptyList(), listOf("*")), selected(emptyList(), listOf("*")) ), Arguments.of( "question mark and brackets are literal", - keyPatterns("search.ranking-v?", "[a]*", "{a,b}"), + FlagSelection(emptyList(), listOf("search.ranking-v?", "[a]*", "{a,b}")), selected(emptyList(), listOf("[a]*", "search.ranking-v?", "{a,b}")) ), Arguments.of( "braces are literal even when an alternative names a flag", - keyPatterns("{mobile.dark-mode,missing}"), + FlagSelection(emptyList(), listOf("{mobile.dark-mode,missing}")), selected(emptyList(), listOf("{mobile.dark-mode,missing}")) ), Arguments.of( "tags are never globs", - tags("team:*"), + FlagSelection(listOf("team:*"), emptyList()), selected(listOf("team:*"), emptyList()) ), Arguments.of( "tags match case-sensitively", - tags("Team:Payments", "MOBILE"), + FlagSelection(listOf("Team:Payments", "MOBILE"), emptyList()), selected(listOf("MOBILE", "Team:Payments"), emptyList()) ), + Arguments.of( + "key patterns match case-sensitively", + FlagSelection(emptyList(), listOf("CHECKOUT.*", "Mobile.Dark-Mode")), + selected(emptyList(), listOf("CHECKOUT.*", "Mobile.Dark-Mode")) + ), + Arguments.of( + "values are trimmed, empties dropped and duplicates removed", + FlagSelection(listOf(" mobile ", "mobile", "", " "), emptyList()), + selected(listOf("mobile"), emptyList()) + ), + Arguments.of( + "lists that are empty after normalization select every flag", + FlagSelection(listOf("", " "), listOf(" ")), + FlagSelectionResult.None + ), + Arguments.of( + "values are trimmed of Unicode white space", + FlagSelection(listOf("\u00A0mobile\u3000", "\u0085ui\u2028", "\u0009 team:search \u000A"), emptyList()), + selected(listOf("mobile", "team:search", "ui"), emptyList()) + ), Arguments.of( "characters outside Unicode White_Space are not trimmed", - tags("\uFEFFmobile", "mobile\u200B"), + FlagSelection(listOf("\uFEFFmobile", "mobile\u200B"), emptyList()), selected(listOf("mobile\u200B", "\uFEFFmobile"), emptyList()) ), Arguments.of( "values sort by UTF-8 bytes", - tags(GRINNING_FACE, "\uFF5E", "a", "Z"), + FlagSelection(listOf(GRINNING_FACE, "\uFF5E", "a", "Z"), emptyList()), selected(listOf("Z", "a", "\uFF5E", GRINNING_FACE), emptyList()) ), + Arguments.of( + "partial match serves only the matching flags", + FlagSelection(listOf("mobile", "missing"), listOf("nothing.*")), + selected(listOf("missing", "mobile"), listOf("nothing.*")) + ), + Arguments.of( + "31 values in one list are allowed", + FlagSelection(numbered("t", 31), emptyList()), + selected(numbered("t", 31), emptyList()) + ), + Arguments.of( + "32 values in one list are rejected", + FlagSelection(emptyList(), numbered("k", 32)), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), Arguments.of( "31 values in each list are allowed", FlagSelection(numbered("t", 31), numbered("p", 31)), selected(numbered("t", 31), numbered("p", 31)) ), + Arguments.of( + "32 values in one list are rejected even when the other is within the limit", + FlagSelection(numbered("t", 31), numbered("p", 32)), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), Arguments.of( "duplicates do not count toward the limit", - FlagSelection(numbered("t", 31) + "t01" + " t02 ", emptyList()), + FlagSelection(numbered("t", 31) + listOf("t01", " t02 "), emptyList()), selected(numbered("t", 31), emptyList()) ), + Arguments.of( + "key pattern of 200 bytes is allowed", + FlagSelection(emptyList(), listOf("*" + "x".repeat(199))), + selected(emptyList(), listOf("*" + "x".repeat(199))) + ), + Arguments.of( + "key pattern of 201 bytes is invalid", + FlagSelection(emptyList(), listOf("*" + "x".repeat(200))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ), + Arguments.of( + "key pattern length is measured after trimming", + FlagSelection(emptyList(), listOf(" *" + "x".repeat(199) + " ")), + selected(emptyList(), listOf("*" + "x".repeat(199))) + ), Arguments.of( "key pattern length counts UTF-8 bytes: 100 two-byte characters are allowed", - keyPatterns("\u00E9".repeat(100)), + FlagSelection(emptyList(), listOf("\u00E9".repeat(100))), selected(emptyList(), listOf("\u00E9".repeat(100))) ), + Arguments.of( + "key pattern length counts UTF-8 bytes: 201 bytes are invalid", + FlagSelection(emptyList(), listOf("x" + "\u00E9".repeat(100))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ), + Arguments.of( + "tag of 200 bytes is allowed", + FlagSelection(listOf("a".repeat(200)), emptyList()), + selected(listOf("a".repeat(200)), emptyList()) + ), + Arguments.of( + "tag of 201 bytes is invalid", + FlagSelection(listOf("a".repeat(201)), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) + ), Arguments.of( "tag length is measured after trimming", - tags(" ${"a".repeat(200)} "), + FlagSelection(listOf(" " + "a".repeat(200) + " "), emptyList()), selected(listOf("a".repeat(200)), emptyList()) ), Arguments.of( - "32 values in one list are rejected even when the other is within the limit", - FlagSelection(numbered("t", 31), numbered("p", 32)), - FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + "tag length counts UTF-8 bytes: 100 two-byte characters are allowed", + FlagSelection(listOf("\u00E9".repeat(100)), emptyList()), + selected(listOf("\u00E9".repeat(100)), emptyList()) ), Arguments.of( - "key pattern length counts UTF-8 bytes: 201 bytes are invalid", - keyPatterns("x" + "\u00E9".repeat(100)), - FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + "tag length counts UTF-8 bytes: 201 bytes are invalid", + FlagSelection(listOf("x" + "\u00E9".repeat(100)), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) ), Arguments.of( "a long tag invalidates the selection even when other tags match", - tags("mobile", "a".repeat(201)), + FlagSelection(listOf("mobile", "a".repeat(201)), emptyList()), FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) ), + Arguments.of( + "too_many_values takes precedence over invalid_key_pattern", + FlagSelection(numbered("t", 32), listOf("*" + "x".repeat(200))), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), Arguments.of( "too_many_values takes precedence over invalid_tag", FlagSelection(numbered("t", 31) + "a".repeat(201), emptyList()), FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "invalid_key_pattern takes precedence over invalid_tag", + FlagSelection(listOf("a".repeat(201)), listOf("a".repeat(201))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) ) ) From f80f2c3f747e6bf52b448e591aa8db78d446c9e4 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:53:33 -0400 Subject: [PATCH 15/44] FFL-3459: Test the ignored header name without case sensitivity --- .../PrecomputedAssignmentsDownloaderTest.kt | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index dd453c36a5..f6caeb29b7 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -135,6 +135,27 @@ internal class PrecomputedAssignmentsDownloaderTest { assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) } + @Test + fun `M return the ignored reason W readPrecomputedFlags() { selection sent, lowercase header name }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + stubSuccessfulCall( + fakeResponseBody, + fakeUrl, + ignoredReason = "too_many_values", + headerName = "dd-flag-selection-ignored" + ) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, "too_many_values")) + } + @Test fun `M return no ignored reason W readPrecomputedFlags() { selection sent, empty ignored header }`( @StringForgery fakeResponseBody: String, @@ -452,11 +473,16 @@ internal class PrecomputedAssignmentsDownloaderTest { // region Helper methods - private fun stubSuccessfulCall(body: String, url: String, ignoredReason: String) { + private fun stubSuccessfulCall( + body: String, + url: String, + ignoredReason: String, + headerName: String = "DD-Flag-Selection-Ignored" + ) { val fakeRequest = Request.Builder().url(url).build() val fakeResponse = createSuccessfulResponse(body, url) .newBuilder() - .header("DD-Flag-Selection-Ignored", ignoredReason) + .header(headerName, ignoredReason) .build() whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics)) .doReturn(fakeRequest) From f79b4d776942a788e4d22f58fd6c9b5e1ceaad57 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:53:33 -0400 Subject: [PATCH 16/44] FFL-3459: Test equivalent selections and empty configurations in the repository --- .../repository/DefaultFlagsRepositoryTest.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 7b624d4c6d..412ba8773c 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -15,6 +15,9 @@ import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionCanonicalizer +import com.datadog.android.flags.internal.selection.FlagSelectionResult import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import com.datadog.android.flags.utils.forge.ForgeConfigurator @@ -529,6 +532,33 @@ internal class DefaultFlagsRepositoryTest { assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() } + @Test + fun `M install persisted state W initial disk read completes { equivalent configured selection }`(forge: Forge) { + // Given + val context = forge.getForgery() + val flagKey = forge.anAlphabeticalString() + val persistedSelection = canonical(FlagSelection(tags = listOf("checkout"), keyPatterns = null)) + val configuredSelection = canonical(FlagSelection(tags = listOf(" checkout", "checkout"), keyPatterns = null)) + val callback = preparePersistenceLoad(forge, configuredSelection) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry( + context, + mapOf(flagKey to forge.getForgery()), + forge.aLong(), + flagSelection = persistedSelection + ) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) + assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() + } + @Test fun `M persist the configured flag selection W setFlagsAndContext()`(forge: Forge) { // Given @@ -550,6 +580,19 @@ internal class DefaultFlagsRepositoryTest { assertThat(entry.firstValue.flagSelection).isEqualTo(flagSelection) } + @Test + fun `M return no flag with the context W getPrecomputedFlagWithContext() { empty flags map }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, emptyMap()) + + // When + val result = testedRepository.getPrecomputedFlagWithContext(forge.anAlphabeticalString()) + + // Then + assertThat(result).isEqualTo(null to context) + } + @Test fun `M return true W hasLoadedFlagsForContext() { empty flags map for the same context }`(forge: Forge) { // Given @@ -572,6 +615,9 @@ internal class DefaultFlagsRepositoryTest { ).isFalse() } + private fun canonical(selection: FlagSelection): CanonicalFlagSelection = + checkNotNull(FlagSelectionCanonicalizer.canonicalize(selection) as? FlagSelectionResult.Selected).selection + private fun preparePersistenceLoad( forge: Forge, flagSelection: CanonicalFlagSelection? = null From ca5b0801a31fa9392eba5096dcf1f4be788be1fb Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:53:34 -0400 Subject: [PATCH 17/44] FFL-3459: Test selections with no values after canonicalization --- .../flags/internal/FlagsFeatureTest.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index c7c11f2e99..24f4e258e2 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -23,12 +23,17 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.verify @@ -118,6 +123,39 @@ internal class FlagsFeatureTest { assertThat(feature.precomputedRequestFactory.sendsFlagSelection).isTrue() } + @ParameterizedTest(name = "{0}") + @MethodSource("selectionsWithoutValues") + fun `M send no selection and log nothing W onInitialize() { no values after canonicalization }`( + description: String, + tags: List?, + keyPatterns: List? + ) { + // Given + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = tags, keyPatterns = keyPatterns) + .build() + ) + + // When + feature.onInitialize(mockContext) + + // Then + assertThat(feature.flagSelection).describedAs(description).isNull() + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).describedAs(description).isFalse() + val messages = argumentCaptor<() -> String>() + verify(mockInternalLogger, atLeast(0)).log( + any(), + any(), + messages.capture(), + anyOrNull(), + any(), + anyOrNull() + ) + assertThat(messages.allValues.map { it() }).noneMatch { "flagSelection" in it || "Flag selection" in it } + } + @Test fun `M log the dropped flag selection W onInitialize() { selection over a limit }`() { // Given @@ -440,4 +478,13 @@ internal class FlagsFeatureTest { } // endregion + + companion object { + @JvmStatic + fun selectionsWithoutValues() = listOf( + Arguments.of("empty lists", emptyList(), emptyList()), + Arguments.of("whitespace-only values", listOf(" ", "\u3000"), listOf("")), + Arguments.of("null lists", null, null) + ) + } } From db50cef33c1250fea890be327dfa2725cbd51d9c Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 10:55:30 -0400 Subject: [PATCH 18/44] FFL-3459: Drop a selection with non-string elements instead of throwing A Java caller can pass a raw list, so the canonicalizer checks each element's type. A non-string element is malformed_selection, as in the browser SDK, instead of a ClassCastException from Flags.enable. --- .../flags/internal/selection/FlagSelection.kt | 5 +++-- .../selection/FlagSelectionCanonicalizerTest.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index 998378766b..06f657a384 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -111,8 +111,9 @@ internal object FlagSelectionCanonicalizer { private fun String.utf8Size(): Int = toByteArray(Charsets.UTF_8).size - private fun List.wellFormedOrNull(): List? = - if (all { it != null && !it.hasUnpairedSurrogate() }) filterIsInstance() else null + // A Java caller can pass a raw list, so check each element's type instead of trusting the generic type. + private fun List<*>.wellFormedOrNull(): List? = + if (all { it is String && !it.hasUnpairedSurrogate() }) filterIsInstance() else null private fun String.hasUnpairedSurrogate(): Boolean { var index = 0 diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index 3c8617f9b8..e2f4109c4b 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -176,6 +176,10 @@ internal class FlagSelectionCanonicalizerTest { private fun values(count: Int) = List(count) { "value-$it" } + // Java callers can pass a raw list, so the elements are not checked to be strings. + @Suppress("UNCHECKED_CAST") + private fun uncheckedStrings(vararg values: Any?) = values.toList() as List + private fun tags(vararg tags: String?) = FlagSelection(tags = tags.toList(), keyPatterns = emptyList()) private fun keyPatterns(vararg keyPatterns: String?) = @@ -423,6 +427,16 @@ internal class FlagSelectionCanonicalizerTest { ), Arguments.of("null tag", tags("mobile", null), FlagSelectionError.MALFORMED_SELECTION), Arguments.of("null key pattern", keyPatterns(null), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of( + "non-string tag from an unchecked cast", + FlagSelection(tags = uncheckedStrings("mobile", 1), keyPatterns = emptyList()), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "non-string key pattern from an unchecked cast", + FlagSelection(tags = emptyList(), keyPatterns = uncheckedStrings(true)), + FlagSelectionError.MALFORMED_SELECTION + ), Arguments.of( "malformed before too many values", FlagSelection(values(32) + null, emptyList()), From 765454c72d8d4245d007f2b3d1c4d25dfd3f4785 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 11:12:18 -0400 Subject: [PATCH 19/44] FFL-3459: Remove the region markers from the flag selection client test --- .../test/kotlin/com/datadog/android/flags/FlagsClientTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt index f476f086d6..6328f1efeb 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt @@ -131,8 +131,6 @@ internal class FlagsClientTest { flagsFeature?.clearClients() } - // region Flag Selection Wiring - @Test fun `M key persisted flags by the configured selection W createInternal() { flag selection }`( @StringForgery fakeFlagKey: String, @@ -194,8 +192,6 @@ internal class FlagsClientTest { assertThat(client.getFlagAssignmentsSnapshot()).containsOnlyKeys(fakeFlagKey) } - // endregion - // region Static Flag Resolution Methods @Test From ff7c44462bc05eea64b02e9ec2782593b199f640 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 11:12:19 -0400 Subject: [PATCH 20/44] FFL-3459: Verify the absent selection logs with verifyLog --- .../flags/internal/FlagsFeatureTest.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index 24f4e258e2..6f62a3f056 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -13,6 +13,7 @@ import com.datadog.android.api.feature.Feature.Companion.FLAGS_FEATURE_NAME import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.flags.FlagsConfiguration import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionError import com.datadog.android.flags.internal.storage.ExposureEventRecordWriter import com.datadog.android.flags.internal.storage.NoOpRecordWriter import com.datadog.android.utils.verifyLog @@ -31,11 +32,10 @@ import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.atLeast import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -144,16 +144,21 @@ internal class FlagsFeatureTest { // Then assertThat(feature.flagSelection).describedAs(description).isNull() assertThat(feature.precomputedRequestFactory.sendsFlagSelection).describedAs(description).isFalse() - val messages = argumentCaptor<() -> String>() - verify(mockInternalLogger, atLeast(0)).log( - any(), - any(), - messages.capture(), - anyOrNull(), - any(), - anyOrNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "flagSelection" in it }, + mode = never() ) - assertThat(messages.allValues.map { it() }).noneMatch { "flagSelection" in it || "Flag selection" in it } + FlagSelectionError.entries.forEach { + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection dropped", + mode = never(), + additionalProperties = mapOf("reason" to it.reason) + ) + } } @Test From d29e8e3d1ea70c088ff40c15f7195d81dab7917f Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 11:13:39 -0400 Subject: [PATCH 21/44] FFL-3459: Cover every pair of the selection error order --- .../selection/FlagSelectionCanonicalizerTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index e2f4109c4b..5c9f895ef8 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -442,6 +442,16 @@ internal class FlagSelectionCanonicalizerTest { FlagSelection(values(32) + null, emptyList()), FlagSelectionError.MALFORMED_SELECTION ), + Arguments.of( + "malformed before invalid key pattern", + FlagSelection(listOf("a\uD83D"), listOf("a".repeat(201))), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "malformed before invalid tag", + FlagSelection(listOf("a".repeat(201)), listOf(null)), + FlagSelectionError.MALFORMED_SELECTION + ), Arguments.of( "too many values before invalid key pattern", FlagSelection(values(32), listOf("a".repeat(201))), From f68612f3e41cfc959bc2ee8bdaed1fe8ee449a91 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 13:00:57 -0400 Subject: [PATCH 22/44] FFL-3459: Keep selected flag state out of the key older SDKs read SDK versions without flag selection read flags-state- with any version and ignore the flagSelection field, so a rollback served a selected subset as the full configuration. Selected state now uses flags-selection-state-, and creating an instance removes the other key so one state remains per instance. Unselected state stays where older SDKs read it. --- .../persistence/FlagsPersistenceManager.kt | 12 ++- .../repository/DefaultFlagsRepository.kt | 2 +- .../repository/DefaultFlagsRepositoryTest.kt | 95 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt index 12c750724d..bdf5b81bff 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt @@ -20,21 +20,28 @@ import com.datadog.android.flags.model.EvaluationContext internal class FlagsPersistenceManager( private val dataStore: DataStoreHandler, instanceName: String, + private val flagSelection: CanonicalFlagSelection?, private val internalLogger: InternalLogger, onStateLoaded: (FlagsStateEntry?) -> Unit ) { private val serializer = FlagsStateSerializer(internalLogger) private val deserializer = FlagsStateDeserializer(internalLogger) - private val flagsStateKey: String = "$FLAGS_STATE_KEY_PREFIX-$instanceName" + + // SDK versions without flag selection read only the unselected key and ignore the flagSelection field, + // so selected state uses its own key and is never read by them as a full configuration. + private val unselectedStateKey = "$FLAGS_STATE_KEY_PREFIX-$instanceName" + private val selectedStateKey = "$FLAGS_SELECTION_STATE_KEY_PREFIX-$instanceName" + private val flagsStateKey: String = if (flagSelection == null) unselectedStateKey else selectedStateKey init { + // One persisted state per instance: changing to or from a selection discards the other key's state. + dataStore.removeValue(if (flagSelection == null) selectedStateKey else unselectedStateKey) loadFlagsState(onStateLoaded) } internal fun saveFlagsState( context: EvaluationContext, flags: Map, - flagSelection: CanonicalFlagSelection?, currentTimestamp: Long, callback: DataStoreWriteCallback? = null, obfuscation: FlagKeyObfuscation? = null @@ -79,5 +86,6 @@ internal class FlagsPersistenceManager( companion object { private const val FLAGS_STATE_KEY_PREFIX = "flags-state" + private const val FLAGS_SELECTION_STATE_KEY_PREFIX = "flags-selection-state" } } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt index a0a92a6329..0f3d4515af 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt @@ -52,6 +52,7 @@ internal class DefaultFlagsRepository( private val persistenceManager = FlagsPersistenceManager( dataStore = dataStore, instanceName = instanceName, + flagSelection = flagSelection, internalLogger = internalLogger ) { loadedState -> // State fetched with a different selection holds a different set of flags. @@ -89,7 +90,6 @@ internal class DefaultFlagsRepository( persistenceManager.saveFlagsState( context = context, flags = flags, - flagSelection = flagSelection, currentTimestamp = featureSdkCore.timeProvider.getDeviceTimestampMillis(), callback = object : DataStoreWriteCallback { override fun onSuccess() { diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 412ba8773c..543e2fb8ac 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -22,6 +22,7 @@ import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import com.datadog.android.flags.utils.forge.ForgeConfigurator import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -40,6 +41,7 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -559,6 +561,99 @@ internal class DefaultFlagsRepositoryTest { assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() } + @Test + fun `M persist the state under the selection key W setFlagsAndContext() { flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String, + forge: Forge + ) { + // Given + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + fakeInstanceName, + mockDataStore, + CanonicalFlagSelection(listOf("mobile"), emptyList()) + ) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + verify(mockDataStore).setValue( + key = eq("flags-selection-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + verify(mockDataStore, never()).setValue( + key = eq("flags-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + } + + @Test + fun `M persist the state under the unselected key W setFlagsAndContext() { no flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String, + forge: Forge + ) { + // Given + testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, fakeInstanceName, mockDataStore, null) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + verify(mockDataStore).setValue( + key = eq("flags-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + } + + @Test + fun `M read the selection key and remove the unselected state W init { flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String + ) { + // When + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + fakeInstanceName, + mockDataStore, + CanonicalFlagSelection(listOf("mobile"), emptyList()) + ) + + // Then + verify(mockDataStore).value( + key = eq("flags-selection-state-$fakeInstanceName"), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + verify(mockDataStore).removeValue(eq("flags-state-$fakeInstanceName"), anyOrNull()) + } + + @Test + fun `M read the unselected key and remove the selected state W init { no flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String + ) { + // When + testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, fakeInstanceName, mockDataStore, null) + + // Then + verify(mockDataStore).value( + key = eq("flags-state-$fakeInstanceName"), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + verify(mockDataStore).removeValue(eq("flags-selection-state-$fakeInstanceName"), anyOrNull()) + } + @Test fun `M persist the configured flag selection W setFlagsAndContext()`(forge: Forge) { // Given From f10b90f815b646ba1780dafd55569642686148a4 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 13:28:47 -0400 Subject: [PATCH 23/44] FFL-3459: Report only known DD-Flag-Selection-Ignored reasons The header value reaches USER logs and telemetry, so keep the five reasons the server sends and report any other non-empty value as unknown. The selection is still treated as ignored and the full configuration is still installed. --- .../net/PrecomputedAssignmentsDownloader.kt | 5 +- .../net/PrecomputedAssignmentsReader.kt | 2 +- .../flags/internal/selection/FlagSelection.kt | 22 ++++++++ .../evaluation/EvaluationsManagerTest.kt | 4 +- .../PrecomputedAssignmentsDownloaderTest.kt | 33 +++++++++++- .../FlagSelectionIgnoredReasonTest.kt | 52 +++++++++++++++++++ 6 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index edd9b3b80c..476a753b4e 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.DatadogContext import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Diagnostics +import com.datadog.android.flags.internal.selection.FlagSelectionIgnoredReason import com.datadog.android.flags.model.EvaluationContext import okhttp3.Call import okhttp3.Request @@ -58,7 +59,9 @@ internal class PrecomputedAssignmentsDownloader( PrecomputedAssignmentsResponse( body = body, flagSelectionIgnoredReason = if (requestFactory.sendsFlagSelection) { - response.header(HEADER_FLAG_SELECTION_IGNORED)?.takeIf { it.isNotEmpty() } + response.header(HEADER_FLAG_SELECTION_IGNORED) + ?.takeIf { it.isNotEmpty() } + ?.let(FlagSelectionIgnoredReason::sanitize) } else { null } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt index 42666a17e2..0f5c7c7615 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt @@ -23,7 +23,7 @@ internal interface PrecomputedAssignmentsReader { } /** - * [flagSelectionIgnoredReason] is the `DD-Flag-Selection-Ignored` header value, or null when absent. + * [flagSelectionIgnoredReason] is the sanitized `DD-Flag-Selection-Ignored` reason, or null when the header is absent. */ internal data class PrecomputedAssignmentsResponse( val body: String, diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index 06f657a384..eac8cfbc10 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -45,6 +45,28 @@ internal enum class FlagSelectionError(val reason: String, val limit: String) { ) } +/** + * Sanitizes the `DD-Flag-Selection-Ignored` response header, so a raw header value never reaches logs or telemetry. + * A known reason is kept, and any other non-empty value becomes [UNKNOWN]. + */ +internal object FlagSelectionIgnoredReason { + + internal const val UNKNOWN = "unknown" + + // The longest known reason has 19 characters. The limit bounds the work on an oversized header. + private const val MAX_LENGTH = 32 + + private val KNOWN_REASONS = setOf( + FlagSelectionError.MALFORMED_SELECTION.reason, + FlagSelectionError.TOO_MANY_VALUES.reason, + FlagSelectionError.INVALID_KEY_PATTERN.reason, + FlagSelectionError.INVALID_TAG.reason, + "tag_load_failed" + ) + + fun sanitize(value: String): String = if (value.length <= MAX_LENGTH && value in KNOWN_REASONS) value else UNKNOWN +} + internal sealed interface FlagSelectionResult { object None : FlagSelectionResult data class Selected(val selection: CanonicalFlagSelection) : FlagSelectionResult diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index a63c5a8eb0..64037a78e5 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -1644,8 +1644,8 @@ internal class EvaluationsManagerTest { // endregion @ParameterizedTest - @ValueSource(strings = ["too_many_values", "tag_load_failed", "Some Future-Reason"]) - fun `M log the reason verbatim and install W updateEvaluationsForContext() { flag selection ignored }`( + @ValueSource(strings = ["too_many_values", "tag_load_failed", "unknown"]) + fun `M log the reason and install W updateEvaluationsForContext() { flag selection ignored }`( fakeIgnoredReason: String ) { // Given diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index f6caeb29b7..1348a73465 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -18,6 +18,7 @@ import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import okhttp3.Call +import okhttp3.Headers import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Request @@ -117,8 +118,10 @@ internal class PrecomputedAssignmentsDownloaderTest { } @ParameterizedTest - @ValueSource(strings = ["too_many_values", "tag_load_failed", "Some Future-Reason"]) - fun `M return the header value verbatim W readPrecomputedFlags() { selection sent, ignored header }`( + @ValueSource( + strings = ["malformed_selection", "too_many_values", "invalid_key_pattern", "invalid_tag", "tag_load_failed"] + ) + fun `M return the known reason W readPrecomputedFlags() { selection sent, ignored header }`( fakeIgnoredReason: String, forge: Forge ) { @@ -135,6 +138,32 @@ internal class PrecomputedAssignmentsDownloaderTest { assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) } + @ParameterizedTest + @ValueSource(strings = ["Some Future-Reason", "Too_Many_Values", "too_many_values\u0001", "too_many_values\u00E9"]) + fun `M return the fallback reason W readPrecomputedFlags() { selection sent, unknown ignored header }`( + fakeIgnoredReason: String, + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + val fakeRequest = Request.Builder().url(fakeUrl).build() + val fakeResponse = createSuccessfulResponse(fakeResponseBody, fakeUrl) + .newBuilder() + .headers(Headers.Builder().addUnsafeNonAscii("DD-Flag-Selection-Ignored", fakeIgnoredReason).build()) + .build() + whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics)) + .doReturn(fakeRequest) + whenever(mockCallFactory.newCall(fakeRequest)).doReturn(mockCall) + whenever(mockCall.execute()).doReturn(fakeResponse) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, "unknown")) + } + @Test fun `M return the ignored reason W readPrecomputedFlags() { selection sent, lowercase header name }`( @StringForgery fakeResponseBody: String, diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt new file mode 100644 index 0000000000..1baefa9c97 --- /dev/null +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +internal class FlagSelectionIgnoredReasonTest { + + @ParameterizedTest + @ValueSource( + strings = ["malformed_selection", "too_many_values", "invalid_key_pattern", "invalid_tag", "tag_load_failed"] + ) + fun `M keep the reason W sanitize() { known reason }`(reason: String) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo(reason) + } + + @ParameterizedTest + @ValueSource( + strings = [ + "Some Future-Reason", + "unknown_reason", + "Too_Many_Values", + "TAG_LOAD_FAILED", + " too_many_values", + "too_many_values\u0001", + "invalid_tag\n", + "too_many_values\u00E9", + "\uD83D\uDE00" + ] + ) + fun `M return the fallback W sanitize() { unknown reason }`(reason: String) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo("unknown") + } + + @ParameterizedTest + @ValueSource(ints = [33, 200, 10_000]) + fun `M return the fallback W sanitize() { value longer than 32 characters }`(length: Int) { + // Given + val reason = "too_many_values".padEnd(length, '_') + + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo("unknown") + } +} From 344f0f8d4a3cc22f68df704a4da941aa3d3ccede Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Wed, 28 Oct 2026 13:28:49 -0400 Subject: [PATCH 24/44] FFL-3459: Document unknown ignored reasons and the proxy header --- features/dd-sdk-android-flags/README.md | 1 + .../main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index d2b279ea7a..e5b93c77f4 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -110,6 +110,7 @@ val flagsConfig = FlagsConfiguration.Builder() - Each list accepts at most 31 values, and each value is at most 200 UTF-8 bytes. The SDK does not send a selection that is over a limit. It logs an error and loads the full flag configuration. - When the server applies the selection, evaluating a flag outside it returns the default value with `ErrorCode.FLAG_NOT_FOUND`. - The selection is fixed when you call `Flags.enable`. +- If the server ignores a selection, the SDK loads the full flag configuration and logs an error with the reason. A reason that the SDK does not know is reported as `unknown`. - With a custom flag endpoint, the proxy must forward the `DD-Flag-Selection-Ignored` response header. Otherwise, the SDK cannot report a selection that the server ignored. ## Use the Feature Flags SDK diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 5c526f0c7b..49d5c21284 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -188,6 +188,8 @@ data class FlagsConfiguration internal constructor( * UTF-16 surrogate, is not sent, and the full flag configuration is loaded. The server may also ignore a very * large selection (`too_many_values`), in which case the full flag configuration is loaded. Servers that do * not support flag selection, or have it disabled, return the full flag configuration without a warning. + * A server reason that the SDK does not know is reported as `unknown`. With [useCustomFlagEndpoint], the proxy + * must forward the `DD-Flag-Selection-Ignored` response header for an ignored selection to be reported. * * When the server applies the selection, flags outside it are not loaded, so evaluating them returns the * default value with [ErrorCode.FLAG_NOT_FOUND]. From 99e58b4d2e74287b34e9fdd6e65b1c15287fb646 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 29 Oct 2026 16:53:23 -0400 Subject: [PATCH 25/44] FFL-3459: Rename flag presence checks to configuration checks hasFlags() and hasLoadedFlagsForContext() report whether a configuration is installed, including a valid one with zero flags. Name them hasConfiguration() and hasConfigurationForContext() so call sites read as intended. --- .../internal/evaluation/EvaluationsManager.kt | 6 +-- .../repository/DefaultFlagsRepository.kt | 4 +- .../internal/repository/FlagsRepository.kt | 4 +- .../repository/NoOpFlagsRepository.kt | 4 +- .../evaluation/EvaluationsManagerTest.kt | 24 ++++----- .../DefaultFlagsRepositoryObfuscationTest.kt | 4 +- .../repository/DefaultFlagsRepositoryTest.kt | 50 +++++++++---------- .../repository/FirstFlagsInstallationTest.kt | 2 +- 8 files changed, 49 insertions(+), 49 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 82f16e413f..4a65ab04a0 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -166,9 +166,9 @@ internal class EvaluationsManager( { "Processing evaluation context: ${context.targetingKey}" } ) - val hadFlags = flagsRepository.hasFlags() + val hadConfiguration = flagsRepository.hasConfiguration() matchingCachedAssignments.set( - hadFlags && flagsRepository.getEvaluationContext() == context + hadConfiguration && flagsRepository.getEvaluationContext() == context ) val response = datadogContext?.let { assignmentsReader.readPrecomputedFlags(context, it, diagnostics) } val assignments = response?.let(::mapResponse) @@ -264,7 +264,7 @@ internal class EvaluationsManager( val currentState = flagStateManager.getCurrentState() if (currentState != FlagsClientState.Ready && currentState != FlagsClientState.Stale) { val hasMatchingCachedAssignments = matchingCachedAssignments.get() || - flagsRepository.hasLoadedFlagsForContext(context) + flagsRepository.hasConfigurationForContext(context) val timeoutState = if (hasMatchingCachedAssignments) { FlagsClientState.Stale } else { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt index 0f3d4515af..aae178e5b3 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt @@ -161,12 +161,12 @@ internal class DefaultFlagsRepository( return readableState?.context } - override fun hasFlags(): Boolean { + override fun hasConfiguration(): Boolean { waitForPersistenceLoad() return readableState != null } - override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean = + override fun hasConfigurationForContext(context: EvaluationContext): Boolean = readableState?.context == context override fun getPrecomputedFlagWithContext(key: String): Pair? { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt index 8fbcb4d160..f5eda7aafb 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt @@ -33,9 +33,9 @@ internal interface FlagsRepository { val firstFlags: FirstFlagsLatch /** True once a configuration is installed, including a valid configuration with zero flags. */ - fun hasFlags(): Boolean + fun hasConfiguration(): Boolean /** True when the installed configuration is for [context], including a valid configuration with zero flags. */ - fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean + fun hasConfigurationForContext(context: EvaluationContext): Boolean fun getFlagsSnapshot(): Map } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt index c367dd145e..933e2b61db 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt @@ -31,9 +31,9 @@ internal class NoOpFlagsRepository : FlagsRepository { override val firstFlags = FirstFlagsLatch() - override fun hasFlags(): Boolean = false + override fun hasConfiguration(): Boolean = false - override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean = false + override fun hasConfigurationForContext(context: EvaluationContext): Boolean = false override fun getFlagsSnapshot(): Map = emptyMap() } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 64037a78e5..d74be78c6e 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -276,7 +276,7 @@ internal class EvaluationsManagerTest { // Then inOrder(mockFlagsRepository, mockAssignmentsDownloader) { - verify(mockFlagsRepository).hasFlags() + verify(mockFlagsRepository).hasConfiguration() verify(mockAssignmentsDownloader).readPrecomputedFlags( eq(EvaluationContext(fakeTargetingKey, emptyMap())), eq(fakeDatadogContext), @@ -518,7 +518,7 @@ internal class EvaluationsManagerTest { // Given val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) // When evaluationsManager.updateEvaluationsForContext(publicContext) @@ -535,7 +535,7 @@ internal class EvaluationsManagerTest { // Given val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(publicContext) // When @@ -554,7 +554,7 @@ internal class EvaluationsManagerTest { val requestedContext = EvaluationContext(fakeTargetingKey, emptyMap()) val cachedContext = EvaluationContext("different-user", emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(cachedContext) whenever( mockAssignmentsDownloader.readPrecomputedFlags( @@ -609,7 +609,7 @@ internal class EvaluationsManagerTest { val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) val mockCallback = mock() - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) // When evaluationsManager.updateEvaluationsForContext(publicContext, callback = mockCallback) @@ -627,7 +627,7 @@ internal class EvaluationsManagerTest { val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) val mockCallback = mock() - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) // When evaluationsManager.updateEvaluationsForContext(publicContext, callback = mockCallback) @@ -736,7 +736,7 @@ internal class EvaluationsManagerTest { callbackError = error } } - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(context) whenever( mockAssignmentsDownloader.readPrecomputedFlags( @@ -777,8 +777,8 @@ internal class EvaluationsManagerTest { onStateChanged = FlagsStateListener::onStateChanged ) ) - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) - whenever(mockFlagsRepository.hasLoadedFlagsForContext(context)).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) + whenever(mockFlagsRepository.hasConfigurationForContext(context)).thenReturn(true) whenever( mockAssignmentsDownloader.readPrecomputedFlags( eq(context), @@ -882,7 +882,7 @@ internal class EvaluationsManagerTest { val callbackCompleted = CountDownLatch(1) var timeoutAction: (() -> Unit)? = null whenever(mockExecutorService.execute(any())).thenAnswer { null } - whenever(mockFlagsRepository.hasFlags()).thenAnswer { + whenever(mockFlagsRepository.hasConfiguration()).thenAnswer { cacheReadStarted.countDown() check(releaseCacheRead.await(1, TimeUnit.SECONDS)) false @@ -1567,7 +1567,7 @@ internal class EvaluationsManagerTest { val failure = argumentCaptor() verify(mockCallback).onFailure(failure.capture()) assertThat(failure.firstValue.message).isEqualTo("Unable to read the feature flags response.") - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() assertThat(testedRepository.getFlagsSnapshot()).isEmpty() } @@ -1592,7 +1592,7 @@ internal class EvaluationsManagerTest { ) // Configure DataStore to fire callback synchronously during DefaultFlagsRepository - // construction, so the persistence latch is counted down before hasFlags() is called. + // construction, so the persistence latch is counted down before hasConfiguration() is called. val mockDataStore = mock() whenever( mockDataStore.value( diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt index 42b9cc67f6..1fedd7eaa7 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt @@ -135,8 +135,8 @@ internal class DefaultFlagsRepositoryObfuscationTest { // Then val readable = !encoded || source == "android" assertThat(result?.variationValue).isEqualTo(if (readable) fakeFlag.variationValue else null) - assertThat(testedRepository.hasFlags()).isEqualTo(readable) - assertThat(testedRepository.hasLoadedFlagsForContext(fakeContext)).isEqualTo(readable) + assertThat(testedRepository.hasConfiguration()).isEqualTo(readable) + assertThat(testedRepository.hasConfigurationForContext(fakeContext)).isEqualTo(readable) assertThat(testedRepository.getEvaluationContext()).isEqualTo(if (readable) fakeContext else null) assertThat(testedRepository.getPrecomputedFlagWithContext(fakeFlagKey) != null).isEqualTo(readable) assertThat(testedRepository.getFlagsSnapshot().isEmpty()).isEqualTo(encoded) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 543e2fb8ac..3fa24ec99e 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -99,7 +99,7 @@ internal class DefaultFlagsRepositoryTest { instanceName = "default" ) - // Setup test fixtures for hasFlags tests + // Setup test fixtures for hasConfiguration tests testContext = EvaluationContext(forge.anAlphabeticalString(), emptyMap()) singleFlagMap = mapOf( @@ -211,7 +211,7 @@ internal class DefaultFlagsRepositoryTest { } @Test - fun `M not wait for persistence W hasLoadedFlagsForContext() { persistence is loading }`(forge: Forge) { + fun `M not wait for persistence W hasConfigurationForContext() { persistence is loading }`(forge: Forge) { // Given doAnswer { null }.whenever(mockDataStore).value( key = any(), @@ -230,7 +230,7 @@ internal class DefaultFlagsRepositoryTest { val completed = CountDownLatch(1) val context = EvaluationContext(forge.anAlphabeticalString(), emptyMap()) val readThread = Thread { - result.set(testedRepository.hasLoadedFlagsForContext(context)) + result.set(testedRepository.hasConfigurationForContext(context)) completed.countDown() }.apply { isDaemon = true } @@ -421,7 +421,7 @@ internal class DefaultFlagsRepositoryTest { // Then assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) assertThat(testedRepository.getFlagsSnapshot()).isEmpty() - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() verify(mockDataStore, never()).setValue( key = any(), data = any(), @@ -488,7 +488,7 @@ internal class DefaultFlagsRepositoryTest { // Then assertThat(testedRepository.getEvaluationContext()).isNull() - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() } @Test @@ -506,7 +506,7 @@ internal class DefaultFlagsRepositoryTest { // Then assertThat(testedRepository.getEvaluationContext()).isNull() - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() } @Test @@ -689,24 +689,24 @@ internal class DefaultFlagsRepositoryTest { } @Test - fun `M return true W hasLoadedFlagsForContext() { empty flags map for the same context }`(forge: Forge) { + fun `M return true W hasConfigurationForContext() { empty flags map for the same context }`(forge: Forge) { // Given val context = forge.getForgery() testedRepository.setFlagsAndContext(context, emptyMap()) // When / Then - assertThat(testedRepository.hasLoadedFlagsForContext(context)).isTrue() + assertThat(testedRepository.hasConfigurationForContext(context)).isTrue() } @Test - fun `M return false W hasLoadedFlagsForContext() { different context }`(forge: Forge) { + fun `M return false W hasConfigurationForContext() { different context }`(forge: Forge) { // Given val context = forge.getForgery() testedRepository.setFlagsAndContext(context, singleFlagMap) // When / Then assertThat( - testedRepository.hasLoadedFlagsForContext(context.copy(targetingKey = context.targetingKey + "-other")) + testedRepository.hasConfigurationForContext(context.copy(targetingKey = context.targetingKey + "-other")) ).isFalse() } @@ -731,16 +731,16 @@ internal class DefaultFlagsRepositoryTest { return callback } - // region hasFlags + // region hasConfiguration @Test - fun `M return false W hasFlags() { no state set }`() { + fun `M return false W hasConfiguration() { no state set }`() { // When + Then - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() } @Test - fun `M return true W hasFlags() { empty flags map }`(forge: Forge) { + fun `M return true W hasConfiguration() { empty flags map }`(forge: Forge) { // Given testedRepository.setFlagsAndContext( EvaluationContext(forge.anAlphabeticalString(), emptyMap()), @@ -748,29 +748,29 @@ internal class DefaultFlagsRepositoryTest { ) // When + Then - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { single flag }`() { + fun `M return true W hasConfiguration() { single flag }`() { // Given testedRepository.setFlagsAndContext(testContext, singleFlagMap) // When + Then - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { multiple flags }`() { + fun `M return true W hasConfiguration() { multiple flags }`() { // Given testedRepository.setFlagsAndContext(testContext, multipleFlagsMap) // When + Then - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { persistence loads with non-empty flags }`() { + fun `M return true W hasConfiguration() { persistence loads with non-empty flags }`() { // Given val persistedEntry = FlagsStateEntry( flags = singleFlagMap, @@ -796,11 +796,11 @@ internal class DefaultFlagsRepositoryTest { ) // When + Then - assertThat(repository.hasFlags()).isTrue() + assertThat(repository.hasConfiguration()).isTrue() } @Test - fun `M return false W hasFlags() { persistence loads with no data }`() { + fun `M return false W hasConfiguration() { persistence loads with no data }`() { // Given doAnswer { it.getArgument>(2) @@ -820,11 +820,11 @@ internal class DefaultFlagsRepositoryTest { ) // When + Then - assertThat(repository.hasFlags()).isFalse() + assertThat(repository.hasConfiguration()).isFalse() } @Test - fun `M return false W hasFlags() { persistence callback never fires within timeout }`() { + fun `M return false W hasConfiguration() { persistence callback never fires within timeout }`() { // Given doAnswer { // Never call the callback @@ -844,7 +844,7 @@ internal class DefaultFlagsRepositoryTest { ) // When - val result = timeoutRepository.hasFlags() + val result = timeoutRepository.hasConfiguration() // Then assertThat(result).isFalse() diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt index b9f4ef0839..858d90ae91 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt @@ -208,7 +208,7 @@ internal class FirstFlagsInstallationTest { assertThat(tested.getPrecomputedFlag("unknown")).isNull() assertThat(tested.getEvaluationContext()).isNull() assertThat(tested.getFlagsSnapshot()).isEmpty() - assertThat(tested.hasFlags()).isFalse() + assertThat(tested.hasConfiguration()).isFalse() assertThat(tested.getPrecomputedFlagWithContext("unknown")).isNull() } if (missing) disk.onSuccess(null) else disk.onFailure() From e47cbe7ae0271b4946901ef28843b97569819c3a Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 29 Oct 2026 16:53:58 -0400 Subject: [PATCH 26/44] FFL-3459: Share the empty flags response in EvaluationsManagerTest --- .../evaluation/EvaluationsManagerTest.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index d74be78c6e..4a2663fa37 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -679,7 +679,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -831,7 +831,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -998,7 +998,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1047,7 +1047,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1080,7 +1080,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenAnswer { checkNotNull(timeoutAction).invoke() PrecomputedAssignments(emptyMap()) @@ -1115,7 +1115,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1146,7 +1146,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = null, @@ -1207,7 +1207,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -1724,7 +1724,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null)) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) // When @@ -1775,7 +1775,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null), null) + .thenReturn(EMPTY_FLAGS_RESPONSE, null) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = EvaluationsManager( sdkCore = mockSdkCore, @@ -1853,5 +1853,6 @@ internal class EvaluationsManagerTest { }}} """.trimIndent() private const val EMPTY_FLAGS_RESPONSE_JSON = "{\"data\": {\"attributes\": {\"flags\": {}}}}" + private val EMPTY_FLAGS_RESPONSE = PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null) } } From 9c970b18dbb241fbabc6e816700a797cefbe3faa Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 29 Oct 2026 17:03:52 -0400 Subject: [PATCH 27/44] FFL-3459: Report an ignored selection only for an accepted response Log the DD-Flag-Selection-Ignored reason and send its telemetry only after the response maps to a configuration and passes the obfuscation filter, just before it is installed. A body that fails to parse, or an encoded configuration this source cannot use, no longer reports a reason for a configuration that was never loaded. --- .../internal/evaluation/EvaluationsManager.kt | 9 +-- .../evaluation/EvaluationsManagerTest.kt | 62 ++++++++++++------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 4a65ab04a0..845d8ac85d 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -22,7 +22,6 @@ import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Sd import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Trigger import com.datadog.android.flags.internal.net.NetworkRequestFailedException import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader -import com.datadog.android.flags.internal.net.PrecomputedAssignmentsResponse import com.datadog.android.flags.internal.repository.FlagsRepository import com.datadog.android.flags.internal.repository.net.PrecomputeMapper import com.datadog.android.flags.internal.selection.logIgnoredFlagSelection @@ -171,9 +170,10 @@ internal class EvaluationsManager( hadConfiguration && flagsRepository.getEvaluationContext() == context ) val response = datadogContext?.let { assignmentsReader.readPrecomputedFlags(context, it, diagnostics) } - val assignments = response?.let(::mapResponse) + val assignments = response?.let { precomputeMapper.map(it.body) } ?.takeIf { it.obfuscation == null || supportsObfuscation } if (assignments != null) { + response.flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } installFlags(context, assignments, initializationCompletion, callback) } else { val message = if (response == null) NETWORK_REQUEST_FAILED_MESSAGE else INVALID_RESPONSE_MESSAGE @@ -218,11 +218,6 @@ internal class EvaluationsManager( } } - private fun mapResponse(response: PrecomputedAssignmentsResponse): PrecomputedAssignments? { - response.flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } - return precomputeMapper.map(response.body) - } - private fun installFlags( context: EvaluationContext, assignments: PrecomputedAssignments, diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 4a2663fa37..e2a04a2821 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -18,6 +18,7 @@ import com.datadog.android.flags.EvaluationContextCallback import com.datadog.android.flags.FlagsInitializationTimeoutException import com.datadog.android.flags.FlagsStateListener import com.datadog.android.flags.internal.FlagsStateManager +import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Diagnostics @@ -1680,7 +1681,7 @@ internal class EvaluationsManagerTest { } @Test - fun `M log the ignored reason and not install W updateEvaluationsForContext() { ignored reason, invalid body }`( + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, invalid body }`( @StringForgery fakeInvalidBody: String ) { // Given @@ -1698,17 +1699,32 @@ internal class EvaluationsManagerTest { evaluationsManager.updateEvaluationsForContext(context) // Then - mockInternalLogger.verifyLog( - InternalLogger.Level.ERROR, - InternalLogger.Target.USER, - "flagSelection was ignored by the server (too_many_values). Loaded the full flag configuration." - ) - mockInternalLogger.verifyLog( - InternalLogger.Level.DEBUG, - InternalLogger.Target.TELEMETRY, - "Flag selection ignored", - additionalProperties = mapOf("reason" to "too_many_values") + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + verify(mockFlagsRepository, never()).setFlagsAndContext(any(), any(), anyOrNull(), any()) + verify(mockFlagsStateManager).updateState(isA()) + } + + @Test + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, unsupported encoding }`() { + // Given + fakeDatadogContext = fakeDatadogContext.copy(source = "react-native") + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ).thenReturn(PrecomputedAssignmentsResponse(ENCODED_RESPONSE_JSON, "too_many_values")) + whenever(mockPrecomputeMapper.map(ENCODED_RESPONSE_JSON)).thenReturn( + PrecomputedAssignments(emptyMap(), FlagKeyObfuscation.fromSalt("000102030405060708090a0b0c0d0e0f")) ) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } verify(mockFlagsRepository, never()).setFlagsAndContext(any(), any(), anyOrNull(), any()) verify(mockFlagsStateManager).updateState(isA()) } @@ -1731,16 +1747,7 @@ internal class EvaluationsManagerTest { evaluationsManager.updateEvaluationsForContext(context) // Then - val messages = argumentCaptor<() -> String>() - verify(mockInternalLogger, atLeast(0)).log( - any(), - any(), - messages.capture(), - anyOrNull(), - any(), - anyOrNull() - ) - assertThat(messages.allValues.map { it() }).noneMatch { "flagSelection" in it || "Flag selection" in it } + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } verify(mockFlagsRepository).setFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull(), any()) } @@ -1840,6 +1847,19 @@ internal class EvaluationsManagerTest { initializationTimeoutScheduler = scheduler ) + private fun capturedLogMessages(): List { + val messages = argumentCaptor<() -> String>() + verify(mockInternalLogger, atLeast(0)).log( + any(), + any(), + messages.capture(), + anyOrNull(), + any(), + anyOrNull() + ) + return messages.allValues.map { it() } + } + companion object { private val ENCODED_RESPONSE_JSON = """ {"data":{"attributes":{ From 75d6d26adfd3f52298ec218b10dea3a113dff1aa Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 29 Oct 2026 17:03:55 -0400 Subject: [PATCH 28/44] FFL-3459: Test that interior Unicode whitespace is kept --- .../FlagSelectionCanonicalizerTest.kt | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt index 5c9f895ef8..5026efe905 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -61,10 +61,8 @@ internal class FlagSelectionCanonicalizerTest { @Test fun `M trim the Unicode White_Space set W canonicalize()`() { // Given - val whiteSpace = "\t\n\u000B\u000C\r \u0085\u00A0\u1680" + - "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000" val selection = FlagSelection( - tags = listOf("${whiteSpace}mobile$whiteSpace"), + tags = listOf("${UNICODE_WHITE_SPACE}mobile$UNICODE_WHITE_SPACE"), keyPatterns = listOf("\uFEFFcheckout\uFEFF", "\u001Cpayment\u001C") ) @@ -82,6 +80,23 @@ internal class FlagSelectionCanonicalizerTest { ) } + @Test + fun `M keep interior Unicode White_Space W canonicalize()`() { + assertThat(UNICODE_WHITE_SPACE).hasSize(25) + UNICODE_WHITE_SPACE.forEach { whiteSpace -> + // Given + val selection = tags("${whiteSpace}a${whiteSpace}b$whiteSpace") + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result) + .describedAs("U+%04X", whiteSpace.code) + .isEqualTo(selected(listOf("a${whiteSpace}b"), emptyList())) + } + } + @Test fun `M drop empty values and dedupe after trimming W canonicalize()`() { // Given @@ -169,6 +184,10 @@ internal class FlagSelectionCanonicalizerTest { companion object { private const val GRINNING_FACE = "\uD83D\uDE00" + // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt lists 25 White_Space code points. + private const val UNICODE_WHITE_SPACE = "\t\n\u000B\u000C\r \u0085\u00A0\u1680" + + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000" + private fun numbered(prefix: String, count: Int) = List(count) { "$prefix%02d".format(Locale.US, it + 1) } private fun selected(tags: List, keyPatterns: List) = From 28e82cdfd612300dcf6a7e2d5d3310f161b1a0e8 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Thu, 29 Oct 2026 17:03:55 -0400 Subject: [PATCH 29/44] FFL-3459: Document that the selection applies to every client and cache --- features/dd-sdk-android-flags/README.md | 2 +- .../kotlin/com/datadog/android/flags/FlagsConfiguration.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index e5b93c77f4..d563539193 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -109,7 +109,7 @@ val flagsConfig = FlagsConfiguration.Builder() - Tags match exactly and are case-sensitive. A key pattern matches the whole flag key, and only `*` is a wildcard. - Each list accepts at most 31 values, and each value is at most 200 UTF-8 bytes. The SDK does not send a selection that is over a limit. It logs an error and loads the full flag configuration. - When the server applies the selection, evaluating a flag outside it returns the default value with `ErrorCode.FLAG_NOT_FOUND`. -- The selection is fixed when you call `Flags.enable`. +- The selection is fixed when the feature is enabled and applies to every `FlagsClient`. Flags cached under a different selection are not loaded at startup; until the next successful fetch, a client starts without cached flags. - If the server ignores a selection, the SDK loads the full flag configuration and logs an error with the reason. A reason that the SDK does not know is reported as `unknown`. - With a custom flag endpoint, the proxy must forward the `DD-Flag-Selection-Ignored` response header. Otherwise, the SDK cannot report a selection that the server ignored. diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 49d5c21284..3ec7b60909 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -194,8 +194,9 @@ data class FlagsConfiguration internal constructor( * When the server applies the selection, flags outside it are not loaded, so evaluating them returns the * default value with [ErrorCode.FLAG_NOT_FOUND]. * - * The selection is fixed when the Flags feature is enabled. Calling this method again replaces the - * previous selection. + * The selection is fixed when the feature is enabled and applies to every [FlagsClient]. Flags cached under a + * different selection are not loaded at startup; until the next successful fetch, a client starts without + * cached flags. Calling this method again replaces the previous selection. * * @param tags The flag tags to load, or null for none. * @param keyPatterns The flag key patterns to load, or null for none. From 00787acfd7d3fbce6b2acd80d3836d9c5d72f5c9 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Fri, 30 Oct 2026 13:34:04 -0400 Subject: [PATCH 30/44] FFL-3459: Send flag selection telemetry once per process The dropped and ignored selection telemetry events now use onlyOnce, so frequent fetches cannot flood telemetry. The USER error log is unchanged and still reports every ignored response. --- .../flags/internal/selection/FlagSelectionLogging.kt | 8 +++++--- .../datadog/android/flags/internal/FlagsFeatureTest.kt | 1 + .../flags/internal/evaluation/EvaluationsManagerTest.kt | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt index 9c84618cb5..a8b9b4efb2 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt @@ -8,7 +8,7 @@ package com.datadog.android.flags.internal.selection import com.datadog.android.api.InternalLogger -// Telemetry carries only the reason, never selection values. +// Telemetry carries only the reason, never selection values, and is sent once per process. private const val TELEMETRY_REASON_KEY = "reason" private const val FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE = "Flag selection dropped" @@ -27,7 +27,8 @@ internal fun InternalLogger.logDroppedFlagSelection(error: FlagSelectionError) { InternalLogger.Level.DEBUG, InternalLogger.Target.TELEMETRY, { FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE }, - additionalProperties = mapOf(TELEMETRY_REASON_KEY to error.reason) + additionalProperties = mapOf(TELEMETRY_REASON_KEY to error.reason), + onlyOnce = true ) } @@ -41,6 +42,7 @@ internal fun InternalLogger.logIgnoredFlagSelection(reason: String) { InternalLogger.Level.DEBUG, InternalLogger.Target.TELEMETRY, { FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE }, - additionalProperties = mapOf(TELEMETRY_REASON_KEY to reason) + additionalProperties = mapOf(TELEMETRY_REASON_KEY to reason), + onlyOnce = true ) } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index 07193a0660..6f16c2a9ce 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -261,6 +261,7 @@ internal class FlagsFeatureTest { InternalLogger.Level.DEBUG, InternalLogger.Target.TELEMETRY, "Flag selection dropped", + onlyOnce = true, additionalProperties = mapOf("reason" to "too_many_values") ) } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 05a1b7dbff..f5a37d43ff 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -1954,6 +1954,7 @@ internal class EvaluationsManagerTest { InternalLogger.Level.DEBUG, InternalLogger.Target.TELEMETRY, "Flag selection ignored", + onlyOnce = true, additionalProperties = mapOf("reason" to fakeIgnoredReason) ) verify(mockFlagsRepository).installFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull()) From 161f308daaefb187090c9b4758aeaa64ad38a596 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Fri, 30 Oct 2026 14:02:28 -0400 Subject: [PATCH 31/44] FFL-3459: Report an ignored selection only when its flags are installed With the experimentation opt-out, a newer context can supersede an accepted response, which is then not installed. Log the ignored reason after installFlags confirms the install, so the message never claims a configuration that was not loaded. --- .../internal/evaluation/EvaluationsManager.kt | 17 ++++++++++--- .../evaluation/EvaluationsManagerTest.kt | 25 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index be4fb1aeb6..92de400034 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -209,8 +209,13 @@ internal class EvaluationsManager( } ?.takeIf { it.obfuscation == null || supportsObfuscation } if (assignments != null) { - response.flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } - installFlags(request, assignments, initializationCompletion, callback) + installFlags( + request, + assignments, + response.flagSelectionIgnoredReason, + initializationCompletion, + callback + ) } else { handleRequestFailure( request, @@ -268,11 +273,12 @@ internal class EvaluationsManager( private fun installFlags( request: ExperimentationOptOut.Request, assignments: PrecomputedAssignments, + flagSelectionIgnoredReason: String?, initializationCompletion: InitializationCompletion?, callback: EvaluationContextCallback? ) { val context = request.context - synchronized(initializationTerminalLock) { + val installed = synchronized(initializationTerminalLock) { val installed = experimentationOptOut.ifCurrent(request) { val installation = flagsRepository.installFlagsAndContext( context, @@ -294,8 +300,13 @@ internal class EvaluationsManager( FlagsContextSupersededException(SUPERSEDED_MESSAGE) ) } + installed } effects.drain() + // A superseded response is not installed, so its ignored reason would describe flags that never loaded. + if (installed) { + flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } + } internalLogger.log( InternalLogger.Level.DEBUG, InternalLogger.Target.MAINTAINER, diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index f5a37d43ff..762d2a38ca 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -2010,6 +2010,31 @@ internal class EvaluationsManagerTest { verify(mockFlagsStateManager).updateState(isA()) } + @ParameterizedTest + @EnumSource(ExperimentationOptOutMode::class, names = ["IMMEDIATE", "FUTURE_EXPERIMENTS_ONLY"]) + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, superseded request }`( + mode: ExperimentationOptOutMode, + forge: Forge + ) { + // Given + val tasks = mutableListOf() + val testedManager = queuedManager(mode, tasks) + val first = EvaluationContext(forge.anAlphabeticalString()) + val firstCallback = mock() + whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(first), any(), any())) doReturn + PrecomputedAssignmentsResponse("response", "too_many_values") + whenever(mockPrecomputeMapper.map(any(), anyOrNull())) doReturn PrecomputedAssignments(emptyMap(), null) + testedManager.updateEvaluationsForContext(first, firstCallback) + testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString())) + + // When + tasks[0].run() + + // Then + verify(firstCallback).onFailure(isA()) + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + } + @Test fun `M not log a flag selection message W updateEvaluationsForContext() { no ignored reason }`() { // Given From 8db220a97b39d25f797fe840014c6ef6f04fd515 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Fri, 30 Oct 2026 14:02:30 -0400 Subject: [PATCH 32/44] FFL-3459: Remove an inline comment from the opt-out key test --- .../flags/internal/repository/DefaultFlagsRepositoryTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 1f6f6dae27..d6bee059e3 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -703,7 +703,6 @@ internal class DefaultFlagsRepositoryTest { callback = anyOrNull(), serializer = any() ) - // Builds without flag selection read only the unselected key of their mode. verify(mockDataStore, never()).setValue( key = eq(siblingKey), data = any(), From 9ce29b14f4c1b0c9672810441440ebceb7d2bd8e Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Mon, 2 Nov 2026 10:23:29 -0500 Subject: [PATCH 33/44] FFL-3459: Clarify the flag selection scope in the README Say that the flag selection is what every FlagsClient shares, and use a function reference for the empty ignored-reason header check. --- features/dd-sdk-android-flags/README.md | 2 +- .../flags/internal/net/PrecomputedAssignmentsDownloader.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index cfa42466ef..8cebf37890 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -109,7 +109,7 @@ val flagsConfig = FlagsConfiguration.Builder() - Tags match exactly and are case-sensitive. A key pattern matches the whole flag key, and only `*` is a wildcard. - Each list accepts at most 31 values, and each value is at most 200 UTF-8 bytes. The SDK does not send a selection that is over a limit. It logs an error and loads the full flag configuration. - When the server applies the selection, evaluating a flag outside it returns the default value with `ErrorCode.FLAG_NOT_FOUND`. -- The selection is fixed when the feature is enabled and applies to every `FlagsClient`. Flags cached under a different selection are not loaded at startup; until the next successful fetch, a client starts without cached flags. +- The flag selection is fixed when the Flags feature is enabled and applies to every `FlagsClient`. Flags cached under a different selection are not loaded at startup; until the next successful fetch, a client starts without cached flags. - If the server ignores a selection, the SDK loads the full flag configuration and logs an error with the reason. A reason that the SDK does not know is reported as `unknown`. - With a custom flag endpoint, the proxy must forward the `DD-Flag-Selection-Ignored` response header. Otherwise, the SDK cannot report a selection that the server ignored. diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index 476a753b4e..578222adc1 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -60,7 +60,7 @@ internal class PrecomputedAssignmentsDownloader( body = body, flagSelectionIgnoredReason = if (requestFactory.sendsFlagSelection) { response.header(HEADER_FLAG_SELECTION_IGNORED) - ?.takeIf { it.isNotEmpty() } + ?.takeIf(String::isNotEmpty) ?.let(FlagSelectionIgnoredReason::sanitize) } else { null From 98b7fc9b8756ea27e664ab0c68fa2e0e03bce5dc Mon Sep 17 00:00:00 2001 From: Nikita Ogorodnikov Date: Tue, 3 Nov 2026 11:42:18 +0100 Subject: [PATCH 34/44] RUM-19577: Break dependency of test fixtures on :tools:unit module --- dd-sdk-android-core/build.gradle.kts | 9 +- .../tests/elmyr/AccountInfoForgeryFactory.kt | 1 + .../elmyr/DatadogContextForgeryFactory.kt | 1 + .../datadog/android/tests/elmyr/ForgeExt.kt | 31 ---- .../tests/elmyr/UserInfoForgeryFactory.kt | 1 + .../com/datadog/android/utils/DatadogExt.kt | 24 ++++ dd-sdk-android-internal/build.gradle.kts | 9 +- .../tests/elmyr/ErrorForgeryFactory.kt | 21 +++ .../tests/elmyr/ExceptionForgeryFactory.kt | 21 +++ .../android/internal/tests/elmyr/ForgeExt.kt | 136 ++++++++++++++++++ .../elmyr/GsonJsonArrayForgeryFactory.kt | 60 ++++++++ .../elmyr/GsonJsonObjectForgeryFactory.kt | 35 +++++ .../elmyr/GsonJsonPrimitiveForgeryFactory.kt | 32 +++++ ...InternalTelemetryErrorLogForgeryFactory.kt | 1 - .../tests/elmyr/OrgJSONArrayForgeryFactory.kt | 60 ++++++++ .../elmyr/OrgJSONObjectForgeryFactory.kt | 38 +++++ .../tests/elmyr/ThrowableForgeryFactory.kt | 21 +++ .../internal/logger/CombinedLogHandlerTest.kt | 2 +- .../logger/ConditionalLogHandlerTest.kt | 2 +- features/dd-sdk-android-rum/build.gradle.kts | 8 -- .../DatadogSingletonTestConfiguration.kt | 10 +- .../TelemetryUsageEventForgeryFactory.kt | 2 +- .../sqlite/DatadogDatabaseErrorHandlerTest.kt | 2 +- .../utils/forge/ActionEventForgeryFactory.kt | 2 +- .../utils/forge/ErrorEventForgeryFactory.kt | 4 +- .../forge/LongTaskEventForgeryFactory.kt | 2 +- .../forge/ResourceEventForgeryFactory.kt | 2 +- .../utils/forge/ViewEventForgeryFactory.kt | 2 +- .../forge/ViewUpdateEventForgeryFactory.kt | 2 +- .../VitalAppLaunchEventForgeryFactory.kt | 2 +- .../VitalOperationStepEventForgeryFactory.kt | 2 +- .../cronet/CronetIntegrationPluginTest.kt | 2 +- .../DatadogSingletonTestConfiguration.kt | 33 +++++ ...DatadogRUMUncaughtThrowableStrategyTest.kt | 2 +- .../DatadogSingletonTestConfiguration.kt | 33 +++++ .../okhttp/DatadogEventListenerFactoryTest.kt | 2 +- .../DatadogInterceptorWithoutTracesTest.kt | 2 +- .../okhttp/OkHttpIntegrationPluginTest.kt | 2 +- .../okhttp/internal/OkHttpRequestInfoTest.kt | 4 +- .../okhttp/internal/OkHttpResponseInfoTest.kt | 4 +- ...ributesProviderCompatibilityAdapterTest.kt | 2 +- ...gInterceptorContextInjectionSampledTest.kt | 2 +- ...nterceptorNonDdTracerNotSendingSpanTest.kt | 2 +- .../TracingInterceptorNonDdTracerTest.kt | 2 +- .../TracingInterceptorNotSendingSpanTest.kt | 2 +- .../okhttp/trace/TracingInterceptorTest.kt | 2 +- .../DatadogSingletonTestConfiguration.kt | 33 +++++ .../GlobalRumMonitorTestConfiguration.kt | 1 - 48 files changed, 586 insertions(+), 89 deletions(-) create mode 100644 dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt create mode 100644 dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt rename {dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests => features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils}/config/DatadogSingletonTestConfiguration.kt (72%) create mode 100644 integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt create mode 100644 integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt create mode 100644 integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt diff --git a/dd-sdk-android-core/build.gradle.kts b/dd-sdk-android-core/build.gradle.kts index 5213782b81..31166fc47b 100644 --- a/dd-sdk-android-core/build.gradle.kts +++ b/dd-sdk-android-core/build.gradle.kts @@ -146,14 +146,7 @@ dependencies { testFixturesImplementation(libs.okHttp) testFixturesImplementation(libs.bundles.testTools) testFixturesImplementation(project(":dd-sdk-android-internal")) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } + testFixturesImplementation(testFixtures(project(":dd-sdk-android-internal"))) } unMock { diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt index da0c04c86b..4f12dcbf4c 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt @@ -7,6 +7,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.api.context.AccountInfo +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt index 94eaa61af8..7de45b3b82 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt @@ -8,6 +8,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.DatadogSite import com.datadog.android.api.context.DatadogContext +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.privacy.TrackingConsent import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt index bb35a06a32..477898f1bc 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt @@ -13,40 +13,9 @@ import fr.xgouchet.elmyr.Forge import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.util.Date -import java.util.Locale -import java.util.TimeZone const val URL_FORGERY_PATTERN = "http(s?)://[a-z]+\\.com/[a-z]+" -fun Forge.exhaustiveAttributes( - excludedKeys: Set = emptySet() -): MutableMap { - return listOf( - aBool(), - anInt(), - aLong(), - aFloat(), - aDouble(), - anAsciiString(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - aList { anAlphabeticalString() }, - aList { aDouble() }, - null - ) - .associateBy { anAlphaNumericalString() } - .filter { it.key !in excludedKeys } - .toMutableMap() -} - fun T.useCoreFactories(): T { addFactory(DatadogContextForgeryFactory()) addFactory(DeviceInfoForgeryFactory()) diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt index ac4e6bb911..6277c379fb 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt @@ -7,6 +7,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.api.context.UserInfo +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt new file mode 100644 index 0000000000..e1c3a1f2c7 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils + +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore + +/** + * Adds [SdkCore] instance to [Datadog]. + */ +fun Datadog.registerInstance(name: String?, sdkCore: SdkCore) { + registry.register(name, sdkCore) +} + +/** + * Removes all [SdkCore] instances registered. + */ +fun Datadog.clearInstances() { + registry.clear() +} diff --git a/dd-sdk-android-internal/build.gradle.kts b/dd-sdk-android-internal/build.gradle.kts index 4e3c77723d..f8a8c1cbfe 100644 --- a/dd-sdk-android-internal/build.gradle.kts +++ b/dd-sdk-android-internal/build.gradle.kts @@ -63,16 +63,9 @@ dependencies { } } testFixturesImplementation(libs.kotlinRuntime) + testFixturesImplementation(libs.gson) testFixturesImplementation(libs.bundles.jUnit5) testFixturesImplementation(libs.bundles.testTools) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } unmock(libs.robolectric) } diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt new file mode 100644 index 0000000000..40dfdd93df --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Error] instance with a forged message. + */ +class ErrorForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Error { + return forge.anError() + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt new file mode 100644 index 0000000000..800d028cc2 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Exception] instance with a forged message. + */ +class ExceptionForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Exception { + return forge.anException() + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt new file mode 100644 index 0000000000..42092b53a4 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt @@ -0,0 +1,136 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import android.annotation.SuppressLint +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryException +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.Assumptions +import java.io.EOFException +import java.io.File +import java.io.FileNotFoundException +import java.io.IOError +import java.io.IOException +import java.io.UncheckedIOException +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +/** + * @return a random [Throwable] instance with a forged message. + */ +fun Forge.aThrowable(): Throwable { + return anElementFrom( + anError(), + anException() + ) +} + +/** + * @return a random [Error] instance with a forged message. + */ +fun Forge.anError(): Error { + val errorMessage = anAlphabeticalString() + return anElementFrom( + UnknownError(errorMessage), + IOError(RuntimeException(errorMessage)), + NotImplementedError(errorMessage), + StackOverflowError(errorMessage), + OutOfMemoryError(errorMessage) + ) +} + +/** + * @return a random [Exception] instance with a forged message. + * Because Mockito will prevent throwing undeclared checked exceptions, + * all the exceptions here must extend [RuntimeException]. + * [UncheckedIOException] is a wrapper around a checked [IOException], and is considered a new + * API (only available in Android 24+); we ignore the warning since tests are run on the JVM. + */ +@SuppressLint("NewApi") +fun Forge.anException(): Exception { + val errorMessage = anAlphabeticalString() + return anElementFrom( + IndexOutOfBoundsException(errorMessage), + ArithmeticException(errorMessage), + IllegalStateException(errorMessage), + ArrayIndexOutOfBoundsException(errorMessage), + NullPointerException(errorMessage), + ForgeryException(errorMessage), + UnsupportedOperationException(errorMessage), + SecurityException(errorMessage), + UncheckedIOException(IOException(errorMessage)), + UncheckedIOException(FileNotFoundException(errorMessage)), + UncheckedIOException(EOFException(errorMessage)) + ) +} + +/** + * Will generate a map with different value types with a possibility to filter out given keys. + * + * @param excludedKeys Keys to exclude from generated map. + * @param filterThreshold Max ratio of keys removed from originally generated map. If ratio + * is more than that, [Assumptions.assumeTrue] mechanism will be used. + */ +fun Forge.exhaustiveAttributes( + excludedKeys: Set = emptySet(), + filterThreshold: Float = 0.5f +): MutableMap { + val map = generateMapWithExhaustiveValues(this).toMutableMap() + + map[""] = anHexadecimalString() + map[aWhitespaceString()] = anHexadecimalString() + map[anAlphabeticalString()] = generateMapWithExhaustiveValues(this).toMutableMap().apply { + this[anAlphabeticalString()] = generateMapWithExhaustiveValues(this@exhaustiveAttributes) + } + + val filtered = map.filterKeys { it !in excludedKeys } + + assumeDifferenceIsNoMore(filtered.size, map.size, filterThreshold) + + return filtered.toMutableMap() +} + +private fun assumeDifferenceIsNoMore(result: Int, base: Int, maxDifference: Float) { + check(result <= base) { + "Number of elements after filtering cannot exceed the number of original elements." + } + + val diff = (base - result).toFloat() / base + Assumptions.assumeTrue( + diff <= maxDifference, + "Too many elements removed, condition cannot be satisfied." + ) +} + +private fun generateMapWithExhaustiveValues(forge: Forge): MutableMap { + return forge.run { + listOf( + aBool(), + anInt(), + aLong(), + aFloat(), + aDouble(), + anAsciiString(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + aList { anAlphabeticalString() }, + aList { aDouble() }, + null + ).associateByTo(mutableMapOf()) { anAlphaNumericalString() } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt new file mode 100644 index 0000000000..7af3efd12a --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonArray +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonArray] instance. + */ +class GsonJsonArrayForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonArray { + return forge.anElementFrom( + JsonArray(), + forge.aStringArray(), + forge.anIntArray(), + forge.aDoubleArray(), + forge.aBooleanArray() + ) + } + + // region Internal + + private fun Forge.aStringArray(): JsonArray { + return JsonArray().apply { + aList { anAlphabeticalString() } + .forEach { add(it) } + } + } + + private fun Forge.anIntArray(): JsonArray { + return JsonArray().apply { + aList { anInt() } + .forEach { add(it) } + } + } + + private fun Forge.aDoubleArray(): JsonArray { + return JsonArray().apply { + aList { aDouble() } + .forEach { add(it) } + } + } + + private fun Forge.aBooleanArray(): JsonArray { + return JsonArray().apply { + aList { aBool() } + .forEach { add(it) } + } + } + + // endregion +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt new file mode 100644 index 0000000000..e960fc8cca --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonObject] instance. + */ +class GsonJsonObjectForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonObject { + return JsonObject().apply { + val fieldCount = forge.aTinyInt() + repeat(fieldCount + 1) { + add( + forge.anAlphabeticalString(), + forge.anElementFrom( + forge.getForgery(), + forge.getForgery() + ) + ) + } + } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt new file mode 100644 index 0000000000..1d839cab56 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonPrimitive +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonPrimitive] instance. + */ +class GsonJsonPrimitiveForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonPrimitive { + return forge.anElementFrom( + JsonPrimitive(forge.aBool()), + JsonPrimitive(forge.anInt()), + JsonPrimitive(forge.aFloat()), + JsonPrimitive(forge.aLong()), + JsonPrimitive(forge.aDouble()), + JsonPrimitive(forge.anAlphabeticalString()), + JsonPrimitive(forge.anHexadecimalString()), + JsonPrimitive(forge.aNumericalString()), + JsonPrimitive("") + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt index 843bcc581c..2120dd9522 100644 --- a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt @@ -7,7 +7,6 @@ package com.datadog.android.internal.tests.elmyr import com.datadog.android.internal.telemetry.InternalTelemetryEvent -import com.datadog.tools.unit.forge.aThrowable import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt new file mode 100644 index 0000000000..b279c402c4 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.json.JSONArray + +/** + * A [ForgeryFactory] generating a random [JSONArray] instance. + */ +class OrgJSONArrayForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JSONArray { + return forge.anElementFrom( + JSONArray(), + forge.aStringArray(), + forge.anIntArray(), + forge.aDoubleArray(), + forge.aBooleanArray() + ) + } + + // region Internal + + private fun Forge.aStringArray(): JSONArray { + return JSONArray().apply { + aList { anAlphabeticalString() } + .forEach { put(it) } + } + } + + private fun Forge.anIntArray(): JSONArray { + return JSONArray().apply { + aList { anInt() } + .forEach { put(it) } + } + } + + private fun Forge.aDoubleArray(): JSONArray { + return JSONArray().apply { + aList { aDouble() } + .forEach { put(it) } + } + } + + private fun Forge.aBooleanArray(): JSONArray { + return JSONArray().apply { + aList { aBool() } + .forEach { put(it) } + } + } + + // endregion +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt new file mode 100644 index 0000000000..c8c7577e4f --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt @@ -0,0 +1,38 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.json.JSONArray +import org.json.JSONObject + +/** + * A [ForgeryFactory] generating a random [JSONObject] instance. + */ +class OrgJSONObjectForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JSONObject { + return JSONObject().apply { + val fieldCount = forge.aTinyInt() + repeat(fieldCount) { + put( + forge.anAlphabeticalString(), + forge.anElementFrom( + forge.anInt(), + forge.aLong(), + forge.aBool(), + forge.anAlphaNumericalString(), + forge.aDouble(), + forge.getForgery() + ) + ) + } + } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt new file mode 100644 index 0000000000..067fd648aa --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Throwable] instance with a forged message. + */ +class ThrowableForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Throwable { + return forge.aThrowable() + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt index 91f4f4f312..3bb9d21992 100644 --- a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt @@ -6,8 +6,8 @@ package com.datadog.android.log.internal.logger -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt index 7b46450f89..36b8d5a3e2 100644 --- a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt @@ -6,8 +6,8 @@ package com.datadog.android.log.internal.logger -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/features/dd-sdk-android-rum/build.gradle.kts b/features/dd-sdk-android-rum/build.gradle.kts index e86fe5e4b8..346a852fed 100644 --- a/features/dd-sdk-android-rum/build.gradle.kts +++ b/features/dd-sdk-android-rum/build.gradle.kts @@ -95,14 +95,6 @@ dependencies { // Test Fixtures testFixturesImplementation(testFixtures(project(":dd-sdk-android-core"))) testFixturesImplementation(testFixtures(project(":dd-sdk-android-internal"))) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } testFixturesImplementation(libs.kotlinRuntime) testFixturesImplementation(libs.bundles.jUnit5) testFixturesImplementation(libs.okHttp) diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt similarity index 72% rename from dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt rename to features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt index 7caa50ac0c..7e5ab87204 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt @@ -4,20 +4,22 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.tests.config +package com.datadog.android.rum.utils.config import com.datadog.android.Datadog import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance import com.datadog.tools.unit.extensions.config.MockTestConfiguration import fr.xgouchet.elmyr.Forge -class DatadogSingletonTestConfiguration : +internal class DatadogSingletonTestConfiguration : MockTestConfiguration(InternalSdkCore::class.java) { override fun setUp(forge: Forge) { super.setUp(forge) - Datadog.registry.register(null, mockInstance) + Datadog.registerInstance(null, mockInstance) } override fun tearDown(forge: Forge) { @@ -26,6 +28,6 @@ class DatadogSingletonTestConfiguration : } fun clearRegistry() { - Datadog.registry.clear() + Datadog.clearInstances() } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt index fc3cbdc953..b3b345efe6 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt @@ -7,7 +7,7 @@ package com.datadog.android.rum.utils.forge import com.datadog.android.telemetry.model.TelemetryUsageEvent -import com.datadog.android.tests.elmyr.exhaustiveAttributes +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt index e7cd96139e..0ae02ffcce 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt @@ -13,8 +13,8 @@ import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumMonitor +import com.datadog.android.rum.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.utils.forge.Configurator -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt index 641cdacac5..2bf094bce2 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ActionEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt index d9d6eb1f34..4e45eccdcd 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt @@ -6,10 +6,10 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.aThrowable +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.rum.model.ErrorEvent -import com.datadog.tools.unit.forge.aThrowable -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt index c2299a4bdf..b76258f5b3 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.LongTaskEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt index 68ac7e11cc..96cd423f1a 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt @@ -6,6 +6,7 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.domain.scope.connect import com.datadog.android.rum.internal.domain.scope.dns @@ -13,7 +14,6 @@ import com.datadog.android.rum.internal.domain.scope.download import com.datadog.android.rum.internal.domain.scope.firstByte import com.datadog.android.rum.internal.domain.scope.ssl import com.datadog.android.rum.model.ResourceEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt index ec3df2dd51..4cc12bab6c 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt @@ -6,9 +6,9 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ViewEvent import com.datadog.android.rum.model.ViewEvent.Accessibility -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt index 4032af598e..1ccf486d6a 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ViewUpdateEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt index 65e397c199..e0a4eb16e8 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.VitalAppLaunchEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt index 322eac6cc1..367825fbd4 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt @@ -6,10 +6,10 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.VitalOperationStepEvent import com.datadog.android.rum.model.VitalOperationStepEvent.FailureReason import com.datadog.android.rum.model.VitalOperationStepEvent.StepType -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt index 74a2f8fdca..77813346d6 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt @@ -8,9 +8,9 @@ package com.datadog.android.cronet import com.datadog.android.api.InternalLogger import com.datadog.android.cronet.internal.DatadogCronetEngine +import com.datadog.android.cronet.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.configuration.RumNetworkInstrumentationConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.aHostName import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration import com.datadog.android.trace.ApmNetworkTracingScope diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..0c2267cfcf --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.cronet.tests.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt index a042bcddf1..fb3f05f80b 100644 --- a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt +++ b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt @@ -6,10 +6,10 @@ package com.datadog.android.glide +import com.datadog.android.glide.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumMonitor -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..084667135a --- /dev/null +++ b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.glide.tests.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt index b19a813f27..5967cfc633 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt @@ -7,7 +7,7 @@ package com.datadog.android.okhttp import com.datadog.android.okhttp.internal.buildResourceId -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt index d33e7047cf..164e0c71f4 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt @@ -19,13 +19,13 @@ import com.datadog.android.okhttp.trace.newSpanBuilderMock import com.datadog.android.okhttp.trace.newSpanContextMock import com.datadog.android.okhttp.trace.newSpanMock import com.datadog.android.okhttp.trace.newTracerMock +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceAttributesProvider import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.resource.ResourceId -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.api.propagation.DatadogPropagation diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt index 67cb29ae76..b114a8fd84 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt @@ -10,9 +10,9 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.okhttp.internal.ApmInstrumentationOkHttpAdapter import com.datadog.android.okhttp.internal.RegistryTrackingEventListener import com.datadog.android.okhttp.internal.RumInstrumentationOkHttpAdapter +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.configuration.RumNetworkInstrumentationConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration import com.datadog.android.trace.ApmNetworkTracingScope import com.datadog.android.trace.ExperimentalTraceApi diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt index 863f443f08..78cb2795dc 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt @@ -10,10 +10,10 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.instrumentation.network.HttpBodySnapshot import com.datadog.android.okhttp.internal.OkHttpRequestInfo.Companion.ERROR_PEEK_REQUEST_BODY import com.datadog.android.tests.elmyr.URL_FORGERY_PATTERN -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.verifyLog import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.forge.anHttpHeaderMap import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.StringForgery @@ -83,7 +83,7 @@ internal class OkHttpRequestInfoTest { @Test fun `M delegate W headers property`(forge: Forge) { // Given - val fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) } + val fakeHeaders = forge.anHttpHeaderMap().mapValues { listOf(it.value) } val mockHeaders = mock { on { toMultimap() } doReturn fakeHeaders } val mockRequest = mock { on { headers } doReturn mockHeaders } diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt index c6690dd534..6f3fc08802 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt @@ -10,10 +10,10 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.instrumentation.network.HttpBodySnapshot import com.datadog.android.internal.network.HttpSpec import com.datadog.android.okhttp.internal.OkHttpResponseInfo.Companion.ERROR_PEEK_BODY -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.verifyLog import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.forge.anHttpHeaderMap import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery @@ -88,7 +88,7 @@ internal class OkHttpResponseInfoTest { @Test fun `M delegate W headers property`(forge: Forge) { // Given - val fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) } + val fakeHeaders = forge.anHttpHeaderMap().mapValues { listOf(it.value) } val mockHeaders = mock { on { toMultimap() } doReturn fakeHeaders } val mockResponse = mock { on { headers } doReturn mockHeaders } diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt index 29fcfbd32a..512028ba6b 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt @@ -12,10 +12,10 @@ import com.datadog.android.api.instrumentation.network.HttpRequestInfo import com.datadog.android.api.instrumentation.network.HttpResponseInfo import com.datadog.android.core.SdkReference import com.datadog.android.rum.RumResourceAttributesProvider -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt index ce289f682e..77f1244528 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt @@ -13,8 +13,8 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt index df4c46c6da..82fe36ba9b 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt @@ -13,7 +13,7 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt index f73b248443..6ecb379623 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt @@ -13,7 +13,7 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt index 6e9a294291..70f7ed83d3 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt @@ -12,9 +12,9 @@ import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeReso import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.RumResourceMethod -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt index 2a3f712d6d..f357289b70 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt @@ -20,8 +20,8 @@ import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator import com.datadog.android.okhttp.trace.TracingInterceptor.Companion.OKHTTP_INTERCEPTOR_HEADER_TYPES import com.datadog.android.okhttp.trace.TracingInterceptor.Companion.OKHTTP_INTERCEPTOR_SAMPLE_RATE import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..97ee2fa2bd --- /dev/null +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.okhttp.utils.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt index fa11cf657f..809e220eb1 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt @@ -10,7 +10,6 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.extensions.config.MockTestConfiguration import fr.xgouchet.elmyr.Forge import org.mockito.kotlin.mock From 198c6848c9c81c1a6b816e43af6aef4d3ea9e294 Mon Sep 17 00:00:00 2001 From: Mark Cai Date: Tue, 3 Nov 2026 13:25:41 -0500 Subject: [PATCH 35/44] FFL-3459: Clarify install flow logging and model server ignored reasons --- .../internal/evaluation/EvaluationsManager.kt | 51 +++++++++++-------- .../persistence/FlagsPersistenceManager.kt | 3 +- .../flags/internal/selection/FlagSelection.kt | 28 ++++++---- .../evaluation/EvaluationsManagerTest.kt | 5 +- .../FlagSelectionIgnoredReasonTest.kt | 8 +++ 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 92de400034..3b9f3d22b9 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -96,6 +96,7 @@ private class InitializationCompletion(private var callback: EvaluationContextCa * @param experimentationOptOut validates contexts and prevents superseded installations * @param callbackExecutor delivers customer callbacks outside state delivery and network operations */ +@Suppress("TooManyFunctions") // Each step of the fetch, install and completion lifecycle is a separate function. internal class EvaluationsManager( private val sdkCore: FeatureSdkCore, private val executorService: ExecutorService, @@ -279,39 +280,49 @@ internal class EvaluationsManager( ) { val context = request.context val installed = synchronized(initializationTerminalLock) { - val installed = experimentationOptOut.ifCurrent(request) { - val installation = flagsRepository.installFlagsAndContext( - context, - assignments.flags, - assignments.obfuscation - ) - val result = takeCompletion(initializationCompletion, callback) - currentState = FlagsClientState.Ready - effects.enqueue { - installation.complete { - effects.notifySafely { flagStateManager.updateState(FlagsClientState.Ready) } - complete(result) - } - } + val isCurrent = experimentationOptOut.ifCurrent(request) { + installCurrentFlags(context, assignments, initializationCompletion, callback) } - if (!installed) { + if (!isCurrent) { enqueueCompletion( takeCompletion(initializationCompletion, callback), FlagsContextSupersededException(SUPERSEDED_MESSAGE) ) } - installed + isCurrent } effects.drain() // A superseded response is not installed, so its ignored reason would describe flags that never loaded. if (installed) { flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } + internalLogger.log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.MAINTAINER, + { "Successfully processed context ${context.targetingKey} with ${assignments.flags.size} flags" } + ) } - internalLogger.log( - InternalLogger.Level.DEBUG, - InternalLogger.Target.MAINTAINER, - { "Successfully processed context ${context.targetingKey} with ${assignments.flags.size} flags" } + } + + // Callers hold initializationTerminalLock; the Ready notification and callback run later from effects.drain(). + private fun installCurrentFlags( + context: EvaluationContext, + assignments: PrecomputedAssignments, + initializationCompletion: InitializationCompletion?, + callback: EvaluationContextCallback? + ) { + val installation = flagsRepository.installFlagsAndContext( + context, + assignments.flags, + assignments.obfuscation ) + val result = takeCompletion(initializationCompletion, callback) + currentState = FlagsClientState.Ready + effects.enqueue { + installation.complete { + effects.notifySafely { flagStateManager.updateState(FlagsClientState.Ready) } + complete(result) + } + } } private fun startInitializationTimeout( diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt index 39fcbe265b..766b6913a3 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt @@ -34,10 +34,11 @@ internal class FlagsPersistenceManager( private val unselectedStateKey = stateKey(instanceName, experimentationOptOutMode, selected = false) private val selectedStateKey = stateKey(instanceName, experimentationOptOutMode, selected = true) private val flagsStateKey: String = if (flagSelection == null) unselectedStateKey else selectedStateKey + private val staleStateKey: String = if (flagSelection == null) selectedStateKey else unselectedStateKey init { // One persisted state per instance: changing to or from a selection discards the other key's state. - dataStore.removeValue(if (flagSelection == null) selectedStateKey else unselectedStateKey) + dataStore.removeValue(staleStateKey) loadFlagsState(onStateLoaded) } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt index eac8cfbc10..aaa9dddd59 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -45,9 +45,22 @@ internal enum class FlagSelectionError(val reason: String, val limit: String) { ) } +/** + * Reasons the server reports in the `DD-Flag-Selection-Ignored` response header. + * The server validates with the same rules as the SDK, so validation failures reuse the [FlagSelectionError] codes. + * [TAG_LOAD_FAILED] is reported only by the server, so it is not a [FlagSelectionError]. + */ +internal enum class ServerIgnoredReason(val reason: String) { + MALFORMED_SELECTION(FlagSelectionError.MALFORMED_SELECTION.reason), + TOO_MANY_VALUES(FlagSelectionError.TOO_MANY_VALUES.reason), + INVALID_KEY_PATTERN(FlagSelectionError.INVALID_KEY_PATTERN.reason), + INVALID_TAG(FlagSelectionError.INVALID_TAG.reason), + TAG_LOAD_FAILED("tag_load_failed") +} + /** * Sanitizes the `DD-Flag-Selection-Ignored` response header, so a raw header value never reaches logs or telemetry. - * A known reason is kept, and any other non-empty value becomes [UNKNOWN]. + * A [ServerIgnoredReason] is kept, and any other non-empty value becomes [UNKNOWN]. */ internal object FlagSelectionIgnoredReason { @@ -56,15 +69,10 @@ internal object FlagSelectionIgnoredReason { // The longest known reason has 19 characters. The limit bounds the work on an oversized header. private const val MAX_LENGTH = 32 - private val KNOWN_REASONS = setOf( - FlagSelectionError.MALFORMED_SELECTION.reason, - FlagSelectionError.TOO_MANY_VALUES.reason, - FlagSelectionError.INVALID_KEY_PATTERN.reason, - FlagSelectionError.INVALID_TAG.reason, - "tag_load_failed" - ) - - fun sanitize(value: String): String = if (value.length <= MAX_LENGTH && value in KNOWN_REASONS) value else UNKNOWN + fun sanitize(value: String): String { + if (value.length > MAX_LENGTH) return UNKNOWN + return ServerIgnoredReason.entries.firstOrNull { it.reason == value }?.reason ?: UNKNOWN + } } internal sealed interface FlagSelectionResult { diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 762d2a38ca..d9a7e3f10d 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -1957,6 +1957,7 @@ internal class EvaluationsManagerTest { onlyOnce = true, additionalProperties = mapOf("reason" to fakeIgnoredReason) ) + assertThat(capturedLogMessages()).contains("Successfully processed context $fakeTargetingKey with 0 flags") verify(mockFlagsRepository).installFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull()) verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) } @@ -2032,7 +2033,9 @@ internal class EvaluationsManagerTest { // Then verify(firstCallback).onFailure(isA()) - assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + assertThat(capturedLogMessages()).noneMatch { + "flagSelection" in it || "Flag selection" in it || "Successfully processed" in it + } } @Test diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt index 1baefa9c97..4a51f18fbf 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt @@ -8,6 +8,7 @@ package com.datadog.android.flags.internal.selection import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.ValueSource internal class FlagSelectionIgnoredReasonTest { @@ -21,6 +22,13 @@ internal class FlagSelectionIgnoredReasonTest { assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo(reason) } + @ParameterizedTest + @EnumSource(ServerIgnoredReason::class) + fun `M keep the reason W sanitize() { any server ignored reason }`(reason: ServerIgnoredReason) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason.reason)).isEqualTo(reason.reason) + } + @ParameterizedTest @ValueSource( strings = [ From 27a03bcbf793271b0f4fcad900dfa77b480722d2 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 14:27:29 -0500 Subject: [PATCH 36/44] FFL-3568: Enforce shared mobile precomputed response fixtures Run the canonical JSON corpus through the client and persistent cache. Enforce numeric getter types, preserve legacy number reads, and automate signed fixture refreshes. --- .../self.precomputed-fixtures.sts.yaml | 9 + .github/workflows/precomputed-fixtures.yml | 86 ++++ ci/precomputed-fixtures-pr.md | 17 + ci/precomputed-fixtures.json | 3 + ci/sync-precomputed-fixtures.py | 178 +++++++ detekt_custom_safe_calls_third_party.yml | 2 + detekt_custom_unsafe_calls.yml | 1 + features/dd-sdk-android-flags/README.md | 47 ++ .../flags/internal/FlagValueConverter.kt | 16 +- .../repository/net/PrecomputeMapper.kt | 9 +- .../flags/internal/serialization/FlagsJson.kt | 10 + .../flags/internal/DatadogFlagsClientTest.kt | 4 +- .../integration/CanonicalPrecomputedTest.kt | 187 +++++++ .../src/test/resources/precomputed/LICENSE | 201 ++++++++ .../src/test/resources/precomputed/NOTICE | 4 + .../src/test/resources/precomputed/README.md | 231 +++++++++ .../test/resources/precomputed/SOURCE.json | 48 ++ .../cases/emissions/all-doLog-false.json | 87 ++++ .../cases/emissions/duplicate-evaluation.json | 63 +++ .../cases/evaluator/all-types-success.json | 142 ++++++ .../defaults-and-emission-gates.json | 111 +++++ .../cases/evaluator/empty-flags-response.json | 64 +++ .../cases/evaluator/falsy-values.json | 129 +++++ .../integer-precision-native-only.json | 61 +++ .../cases/evaluator/multi-flag-set.json | 471 ++++++++++++++++++ .../cases/evaluator/multiple-reasons.json | 91 ++++ .../cases/evaluator/numeric-float.json | 291 +++++++++++ .../cases/evaluator/numeric-integer.json | 195 ++++++++ .../cases/evaluator/numeric-number.json | 261 ++++++++++ .../cases/evaluator/object-nested-deep.json | 87 ++++ .../evaluator/special-characters-in-keys.json | 108 ++++ .../evaluator/type-mismatch-all-types.json | 144 ++++++ .../cases/obfuscation/obfuscation-keys.json | 259 ++++++++++ .../obfuscation-numeric-float.json | 310 ++++++++++++ .../obfuscation-numeric-integer.json | 214 ++++++++ .../obfuscation-numeric-number.json | 280 +++++++++++ ...uscation-reject-false-with-descriptor.json | 50 ++ ...obfuscation-reject-missing-descriptor.json | 46 ++ .../obfuscation-reject-missing-marker.json | 49 ++ .../obfuscation-reject-missing-salt.json | 49 ++ .../obfuscation-reject-nonhex-digest.json | 59 +++ .../obfuscation-reject-nonhex-salt.json | 50 ++ .../obfuscation-reject-null-descriptor.json | 47 ++ .../obfuscation-reject-null-marker.json | 46 ++ .../obfuscation-reject-null-salt.json | 50 ++ .../obfuscation-reject-plaintext.json | 59 +++ .../obfuscation-reject-short-digest.json | 59 +++ .../obfuscation-reject-short-salt.json | 50 ++ .../obfuscation-reject-string-marker.json | 50 ++ .../obfuscation-reject-unknown-scheme.json | 50 ++ .../obfuscation-reject-uppercase-digest.json | 59 +++ .../obfuscation-reject-uppercase-salt.json | 50 ++ .../obfuscation/obfuscation-rotated-salt.json | 259 ++++++++++ .../obfuscation-unsupported-consumer.json | 47 ++ .../src/test/resources/precomputed/index.json | 39 ++ .../test/resources/precomputed/schema.json | 400 +++++++++++++++ 56 files changed, 5983 insertions(+), 6 deletions(-) create mode 100644 .github/chainguard/self.precomputed-fixtures.sts.yaml create mode 100644 .github/workflows/precomputed-fixtures.yml create mode 100644 ci/precomputed-fixtures-pr.md create mode 100644 ci/precomputed-fixtures.json create mode 100644 ci/sync-precomputed-fixtures.py create mode 100644 reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/LICENSE create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/NOTICE create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/README.md create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/index.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/schema.json diff --git a/.github/chainguard/self.precomputed-fixtures.sts.yaml b/.github/chainguard/self.precomputed-fixtures.sts.yaml new file mode 100644 index 0000000000..10df730638 --- /dev/null +++ b/.github/chainguard/self.precomputed-fixtures.sts.yaml @@ -0,0 +1,9 @@ +issuer: https://token.actions.githubusercontent.com +subject: repo:DataDog/dd-sdk-android:ref:refs/heads/develop +claim_pattern: + event_name: "(schedule|workflow_dispatch)" + workflow_ref: 'DataDog/dd-sdk-android/\.github/workflows/precomputed-fixtures\.yml@refs/heads/develop' + repository: "DataDog/dd-sdk-android" +permissions: + contents: write + pull_requests: write diff --git a/.github/workflows/precomputed-fixtures.yml b/.github/workflows/precomputed-fixtures.yml new file mode 100644 index 0000000000..365876893f --- /dev/null +++ b/.github/workflows/precomputed-fixtures.yml @@ -0,0 +1,86 @@ +name: Precomputed flag fixtures +on: + pull_request: + paths: + - 'ci/*precomputed*' + - 'reliability/single-fit/flags/src/test/resources/precomputed/**' + - '.github/workflows/precomputed-fixtures.yml' + schedule: + - cron: '23 8 * * 1' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: precomputed-fixtures-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Verify the local snapshot + run: python3 ci/sync-precomputed-fixtures.py --verify + - name: Compare with the recorded upstream commit + run: python3 ci/sync-precomputed-fixtures.py --check + update: + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + steps: + - uses: DataDog/dd-octo-sts-action@c85802a58e202b7e764b5441e4378c76043c8108 # v1.0.5 + id: token + with: + scope: DataDog/dd-sdk-android + policy: self.precomputed-fixtures + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Import upstream fixtures + id: sync + run: python3 ci/sync-precomputed-fixtures.py --ref main + - name: Prepare a draft update + id: prepare + if: steps.sync.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + SOURCE_COMMIT: ${{ steps.sync.outputs.source_commit }} + run: | + existing=$(gh pr list --state open --search '"Update precomputed flag fixtures" in:title' --json number --jq 'length') + if [ "$existing" != 0 ]; then + echo 'An update PR is already open. Merge or close it before the next update.' + exit 0 + fi + branch="automation/precomputed-fixtures-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + git add -- 'reliability/single-fit/flags/src/test/resources/precomputed' + echo "branch=$branch" >> "$GITHUB_OUTPUT" + echo 'ready=true' >> "$GITHUB_OUTPUT" + python3 - <<'BODY' + import os + from pathlib import Path + template = Path('ci/precomputed-fixtures-pr.md').read_text() + Path('/tmp/precomputed-pr.md').write_text(template.replace('SOURCE_COMMIT', os.environ['SOURCE_COMMIT'])) + BODY + - name: Create a signed fixture commit + if: steps.prepare.outputs.ready == 'true' + uses: DataDog/commit-headless@d6afabfd690ea1dbdd7d368acbefd71304928699 # action/v3.4.1 + with: + command: commit + token: ${{ steps.token.outputs.token }} + branch: ${{ steps.prepare.outputs.branch }} + head-sha: ${{ github.sha }} + create-branch: true + force: false + message: 'FFL-3568: Update precomputed flag fixtures' + - name: Open the draft pull request + if: steps.prepare.outputs.ready == 'true' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + UPDATE_BRANCH: ${{ steps.prepare.outputs.branch }} + run: | + gh pr create --draft --base develop --head "$UPDATE_BRANCH" --title 'FFL-3568: Update precomputed flag fixtures' --body-file /tmp/precomputed-pr.md diff --git a/ci/precomputed-fixtures-pr.md b/ci/precomputed-fixtures-pr.md new file mode 100644 index 0000000000..59e09253e6 --- /dev/null +++ b/ci/precomputed-fixtures-pr.md @@ -0,0 +1,17 @@ +### What does this PR do? + +Refresh the shared precomputed response fixtures to `SOURCE_COMMIT`. + +### Motivation + +Keep mobile response tests aligned with the canonical JSON contract. Ref: FFL-3568. + +### Additional Notes + +The existing fixture loop runs new cases in normal SDK CI. Review those results before merge. + +### Review checklist (to be filled by reviewers) + +- [ ] Feature or bugfix MUST have appropriate tests (unit, integration, e2e) +- [ ] Make sure you discussed the feature or bugfix with the maintaining team in an Issue +- [ ] Make sure each commit and the PR mention the Issue number (cf the [CONTRIBUTING](../CONTRIBUTING.md) doc) diff --git a/ci/precomputed-fixtures.json b/ci/precomputed-fixtures.json new file mode 100644 index 0000000000..33727dd641 --- /dev/null +++ b/ci/precomputed-fixtures.json @@ -0,0 +1,3 @@ +{ + "destination": "reliability/single-fit/flags/src/test/resources/precomputed" +} diff --git a/ci/sync-precomputed-fixtures.py b/ci/sync-precomputed-fixtures.py new file mode 100644 index 0000000000..d47e01c1ff --- /dev/null +++ b/ci/sync-precomputed-fixtures.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# Unless explicitly stated otherwise, all files in this repository are licensed under the +# Apache License Version 2.0. This product includes software developed at Datadog +# (https://www.datadoghq.com/). Copyright 2025-Present Datadog, Inc. +"""Import a reproducible precompute test snapshot; never edit imported JSON.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +import uuid + +REPOSITORY = 'https://github.com/DataDog/ffe-system-test-data.git' +PREFIX = 'precomputed-assignments/' +FILES = { + 'schemas/precomputed-assignment.schema.json': 'schema.json', + 'precomputed-assignments/README.md': 'README.md', + 'LICENSE': 'LICENSE', + 'NOTICE': 'NOTICE', +} + + +def git(source, *args): + return subprocess.check_output(['git', '-C', str(source), *args]) + + +def snapshot(source, ref): + if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', ref) or '..' in ref: + raise ValueError('Invalid fixture revision') + sha = git(source, 'rev-parse', '--verify', ref + '^{commit}').decode().strip() + if not re.fullmatch(r'[0-9a-f]{40}', sha): + raise ValueError('Expected a full source commit SHA') + result = {} + for entry in git(source, 'ls-tree', '-rz', '--full-tree', sha).split(b'\0'): + if not entry: + continue + metadata, raw_path = entry.split(b'\t', 1) + path = raw_path.decode() + target = FILES.get(path) + if path.startswith(PREFIX + 'cases/') and path.endswith('.json'): + target = path[len(PREFIX):] + if target is None: + continue + mode, kind, blob = metadata.decode().split() + if mode != '100644' or kind != 'blob': + raise ValueError('Fixture entries must be ordinary files: ' + path) + result[target] = git(source, 'cat-file', 'blob', blob) + cases = sorted(p for p in result if p.startswith('cases/')) + if not cases or not all(p in result for p in FILES.values()): + raise ValueError('Missing or empty precomputed fixture corpus') + for path in cases: + data = json.loads(result[path]) + if not isinstance(data, dict) or data.get('name') != Path(path).stem: + raise ValueError('Invalid fixture identity: ' + path) + if not isinstance(data.get('evaluations'), list): + raise ValueError('Missing evaluations: ' + path) + result['index.json'] = (json.dumps(cases, indent=2) + '\n').encode() + result['SOURCE.json'] = (json.dumps({ + 'repository': REPOSITORY, + 'commit': sha, + 'files': {p: hashlib.sha256(v).hexdigest() for p, v in sorted(result.items())}, + }, indent=2) + '\n').encode() + return result + + +def read_snapshot(destination): + if not destination.exists(): + return {} + result = {} + for path in destination.rglob('*'): + if path.is_symlink(): + raise ValueError('Snapshot must not contain symbolic links') + if path.is_file() and path.suffix != '.meta': + result[path.relative_to(destination).as_posix()] = path.read_bytes() + return result + + +def verify(destination): + content = read_snapshot(destination) + metadata = json.loads(content.pop('SOURCE.json')) + if metadata['repository'] != REPOSITORY or not re.fullmatch(r'[0-9a-f]{40}', metadata['commit']): + raise ValueError('Invalid source provenance') + hashes = {p: hashlib.sha256(v).hexdigest() for p, v in sorted(content.items())} + if hashes != metadata['files']: + raise ValueError('Snapshot content differs from its recorded checksums') + cases = sorted(p for p in content if p.startswith('cases/') and p.endswith('.json')) + if not cases or json.loads(content['index.json']) != cases: + raise ValueError('Missing, empty, or stale fixture index') + print(f"Verified {len(cases)} fixtures at {metadata['commit']}") + + +def unity_metadata(destination): + for asset in sorted(destination.rglob('*')): + if asset.suffix == '.meta': + continue + meta = asset.with_name(asset.name + '.meta') + if not meta.exists(): + guid = uuid.uuid5(uuid.NAMESPACE_URL, 'ffe-precomputed/' + asset.relative_to(destination).as_posix()).hex + folder = 'folderAsset: yes\n' if asset.is_dir() else '' + importer = 'DefaultImporter' if asset.is_dir() or asset.suffix != '.json' else 'TextScriptImporter' + meta.write_text(f'fileFormatVersion: 2\nguid: {guid}\n{folder}{importer}:\n externalObjects: {{}}\n userData:\n assetBundleName:\n assetBundleVariant:\n') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--ref', default='main') + parser.add_argument('--source', type=Path, help='Read a local Git checkout at the requested committed revision') + parser.add_argument('--verify', action='store_true', help='Verify local checksums and discovery index without network access') + parser.add_argument('--check', action='store_true', help='Compare with the recorded upstream SHA; do not write') + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + config = json.loads((root / 'ci/precomputed-fixtures.json').read_text()) + relative = Path(config['destination']) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Destination must be inside the repository') + destination = root / relative + if not destination.resolve().is_relative_to(root): + raise ValueError('Destination escapes the repository') + if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', args.ref) or '..' in args.ref: + raise ValueError('Invalid fixture revision') + if args.verify: + verify(destination) + return + before = read_snapshot(destination) + ref = json.loads(before['SOURCE.json'])['commit'] if args.check else args.ref + with tempfile.TemporaryDirectory(prefix='ffe-precompute-') as temporary: + source = args.source + if source is None: + source = Path(temporary) / 'source' + subprocess.run(['git', 'init', '-q', str(source)], check=True) + subprocess.run(['git', '-C', str(source), 'fetch', '-q', '--depth=1', REPOSITORY, ref], check=True) + ref = 'FETCH_HEAD' + after = snapshot(source, ref) + changed = {p:v for p,v in before.items() if p != 'SOURCE.json'} != {p:v for p,v in after.items() if p != 'SOURCE.json'} + if args.check: + if before != after: + raise ValueError('Snapshot differs from its recorded source commit') + elif changed: + destination.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='.precompute-', dir=destination.parent) as staging: + staged = Path(staging) / 'snapshot' + staged.mkdir() + for path, content in after.items(): + output = staged / path + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(content) + # Preserve Unity's existing asset identities when refreshing a snapshot. + if destination.exists(): + for meta in destination.rglob('*.meta'): + rel = meta.relative_to(destination) + asset = staged / str(rel)[:-5] + if asset.exists(): + (staged / rel).write_bytes(meta.read_bytes()) + backup = Path(staging) / 'previous' + destination.rename(backup) + try: + staged.rename(destination) + except BaseException: + backup.rename(destination) + raise + else: + staged.rename(destination) + if config.get('unityMetadata') and not args.check: + unity_metadata(destination) + selected = before if args.check or not changed else after + metadata = json.loads(selected['SOURCE.json']) + count = len(json.loads(selected['index.json'])) + print(f"FFE precomputed fixtures: {metadata['commit']}; {count} files; changed={str(changed).lower()}") + if os.environ.get('GITHUB_OUTPUT'): + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + output.write(f"changed={str(changed).lower()}\nsource_commit={metadata['commit']}\nfixture_count={count}\n") + + +if __name__ == '__main__': + main() diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index 3a930d31d6..1b62929c05 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -842,6 +842,7 @@ datadog: - "kotlin.enums.EnumEntries.associateBy(kotlin.Function1)" - "kotlin.enums.EnumEntries.firstOrNull(kotlin.Function1)" - "kotlin.enums.EnumEntries.forEach(kotlin.Function1)" + - "kotlin.enums.EnumEntries.none(kotlin.Function1)" - "kotlin.enums.EnumEntries.toSet()" - "kotlin.ranges.IntProgression.asSequence()" - "kotlin.sequences.Sequence.groupBy(kotlin.Function1)" @@ -950,6 +951,7 @@ datadog: - "kotlin.String.removePrefix(kotlin.CharSequence)" - "kotlin.String.removeSuffix(kotlin.CharSequence)" - "kotlin.String.takeIf(kotlin.Function1)" + - "kotlin.String.toBigDecimalOrNull()" - "kotlin.String.toBigIntegerOrNull()" - "kotlin.String.toBoolean()" - "kotlin.String.toBooleanStrictOrNull()" diff --git a/detekt_custom_unsafe_calls.yml b/detekt_custom_unsafe_calls.yml index d201a28e77..aadb89f3b1 100644 --- a/detekt_custom_unsafe_calls.yml +++ b/detekt_custom_unsafe_calls.yml @@ -167,6 +167,7 @@ datadog: - "java.lang.System.loadLibrary(kotlin.String?):java.lang.SecurityException,java.lang.UnsatisfiedLinkError,java.lang.NullPointerException" - "java.lang.reflect.Field.get(kotlin.Any):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" - "java.lang.reflect.Field.get(kotlin.Any?):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" + - "java.math.BigDecimal.intValueExact():java.lang.ArithmeticException" - "java.math.BigInteger.and(java.math.BigInteger?):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.constructor(kotlin.String?, kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.shiftRight(kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index 60b8cdcc84..f5d8e8e163 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -432,3 +432,50 @@ For more information on Feature Flags in Datadog, see the [official Feature Flag [1]: https://docs.datadoghq.com/real_user_monitoring/application_monitoring/android/setup [2]: https://docs.datadoghq.com/getting_started/feature_flags/ + +## Shared precomputed response tests + +The Flags module uses the JSON contract from [ffe-system-test-data](https://github.com/DataDog/ffe-system-test-data/tree/main/precomputed-assignments). +This keeps response behavior consistent across mobile SDKs. The snapshot is test data only. +It adds no runtime dependency and requires no Git submodule. + +The generated snapshot is at `reliability/single-fit/flags/src/test/resources/precomputed`. `SOURCE.json` records the source commit and file digests. +The runner discovers all nested JSON cases through the generated index. +Adding a case requires an upstream fixture change and snapshot refresh. It requires no runner or filename-list change. +Missing or empty fixtures fail the suite. Shared `_include` and `_skip` fields describe API applicability. +Do not add local exclusions to hide failures. + +Native Integer getters accept `integer`. Native Double getters accept `float`. +Legacy `number` accepts Double reads and representable integral Integer reads. +A whole-valued `float` still fails an Integer read with `TYPE_MISMATCH`. +React Native's Number API has no separate integer type. +These rules do not enable a new server wire format. Sending specific numeric types requires compatible capability negotiation. + +Cases cover values, defaults, errors, reasons, variants, emission gates, duplicate evaluations, and numeric boundaries. +Obfuscation cases cover fixed digests, salts, Unicode, whitespace, malformed metadata, and plaintext event identity. +The suite uses the feature integration harness. Each valid fixture also runs after cache restoration without networking. It checks public getters, response rejection, and real exposure and evaluation records. + +Run from the repository root: + +```sh +python3 ci/sync-precomputed-fixtures.py --verify +./gradlew :reliability:single-fit:flags:testDebugUnitTest --tests '*CanonicalPrecomputedTest*' +``` + +Refresh the snapshot from a reviewed upstream commit: + +```sh +python3 ci/sync-precomputed-fixtures.py --ref +python3 ci/sync-precomputed-fixtures.py --check +``` + +The importer copies committed bytes, regenerates the index, and removes deleted cases. +It preserves Unity asset GUIDs where applicable. Do not edit imported JSON or its checksums. +Use `--source --ref ` to test an upstream fixture branch before publication. +`--verify` checks local integrity without networking. `--check` also compares the recorded upstream commit. + +The weekly `Precomputed flag fixtures` workflow opens a draft update PR when fixture content changes. +It uses a short-lived token restricted to the default branch and this workflow. +It waits when an update PR is already open. Normal SDK CI must pass before merge. +Merge the upstream fixture contract before merging its consumer PRs. +The update workflow never merges or publishes an SDK release. diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt index a3c7e5b22a..404d8813da 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt @@ -53,7 +53,18 @@ internal object FlagValueConverter { val result: T? = when (targetType) { Boolean::class -> variationValue.lowercase(Locale.US).toBooleanStrictOrNull() as? T String::class -> variationValue as T - Int::class -> variationValue.toIntOrNull() as? T + Int::class -> { + val number = variationValue.toBigDecimalOrNull() + val integer = try { + number?.intValueExact() + } catch (_: ArithmeticException) { + null + } + if (integer == null && number != null) { + throw TypeMismatchException("Flag value cannot be represented as an Int") + } + integer as? T + } Double::class -> variationValue.toDoubleOrNull() as? T Map::class -> variationValue.toMap() as? T JSONObject::class -> { @@ -90,8 +101,7 @@ internal object FlagValueConverter { String::class -> variationType == VariationType.STRING.value Int::class -> variationType == VariationType.INTEGER.value || variationType == VariationType.NUMBER.value Double::class -> - variationType == VariationType.NUMBER.value || variationType == VariationType.FLOAT.value || - variationType == VariationType.INTEGER.value + variationType == VariationType.NUMBER.value || variationType == VariationType.FLOAT.value JSONObject::class -> variationType == VariationType.OBJECT.value Map::class -> variationType == VariationType.OBJECT.value else -> { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt index d7e7a34b92..2cb6eaece8 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt @@ -14,6 +14,7 @@ import com.datadog.android.flags.internal.model.AssignmentsResponse import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.serialization.FlagsJson +import com.datadog.android.flags.internal.serialization.UnsupportedVariationTypeException import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParseException @@ -41,7 +42,13 @@ internal class PrecomputeMapper( val flags = attributes.flags.additionalProperties if (obfuscation != null) FlagsJson.validateKeys(flags.keys.iterator()) PrecomputedAssignments( - flags.mapValues { FlagsJson.readAssignment(FlagsJson.objectValue(it.value)) }, + flags.mapNotNull { (key, value) -> + try { + key to FlagsJson.readAssignment(FlagsJson.objectValue(value)) + } catch (_: UnsupportedVariationTypeException) { + null + } + }.toMap(), obfuscation ) } else { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt index 1d5653f7db..e401c667e6 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt @@ -10,14 +10,18 @@ import com.datadog.android.flags.internal.model.Assignment import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsState import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.model.VariationType import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParseException import org.json.JSONObject +internal class UnsupportedVariationTypeException : JsonParseException("Unsupported assignment variation type") + /** Generated model adapters, legacy JSON conversions, and encoding validation. */ internal object FlagsJson { @Throws(JsonParseException::class) + @Suppress("ThrowsCount") // Validate required fields and isolate unsupported variation types. fun readAssignment(json: JsonObject): PrecomputedFlag { // Preserve JSONObject's string conversion, including JSON variants and explicit null. STRING_FIELDS.forEach { field -> @@ -34,8 +38,14 @@ internal object FlagsJson { val serialId = json.get("serialId") if (serialId?.isJsonPrimitive != true || !serialId.asJsonPrimitive.isNumber) json.remove("serialId") + if (!json.has("extraLogging")) json.add("extraLogging", JsonObject()) val assignment = Assignment.fromJsonObject(json) + val unsupportedType = VariationType.entries.none { it.value == assignment.variationType } + if (unsupportedType) { + throw UnsupportedVariationTypeException() + } + @Suppress("UnsafeThirdPartyFunctionCall") // Generated JSON is a valid object. val extraLogging = JSONObject(assignment.extraLogging.toJson().toString()) return PrecomputedFlag( diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt index ea34486017..ba30be9a69 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt @@ -1587,7 +1587,7 @@ internal class DatadogFlagsClientTest { } @Test - fun `M accept number, float and int W resolveDoubleValue() { compatible types }`(forge: Forge) { + fun `M accept number and float but reject int W resolveDoubleValue() { declared types }`(forge: Forge) { // Given val fakeFlagKey1 = forge.anAlphabeticalString() val fakeFlagKey2 = forge.anAlphabeticalString() @@ -1628,7 +1628,7 @@ internal class DatadogFlagsClientTest { // Then assertThat(result1).isEqualTo(fakeFlagValue) assertThat(result2).isEqualTo(fakeFlagValue) - assertThat(result3).isEqualTo(fakeFlagIntValue.toDouble()) + assertThat(result3).isEqualTo(fakeDefaultValue) } @Test diff --git a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt new file mode 100644 index 0000000000..914b01d316 --- /dev/null +++ b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt @@ -0,0 +1,187 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.integration + +import com.datadog.android.api.feature.Feature +import com.datadog.android.flags.Flags +import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.integration.FlagsClientTest.Companion.createStubSdkCore +import com.datadog.android.flags.integration.tests.elmyr.FlagsIntegrationForgeConfigurator +import com.datadog.android.flags.integration.tests.utils.httpResponse +import com.datadog.android.flags.model.EvaluationContext +import com.datadog.android.flags.model.FlagsClientState +import com.datadog.android.flags.model.ResolutionReason +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@ExtendWith(MockitoExtension::class, ForgeExtension::class) +@ForgeConfiguration(FlagsIntegrationForgeConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class CanonicalPrecomputedTest { + @ParameterizedTest(name = "{0}, restored={1}") + @MethodSource("fixtures") + fun `M satisfy canonical contract W setEvaluationContext() and resolve()`( + path: String, + restored: Boolean, + forge: Forge + ) { + // Given + val fixture = read(path).asJsonObject + assumeTrue(applies(fixture), "Canonical platform exclusion: $path") + var core = createStubSdkCore(forge) + val store = core.stubDataStore(Feature.FLAGS_FEATURE_NAME) + core.stubNetworkResponses { httpResponse(it, body = fixture["response"].toString()) } + Flags.enable( + FlagsConfiguration.Builder().useCustomFlagEndpoint("https://fixture.test/precompute-assignments").build(), + core + ) + var client = FlagsClient.Builder(sdkCore = core).build() + val context = fixture.getAsJsonObject("context") + val attributes = context.getAsJsonObject("attributes").entrySet().associate { it.key to it.value.asString } + + // When + try { + client.setEvaluationContext(EvaluationContext(context["targetingKey"].asString, attributes)) + + // Then + assertThat(core.requestsSent()).hasSize(1) + if (fixture.has("expectedResponseError")) { + assertThat(fixture["expectedResponseError"].asString).isEqualTo("INVALID_RESPONSE") + assertThat(client.state.getCurrentState()).isInstanceOf(FlagsClientState.Error::class.java) + } else { + assertThat(client.state.getCurrentState()).isEqualTo(FlagsClientState.Ready) + if (restored) { + core.stopFeatures() + core = createStubSdkCore(forge) + core.stubDataStore(Feature.FLAGS_FEATURE_NAME, store) + core.stubNetworkResponses { null } + Flags.enable(FlagsConfiguration.Builder().build(), core) + client = FlagsClient.Builder(sdkCore = core).build() + assertThat(core.requestsSent()).isEmpty() + } + fixture.getAsJsonArray("evaluations").map { it.asJsonObject }.filter(::applies).forEach { evaluation -> + val key = evaluation["flag"].asString + val fallback = evaluation["defaultValue"] + val result = when (evaluation["variationType"].asString) { + "boolean" -> client.resolve(key, fallback.asBoolean) + "string" -> client.resolve(key, fallback.asString) + "integer" -> client.resolve(key, fallback.asInt) + "float" -> client.resolve(key, fallback.asDouble) + "object" -> client.resolve(key, Gson().fromJson(fallback, Map::class.java)) + else -> error("Unsupported canonical getter: $evaluation") + } + if (restored && result.errorCode == null) { + assertThat(result.reason).isEqualTo(ResolutionReason.CACHED) + } + val actual = JsonObject().apply { + add("value", Gson().toJsonTree(result.value)) + addProperty("reason", if (result.errorCode != null) "ERROR" else result.reason.toString()) + addProperty("errorCode", result.errorCode?.toString()) + addProperty("variantKey", result.variant) + } + evaluation.getAsJsonObject("expectedResult").entrySet().forEach { (field, expected) -> + if (restored && field == "reason" && result.errorCode == null) return@forEach + assertThat(actual[field]).describedAs("$path: $key: $field").isEqualTo(expected) + } + } + } + core.stopFeatures() + val expectations = fixture.getAsJsonObject("expectations") + assertEvents( + core.eventsWritten(Feature.FLAGS_FEATURE_NAME).map { + JsonParser.parseString(it.eventData) + }, + expectations, + "exposures" + ) + assertEvents( + core.eventsWritten(Feature.FLAGS_EVALUATIONS_FEATURE_NAME).map { + JsonParser.parseString(it.eventData) + }, + expectations, + "evaluationEvents" + ) + } finally { + core.stopFeatures() + } + } + + private fun assertEvents(events: List, expectations: JsonObject, field: String) { + val remaining = events.toMutableList() + expectations.getAsJsonArray(field)?.map { it.asJsonObject }?.filter(::applies)?.forEach { matcher -> + val fields = JsonObject().apply { + matcher.entrySet().filterNot { it.key.startsWith("_") }.forEach { add(it.key, it.value) } + } + repeat(matcher["_count"].asInt) { + val index = remaining.indexOfFirst { matches(it, fields) } + assertThat( + index + ).describedAs("Missing event for $matcher; remaining $remaining").isGreaterThanOrEqualTo(0) + remaining.removeAt(index) + } + } + if (expectations["noUnmatchedEvents"]?.asBoolean == true) { + assertThat(remaining).describedAs("Unmatched $field").isEmpty() + } + } + + private fun matches(actual: JsonElement?, expected: JsonElement): Boolean = when { + expected.isJsonNull -> actual == null || actual.isJsonNull + expected.isJsonObject -> actual?.isJsonObject == true && expected.asJsonObject.entrySet().all { + matches(actual.asJsonObject[it.key], it.value) + } + else -> actual == expected + } + + companion object { + private fun read(path: String): JsonElement = JsonParser.parseString( + checkNotNull(CanonicalPrecomputedTest::class.java.getResource("/precomputed/$path")) { + "Missing fixture: $path" + }.readText() + ) + + private fun applies(value: JsonObject): Boolean { + fun contains(field: String) = + value.getAsJsonArray(field)?.any { it.asJsonObject["platform"].asString == "android" } == true + return !contains("_skip") && (!value.has("_include") || contains("_include")) + } + + @JvmStatic + fun fixtures(): List { + val paths = read("index.json").asJsonArray.map { it.asString } + check(paths.isNotEmpty()) { "Empty canonical fixture corpus" } + check(paths.distinct().size == paths.size) { "Duplicate fixture index entries" } + check(paths.any { applies(read(it).asJsonObject) }) { "All canonical fixtures excluded" } + return paths.flatMap { path -> + val fixture = read(path).asJsonObject + val modes = if (applies(fixture) && + !fixture.has("expectedResponseError") + ) { + listOf(false, true) + } else { + listOf(false) + } + modes.map { Arguments.of(path, it) } + } + } + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE b/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE b/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE new file mode 100644 index 0000000000..cec0e71a28 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE @@ -0,0 +1,4 @@ +Datadog ffe-system-test-data +Copyright 2026-Present Datadog, Inc. + +This product includes software developed at Datadog (https://www.datadoghq.com/). diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/README.md b/reliability/single-fit/flags/src/test/resources/precomputed/README.md new file mode 100644 index 0000000000..0a07911689 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/README.md @@ -0,0 +1,231 @@ +# Precomputed Assignment Fixtures (`precomputed-assignments/`) + +These fixtures cover the Datadog precompute assignment API consumed by mobile +and browser SDKs which do not run the evaluator locally. + +## Directory structure + +``` +precomputed-assignments/ + cases/ + evaluator/ # Core value resolution, types, defaults, errors + emissions/ # Exposure and evaluation event behavior + parsing/ # Malformed data handling, isolation (future) + README.md +``` + +Test runners should recursively discover `*.json` files under `cases/`. + +## Fixture structure + +Each fixture file is a single JSON object: + +```json +{ + "name": "fixture-name", + "description": "What this fixture tests.", + "context": { + "targetingKey": "user-id", + "attributes": {} + }, + "response": { ... }, + "evaluations": [ ... ], + "expectations": { ... } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | yes | Fixture identifier; must match filename stem | +| `description` | string | yes | What the fixture tests | +| `context` | object | yes | `targetingKey` and `attributes` for the evaluation context | +| `response` | object | yes | Mocked precomputed-assignments response (open-ended) | +| `evaluations` | array | yes | Typed flag evaluations to run | +| `expectations` | object | yes | Event matchers (see below) | +| `_skip` | array | no | Skip entire fixture for listed platforms | +| `_include` | array | no | Run fixture only for listed platforms | + +## Evaluations + +Each evaluation calls a typed getter on the SDK and asserts the result: + +```json +{ + "flag": "flag-key", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } +} +``` + +`defaultValue` and `expectedResult.value` must use the JSON type declared by +`variationType`: boolean, string, integer, number for `float`, or object. +`float` accepts any JSON number; `integer` accepts only integral JSON numbers +(including `1.0`). Arrays and null are not valid for either field. + +Evaluations may carry `_skip` or `_include` to control per-platform applicability. + +## Expectations and event matchers + +The `expectations` block uses ordered, exclusive matchers to assert on emitted +exposure and evaluation events. + +```json +"expectations": { + "noUnmatchedEvents": true, + "exposures": [ + {"_count": 1, "flag": "my-flag"} + ], + "evaluationEvents": [ + {"_count": 2, "_skip": [{"platform": "web"}]}, + {"_count": 1, "_include": [{"platform": "web"}], "evaluation_count": 2} + ] +} +``` + +### Matcher semantics + +Matchers are processed **in order**. Each received event is consumed by the +first matcher whose non-`_` fields are a subset of the event. Once consumed, +the event is unavailable to later matchers. + +- `_count` *(required)*: how many events this matcher should consume. +- All other non-`_` keys are match fields compared against event data. +- A match field set to `null` asserts the field is either absent or null on + the actual event. +- A matcher with only `_count` (no match fields) matches any event. + +### `noUnmatchedEvents` + +When `true`, the test runner asserts that all received events (both exposures +and evaluations) are consumed by matchers. No extra events allowed. Applies to +both event types. + +### Control fields + +All fields prefixed with `_` are control fields, not match fields: + +| Field | Description | +|-------|-------------| +| `_count` | Number of events to consume (required on matchers) | +| `_skip` | Skip this matcher/evaluation/fixture for listed platforms | +| `_include` | Apply this matcher/evaluation/fixture only for listed platforms | + +Test runners in dynamic languages should filter keys starting with `_` to +separate control from match fields. Typed runners can use the known set. + +## `_skip` and `_include` + +Platform filters are always an array of `{platform, reason?}` objects, +regardless of where they appear (case, evaluation, or matcher level). + +```json +"_skip": [ + {"platform": "web", "reason": "Browser SDK aggregates differently."} +] + +"_include": [ + {"platform": "web"} +] +``` + +- **Default** (neither present): applies to all platforms. +- `_skip`: applies to all platforms except listed. +- `_include`: applies only to listed platforms. +- **Mutually exclusive**: specifying both is invalid. +- `reason` is optional. + +Valid platform identifiers: `web`, `ios`, `android`, `react-native`, `flutter`, +`maui`, `kotlin-multiplatform`, `electron`, `unity`. + +## How to implement a test runner + +1. Recursively discover `*.json` files under `precomputed-assignments/cases/`. +2. For each fixture, check `_skip`/`_include` at case level. Skip if the + current platform is excluded. +3. Initialize the SDK with `context`. Feed `response` as the mocked server + payload. +4. Walk `evaluations` in order. For each evaluation not skipped for the current + platform, call the typed getter (e.g., `getBooleanValue`) with `flag`, + `defaultValue`. Assert against `expectedResult`. +5. Flush the SDK. +6. Collect emitted exposure and evaluation events. +7. Walk `expectations.exposures` matchers in order. For each applicable matcher + (not skipped for the current platform), greedily consume up to `_count` + events whose fields are a superset of the matcher's non-`_` fields. Fail if + fewer than `_count` matched. +8. Repeat step 7 for `expectations.evaluationEvents`. +9. If `noUnmatchedEvents` is `true`, fail if any unconsumed events remain. + +## Field naming + +- All schema field names use **camelCase** (`expectedResult`, `variationType`). +- Snake_case in matchers is only for wire-format fields that match emitted + event schemas (e.g., `evaluation_count`, `serial_id`). +- Fixture filenames use **kebab-case**. + +## Schema and validation + +The fixture schema is defined in `schemas/precomputed-assignment.schema.json`. +Validate fixtures locally with: + +```bash +python3 ci/validate-precomputed-fixtures.py +``` + +## Mobile conformance and numeric types + +Native Integer getters accept `integer`. Native Double getters accept `float`. +The declared type controls this decision even when a `float` value is whole. +Legacy `number` accepts Double reads and representable integral Integer reads. +Fractional Integer reads return the default with `TYPE_MISMATCH`. +Single-Number APIs use the explicit evaluation filters for cross-type tests. + +Discover cases recursively. Do not maintain a filename list. Fail when the +corpus is missing or empty. Report the fixture revision, executed evaluations, +and exclusions. New JSON files require a snapshot refresh, not runner edits. +The generated `index.json` supports environments that cannot enumerate packaged +resources. It must be regenerated from the complete directory, never edited. + +`expectedResponseError: "INVALID_RESPONSE"` requires rejection of the complete +response before installation. Such a case has no evaluations. Assert rejection +explicitly; do not catch every exception as a successful expected rejection. + +The obfuscation cases use `flag-key-sha256-v1`. Hash the UTF-8 domain prefix +`datadog.feature-flags.flag-key.v1\0`, the decoded 16-byte salt, and the unchanged +UTF-8 flag key. Digest vectors are fixed JSON data. Never derive an expected +digest using the SDK being tested. Lookup uses the caller's plaintext key; +emitted events retain that plaintext identity. Missing encoded lookups must not +retry plaintext lookup. Unsupported consumers must reject encoded responses. + +The corpus checks duplicate evaluations after one flush. Aggregating consumers +emit one event with `evaluation_count: 2`. Exposure deduplication is independent. +Runners must inspect real event records, not synthesize them from expectations. +Keep native bridge and deployed-backend proof separate from mocked conformance. + +## Snapshot maintenance + +Consumers can copy `ci/sync-precomputed-fixtures.py` and configure the destination +in `ci/precomputed-fixtures.json`. The importer copies committed source bytes, +records the full SHA and file digests in `SOURCE.json`, generates `index.json`, +and removes deleted cases. `--check` compares the complete snapshot with its +recorded source commit. `--source` permits testing a committed local fixture +branch before publication. Tests use the checked-in snapshot without networking. + +Fixture PRs can expose existing SDK defects. Record those failures rather than +changing expectations or adding local skips. Platform filters describe API +applicability, not temporary implementation failures. `unity` is the identifier +for the Unity Flags implementation. + +`--verify` checks local file digests and the discovery index without networking. +Use it in consumer CI. `--check` also verifies upstream provenance. For Unity, +set `unityMetadata: true` to preserve existing asset GUIDs and create missing metadata. + +Unsupported variation types are omitted independently. Their lookups return +`FLAG_NOT_FOUND`. A supported flag read with the wrong getter returns `TYPE_MISMATCH`. +Missing `extraLogging` is equivalent to an empty object. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json new file mode 100644 index 0000000000..c4da2bf85a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -0,0 +1,48 @@ +{ + "repository": "https://github.com/DataDog/ffe-system-test-data.git", + "commit": "e4f93740f783ddc0823be5f15daaf78c01869d48", + "files": { + "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", + "README.md": "0c29b94146aa81088dc0e70435019f6d8bded371a068d566768ecdb2e7cb86e1", + "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", + "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", + "cases/evaluator/all-types-success.json": "ff22e47d5bda8a1fef19892c853e4396ccdb8509a3741888eb69169847fc4fde", + "cases/evaluator/defaults-and-emission-gates.json": "fd9c306086b7ce87cacd7f69d2f2c7af03921fffa9b43455dcc89dc17f3c3bfd", + "cases/evaluator/empty-flags-response.json": "f9ddb39e6a425b2c3f7884c7bdf92516e5ebac5b0ed4d5b7be6aa5e38950ea99", + "cases/evaluator/falsy-values.json": "93f26466d4536563c38619499275e2efe9f6c98ce2bc10c55ad57b15292b6647", + "cases/evaluator/integer-precision-native-only.json": "96c4bed183bfeeab7b59209dcdf5b36726e417f1035a28198ef95642ea673622", + "cases/evaluator/multi-flag-set.json": "b1c76977764c1bc11f430a50d33a06bb33daa60ed0d184547250362559f34a0b", + "cases/evaluator/multiple-reasons.json": "812b48f5131814fa7e13b3779b47f8805e59a7b063b5737b17139d01111c0134", + "cases/evaluator/numeric-float.json": "f8ce19ac8b0cf9e5750a9eb54975e056bfa53e358015e6b36e84025e3799eecd", + "cases/evaluator/numeric-integer.json": "9522fe4f68e3b8a48aac3cb2ed60fd254ced98f9b8d29eb61eeaa32ce521388c", + "cases/evaluator/numeric-number.json": "dc3b2b0086f9fb0d24e5e5f41249bbb78a77f5880adf22f05e288e9191a8077a", + "cases/evaluator/object-nested-deep.json": "22f8f0c516cfe0456d34697f29bbf39d7736559e0b188de4671d0fee6545fd3d", + "cases/evaluator/special-characters-in-keys.json": "07e24ba1a407ef9604af0bc5ab3f8faafe199dd83db94d96d0e3d689430054ce", + "cases/evaluator/type-mismatch-all-types.json": "1c4eb5a6407cca9ca9c8e7d5fd09d241cb8296cb5b42c9a56432cc278a86933a", + "cases/obfuscation/obfuscation-keys.json": "3af487d59d5a7064e0e8e6508343db21689180d7d6e862b055934036e1716643", + "cases/obfuscation/obfuscation-numeric-float.json": "92760c90a0d289fc9c8ae38323bc81b5c6a5608d43a05b7b9c4831c097b971bd", + "cases/obfuscation/obfuscation-numeric-integer.json": "eab193a1eb876522c37eb041e704efa91a00c0c92304d52a65897882e589dc35", + "cases/obfuscation/obfuscation-numeric-number.json": "d05e213350654a8f4fd08883316dd10ed7e2a5be51383b8a3c94fd025027833b", + "cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "452e57fbcec5a254c65295014ae11675d406a87567e6887933ba493a14109093", + "cases/obfuscation/obfuscation-reject-missing-descriptor.json": "f0105da777c3b35ff10c7082e62ddd1f7ce6a4bc15dae27302f279b18b3bfc29", + "cases/obfuscation/obfuscation-reject-missing-marker.json": "4fb270faacdcfe92477120f9bf9ea56f98e1503cf992c72da71ecc6fbca5e0dc", + "cases/obfuscation/obfuscation-reject-missing-salt.json": "3f570926fd565dbcbdffa0ba83c7142fbffa85e1092d9562f190194b6e746a59", + "cases/obfuscation/obfuscation-reject-nonhex-digest.json": "64f7a96b1ee94cc978fd6ba5dcf4fe10652402ca0cab49c7091c5529b26e9bde", + "cases/obfuscation/obfuscation-reject-nonhex-salt.json": "4813bf66119b890770029039d70ce8dbb8e7749bae808ab3cf953b6ddbf40625", + "cases/obfuscation/obfuscation-reject-null-descriptor.json": "85010c74f15be5a2188c3f534539e82e4138ba1b182bf94b8036348753e3dfa8", + "cases/obfuscation/obfuscation-reject-null-marker.json": "c11bcc39b8235f278d95c9848c47d43ecda5649fe2c6156f37266ffe710eb375", + "cases/obfuscation/obfuscation-reject-null-salt.json": "c7c93a8fe80b4ba3704112124d95b58caff95ac3c07cff232031b4a87626bc61", + "cases/obfuscation/obfuscation-reject-plaintext.json": "5487e40a247337ed0c4ed7921bd62b7232b28309839de91ecfa5ba3d39fd4bf4", + "cases/obfuscation/obfuscation-reject-short-digest.json": "90e7aa899491117416d10989589f574bb5e530394fbaa4b460924083941ee29f", + "cases/obfuscation/obfuscation-reject-short-salt.json": "3e71c820b5eb0e19da5dbd0af088b43b9a854a7f367066576d096ef33770a1be", + "cases/obfuscation/obfuscation-reject-string-marker.json": "a22240089a26b1528ca07f1f7c958d8f7535438e2b47ad97fb717944eef73ad9", + "cases/obfuscation/obfuscation-reject-unknown-scheme.json": "ba4297333f5b8eed888e74a64dacb83d8db7ac111bc5465d61ec07b523331fc5", + "cases/obfuscation/obfuscation-reject-uppercase-digest.json": "a7e8b2de615fc55e5d39cca5ea42d1dfe14dc7a15b40db0d012a80992cb5943e", + "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", + "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", + "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", + "index.json": "07d8036a97955a47a5ea5e840c7f612f69b2f13bce67ddbd636d2a6dfdde6a43", + "schema.json": "0353d393b1b4193346b2fa907f500b5c60be51318759c3afe52e549f1662195d" + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json new file mode 100644 index 0000000000..701d304e25 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json @@ -0,0 +1,87 @@ +{ + "name": "all-doLog-false", + "description": "All flags have doLog=false. Evaluations succeed but zero exposure events should be emitted.", + "context": { + "targetingKey": "silent-user", + "attributes": { + "plan": "internal" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.silent.bool": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.string": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.int": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.silent.bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "fixture.silent.string", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "hello", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "greeting" + } + }, + { + "flag": "fixture.silent.int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 10, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-ten" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json new file mode 100644 index 0000000000..0e26bc826c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json @@ -0,0 +1,63 @@ +{ + "name": "duplicate-evaluation", + "description": "The same flag is evaluated twice. Validates that the SDK returns consistent values and that exposure dedup produces only one exposure event. Web SDK's FlagEvaluationAggregator aggregates duplicate evaluations into one event with evaluation_count=2; the web override and expectations.events matcher verify this. Non-aggregating platforms emit two separate events.", + "context": { + "targetingKey": "dedup-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.dedup.flag": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + }, + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1, + "evaluation_count": 2 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json new file mode 100644 index 0000000000..b2d1b3d608 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json @@ -0,0 +1,142 @@ +{ + "name": "all-types-success", + "description": "Successful precomputed assignments for all supported Flutter value types.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "pro", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.enabled": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.title": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.limit": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.ratio": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.config": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "enabled" + } + }, + { + "flag": "flutter.fixture.title", + "variationType": "string", + "defaultValue": "Fallback title", + "expectedResult": { + "value": "Datadog Flags", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "copy-a" + } + }, + { + "flag": "flutter.fixture.limit", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-five" + } + }, + { + "flag": "flutter.fixture.ratio", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "half" + } + }, + { + "flag": "flutter.fixture.config", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "object-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json new file mode 100644 index 0000000000..bf906705df --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json @@ -0,0 +1,111 @@ +{ + "name": "defaults-and-emission-gates", + "description": "Precomputed assignments that validate exposure gates, unknown variation isolation, and default/error flagevaluation payloads. Unsupported variation types are omitted; lookup returns FLAG_NOT_FOUND.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "free", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.silent": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "flutter.fixture.string": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.unknown": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.silent", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "silent-on" + } + }, + { + "flag": "flutter.fixture.string", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.unknown", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 4, + "_skip": [ + { + "platform": "web" + } + ] + }, + { + "_count": 1, + "_include": [ + { + "platform": "web" + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json new file mode 100644 index 0000000000..0114c621c3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json @@ -0,0 +1,64 @@ +{ + "name": "empty-flags-response", + "description": "Server returns an empty flags object. All evaluations must return their default values with a flagNotFound error.", + "context": { + "targetingKey": "empty-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {} + } + } + }, + "evaluations": [ + { + "flag": "fixture.bool.missing", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.string.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.int.missing", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 0, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 3, + "_skip": [ + { + "platform": "web" + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json new file mode 100644 index 0000000000..ecb04d7640 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json @@ -0,0 +1,129 @@ +{ + "name": "falsy-values", + "description": "Flags whose variation values are falsy (false, 0, 0.0, empty string, empty object). SDK must return these as real values, not fall back to defaults.", + "context": { + "targetingKey": "falsy-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.falsy.bool": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.int": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.float": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.string": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.object": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.falsy.bool", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "fixture.falsy.int", + "variationType": "integer", + "defaultValue": 99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero" + } + }, + { + "flag": "fixture.falsy.float", + "variationType": "float", + "defaultValue": 1.0, + "expectedResult": { + "value": 0.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero-f" + } + }, + { + "flag": "fixture.falsy.string", + "variationType": "string", + "defaultValue": "non-empty", + "expectedResult": { + "value": "", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty" + } + }, + { + "flag": "fixture.falsy.object", + "variationType": "object", + "defaultValue": { + "fallback": true + }, + "expectedResult": { + "value": {}, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty-obj" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json new file mode 100644 index 0000000000..a938cc6942 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json @@ -0,0 +1,61 @@ +{ + "name": "integer-precision-native-only", + "description": "Validates that SDKs with 64-bit integer flag evaluation preserve values outside JavaScript's safe integer range (Number.MAX_SAFE_INTEGER = 2^53-1).", + "context": { + "targetingKey": "precomputed-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large-int": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large-int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "large" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "The native integer API preserves signed 64-bit integers. Browser Dart must report separate applicability." + }, + { + "platform": "flutter", + "reason": "The native integer API preserves signed 64-bit integers. Browser Dart must report separate applicability." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json new file mode 100644 index 0000000000..303b816d5a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json @@ -0,0 +1,471 @@ +{ + "name": "multi-flag-set", + "description": "Precomputed response with 50 flags of mixed types. Validates that SDK parsers handle moderate flag sets without truncation or memory errors. Only a sample of flags are evaluated; the remainder are parsed but not asserted.", + "context": { + "targetingKey": "large-set-user", + "attributes": { + "plan": "enterprise" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large.flag-01": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-02": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-03": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-04": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-05": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-06": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.large.flag-07": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-08": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-09": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-10": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-11": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-12": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-13": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "fixture.large.flag-14": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-15": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-16": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-17": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-18": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-19": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-20": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.021": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.022": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.023": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.024": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.025": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.026": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.027": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.028": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.029": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.030": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.031": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.032": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.033": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.034": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.035": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.036": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.037": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.038": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.039": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.040": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.041": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.042": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.043": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.044": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.045": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.046": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.047": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.048": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.049": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.050": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large.flag-01", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v1" + } + }, + { + "flag": "fixture.large.flag-10", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v10" + } + }, + { + "flag": "fixture.large.flag-20", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v20" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json new file mode 100644 index 0000000000..6d507829ae --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json @@ -0,0 +1,91 @@ +{ + "name": "multiple-reasons", + "description": "Flags with different evaluation reasons in a single response: TARGETING_MATCH, DEFAULT, and SPLIT.", + "context": { + "targetingKey": "reason-user", + "attributes": { + "plan": "pro" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.reason.targeted": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.reason.default": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "fixture.reason.split": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.reason.targeted", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "targeted-on" + } + }, + { + "flag": "fixture.reason.default", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "baseline", + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "default-val" + } + }, + { + "flag": "fixture.reason.split", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 42, + "reason": "SPLIT", + "errorCode": null, + "variantKey": "bucket-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json new file mode 100644 index 0000000000..4bbd9d3a7c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json @@ -0,0 +1,291 @@ +{ + "name": "numeric-float", + "description": "numeric float.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "float-0-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float-0-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float-5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float-5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float--5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float--5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float-1.25-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float-1.25-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float--1.25-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "float--1.25-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + }, + "evaluations": [ + { + "flag": "float-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float-1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float--1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float--1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 10, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 5, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json new file mode 100644 index 0000000000..381cfa53a1 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json @@ -0,0 +1,195 @@ +{ + "name": "numeric-integer", + "description": "numeric integer.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "integer-0-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "integer-0-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "integer-5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "integer-5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "integer--5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "integer--5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + }, + "evaluations": [ + { + "flag": "integer-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "integer-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "integer--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 6, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 3, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json new file mode 100644 index 0000000000..946151c604 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json @@ -0,0 +1,261 @@ +{ + "name": "numeric-number", + "description": "numeric number.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "number-0-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number-0-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number-5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number-5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number--5-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number--5-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number-1.25-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number-1.25-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number--1.25-as-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "number--1.25-as-float": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + }, + "evaluations": [ + { + "flag": "number-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "number-1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "number--1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 10, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 8, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json new file mode 100644 index 0000000000..86bd89aab3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json @@ -0,0 +1,87 @@ +{ + "name": "object-nested-deep", + "description": "Object flag with deeply nested structure. SDK must preserve full object hierarchy without flattening or truncating.", + "context": { + "targetingKey": "nested-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.config.deep": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.config.deep", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "deep-config" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json new file mode 100644 index 0000000000..00a4666dd7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json @@ -0,0 +1,108 @@ +{ + "name": "special-characters-in-keys", + "description": "Flag keys contain dots, hyphens, and underscores. SDK must handle these without mangling the key during lookup or emission.", + "context": { + "targetingKey": "special-key-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "com.example.feature.enabled": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "my-team--feature-toggle": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "legacy_flag_name_v2": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "mix.ed-key_style.v1": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "com.example.feature.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "my-team--feature-toggle", + "variationType": "string", + "defaultValue": "no", + "expectedResult": { + "value": "yes", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "active" + } + }, + { + "flag": "legacy_flag_name_v2", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 100, + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "v2-val" + } + }, + { + "flag": "mix.ed-key_style.v1", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 2.718, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "mixed-val" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 4 + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json new file mode 100644 index 0000000000..2fd77213f9 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json @@ -0,0 +1,144 @@ +{ + "name": "type-mismatch-all-types", + "description": "Wrong typed getters return the caller default with TYPE_MISMATCH. APIs with one Number getter do not implement integer-versus-float mismatch.", + "context": { + "targetingKey": "mismatch-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.mismatch.bool-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.string-as-int": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.int-as-float": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.float-as-bool": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.object-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.mismatch.bool-as-string", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.string-as-int", + "variationType": "integer", + "defaultValue": -1, + "expectedResult": { + "value": -1, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.int-as-float", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 0.0, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter." + } + ] + }, + { + "flag": "fixture.mismatch.float-as-bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.object-as-string", + "variationType": "string", + "defaultValue": "none", + "expectedResult": { + "value": "none", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 5, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json new file mode 100644 index 0000000000..9c1be0a323 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json @@ -0,0 +1,259 @@ +{ + "name": "obfuscation-keys", + "description": "obfuscation keys.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "adca75d2141c51b0c0f084c1058edfb8e6e763f91aaf54ca06326d9847bd9586": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1a5f851cc82a3fdf03a461d72a2241584dcf455df1d384e02a937901b3bc80b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3bfa8c3c17c1b61035b98ecf14007cdf5cba5ce61a48e8cfb65f8106541892d3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1e8b7ec5e8028a1ec96b38dd37f6ccea041c0a90358904af40ac5810c23c8765": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "94b611e0d3b26b52f6ad66013d1c72f8a92ea109390049759e75d3c8d3aa4dab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bac134d201be5e7f28fc7019248f0809c2a013b137e866446ee71add2bc344c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "072d985b427f536ad0a11b2d4c5e0f7e6f0ff6d23e779e082f75599ce3fe3eba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-0", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "Flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-1", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": " flag ", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-2", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "caf\u00e9", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-3", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "cafe\u0301", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-4", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "\ud83d\udeb2/\u65d7", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-5", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "a\u0000b", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-6", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-7", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "flag" + } + }, + { + "_count": 1, + "flag": { + "key": "Flag" + } + }, + { + "_count": 1, + "flag": { + "key": " flag " + } + }, + { + "_count": 1, + "flag": { + "key": "caf\u00e9" + } + }, + { + "_count": 1, + "flag": { + "key": "cafe\u0301" + } + }, + { + "_count": 1, + "flag": { + "key": "\ud83d\udeb2/\u65d7" + } + }, + { + "_count": 1, + "flag": { + "key": "a\u0000b" + } + }, + { + "_count": 1, + "flag": { + "key": "" + } + } + ], + "evaluationEvents": [ + { + "_count": 9 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json new file mode 100644 index 0000000000..6547f39aaa --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json @@ -0,0 +1,310 @@ +{ + "name": "obfuscation-numeric-float", + "description": "numeric float.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "4fc70afd0edbe9f6c3605f50b73892f4167f395a93aa5c2e91c5380818270237": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "01c7520392b183ba08426484c8981ef7b9574d8d5e1f0b5b0afc04945b1fdc65": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "30a0b6a28e18bbdd712d677df8b0a0012086219260528d4967214fa45f36f90b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "b432d3d8d83d0f10acfb8d8099c7dd1238b96b45e12f2407ae6bfd938ad92354": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "26a957788e3e02fda8ace878f452615d85c2d80a539a19522b45c9fac903b905": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fd7b22e26bc6227562127dc3945e30e159e1b522ba003b52706d5b33c8db0519": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "93cafc55f2224734ccd9a3a0030a8bd007dcded0f48320a16d2044744cc6a4f6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "5ad25a909e7b8ef40cf13c71a7ec8f2ab4bb8a3ae4a5439029357f9982508bf8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "4d677ea458fe5ed9851dbdba4f92981a432b5b9b467445032483b9fa2d2f7368": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "eb98f328fb6978a74722d54badcaf0335bc7433ca86e205ecca0be0ac86012c8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "float-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float-1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float-1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "float--1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "float--1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 10, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 5, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json new file mode 100644 index 0000000000..7380abfe85 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json @@ -0,0 +1,214 @@ +{ + "name": "obfuscation-numeric-integer", + "description": "numeric integer.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "3a56e171b0efb081585be54227ac1792ebae79900fb0e0ee2c3850ebc7dd7fd0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "3df507231bc602ed78f6e01db4497a88b33c086a455deba043f35dde999c0b7d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "58e7c159ddb85fe9250395e7797d80ecb7dca88e1a22767261a9482611baa358": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "a4ef458efa0a3876a8509351d70fa007a68460b21c64cc64680c1099ec450dab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "01c7f586a7f7bd98d4cbc75760f506b6ff8a9ba7195e88f975a708cc7b6ebb30": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "14a559f9d240120a5038b7f3f6529a0cba11d3cbc84409c84678290f4116b809": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "integer-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "integer-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "integer--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "integer--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 6, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 3, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json new file mode 100644 index 0000000000..9e54ced94d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json @@ -0,0 +1,280 @@ +{ + "name": "obfuscation-numeric-number", + "description": "numeric number.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "085fa5b25a3d42952668a59d052deb6ee5bc2418a352b9887556fe862ef78ca8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "66aeb85f02e96149bd485b28375161c4bc154e748545593a40da549d53f479c2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "eef7048e9d655ec6f29019eec43986e1d4ba6ff42523ecd8f46b6ac0ae883050": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "a46a4fdb29e1de92315dc1ee481f59a33455841d317e38191f13cdcdd9fd2eb0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "5f25ccca362e35bf27475aa056da166b4e7ecf6ed66b65bc25f9d573717d2628": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "914ae5836aae14f5bd8348d1669e008e400c43887d08cd6dc67b028d914e8c6a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "3cecd82b1b8e3c49bc3fe1886fdd0a57de07ac68a437f59bf7c6c6317d83b03b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "5a539f4ac96a34becffeed98230a919b3082e085bb79deed40d88600d5500636": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "ec97872ee38dc677992d1df24af2093298d35a35d246ea3dd39df7e33279b358": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "4b33526589906dcabeefa0e880ad6697d4e6673af26187c67947124777a67bbc": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -1.25, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "number-0-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-0-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--5-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--5-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number-1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "number-1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "number--1.25-as-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_skip": [ + { + "platform": "web", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + }, + { + "platform": "react-native", + "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + } + ] + }, + { + "flag": "number--1.25-as-float", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 10, + "_skip": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + }, + { + "_count": 8, + "_include": [ + { + "platform": "web", + "reason": "Single Number API." + }, + { + "platform": "react-native", + "reason": "Single Number API." + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json new file mode 100644 index 0000000000..40a036689d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-false-with-descriptor", + "description": "obfuscation reject false with descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": false, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json new file mode 100644 index 0000000000..0c0d8633f1 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json @@ -0,0 +1,46 @@ +{ + "name": "obfuscation-reject-missing-descriptor", + "description": "obfuscation reject missing descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json new file mode 100644 index 0000000000..18741990fb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json @@ -0,0 +1,49 @@ +{ + "name": "obfuscation-reject-missing-marker", + "description": "obfuscation reject missing marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json new file mode 100644 index 0000000000..ce837948c3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json @@ -0,0 +1,49 @@ +{ + "name": "obfuscation-reject-missing-salt", + "description": "obfuscation reject missing salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json new file mode 100644 index 0000000000..1cbb1a4e6b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-nonhex-digest", + "description": "obfuscation reject nonhex digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json new file mode 100644 index 0000000000..1c2c2ffbb1 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-nonhex-salt", + "description": "obfuscation reject nonhex salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json new file mode 100644 index 0000000000..e1d273588c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json @@ -0,0 +1,47 @@ +{ + "name": "obfuscation-reject-null-descriptor", + "description": "obfuscation reject null descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": null + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json new file mode 100644 index 0000000000..67a35537ed --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json @@ -0,0 +1,46 @@ +{ + "name": "obfuscation-reject-null-marker", + "description": "obfuscation reject null marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": null + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json new file mode 100644 index 0000000000..84350ae9e2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-null-salt", + "description": "obfuscation reject null salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": null + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json new file mode 100644 index 0000000000..94059a2655 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-plaintext", + "description": "obfuscation reject plaintext.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flag": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json new file mode 100644 index 0000000000..0e2f913c12 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-short-digest", + "description": "obfuscation reject short digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json new file mode 100644 index 0000000000..2b54a3938c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-short-salt", + "description": "obfuscation reject short salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json new file mode 100644 index 0000000000..1c1a8bf92d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-string-marker", + "description": "obfuscation reject string marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": "true", + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json new file mode 100644 index 0000000000..7e98d9e894 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-unknown-scheme", + "description": "obfuscation reject unknown scheme.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "unknown", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json new file mode 100644 index 0000000000..4d53d3e8c2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-uppercase-digest", + "description": "obfuscation reject uppercase digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json new file mode 100644 index 0000000000..2d89321ff3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-uppercase-salt", + "description": "obfuscation reject uppercase salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090A0B0C0D0E0F" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json new file mode 100644 index 0000000000..ba16c9a0ce --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json @@ -0,0 +1,259 @@ +{ + "name": "obfuscation-rotated-salt", + "description": "obfuscation rotated salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb0951b2113f60f04611546a5ec4c7e7e12cabc968eff59362aaeecc6d683fa9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd79ba8d71066ff40da24e6ed335f7669113d17fef540cde96044c03411af1c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a2d89601f57c605c641d06d9bbe736d7e7873744ada2beecff36c051d67c1114": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "65675bf54578b33d06926dc6dbb43abe39d12458b7f7202d72d22d3cf294e63d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2d84852642a347b30811707354eed8d3ea564967f46d549be94b4c58e53c78c8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd982551b4c9c73d5a4d857288e0889e2641a71ea2be43767284d29348399139": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f43ee46b2cdf25f71c542e857876b885b68295fe1845d7f3ea83f32f334c276c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "ffffffffffffffffffffffffffffffff" + } + } + } + }, + "evaluations": [ + { + "flag": "flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-0", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "Flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-1", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": " flag ", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-2", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "caf\u00e9", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-3", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "cafe\u0301", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-4", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "\ud83d\udeb2/\u65d7", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-5", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "a\u0000b", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-6", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-7", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "flag" + } + }, + { + "_count": 1, + "flag": { + "key": "Flag" + } + }, + { + "_count": 1, + "flag": { + "key": " flag " + } + }, + { + "_count": 1, + "flag": { + "key": "caf\u00e9" + } + }, + { + "_count": 1, + "flag": { + "key": "cafe\u0301" + } + }, + { + "_count": 1, + "flag": { + "key": "\ud83d\udeb2/\u65d7" + } + }, + { + "_count": 1, + "flag": { + "key": "a\u0000b" + } + }, + { + "_count": 1, + "flag": { + "key": "" + } + } + ], + "evaluationEvents": [ + { + "_count": 9 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json new file mode 100644 index 0000000000..94894f1bc6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json @@ -0,0 +1,47 @@ +{ + "name": "obfuscation-unsupported-consumer", + "description": "obfuscation unsupported consumer.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "react-native", + "reason": "Encoded assignments are unsupported; reject rather than install digest keys." + }, + { + "platform": "unity", + "reason": "Encoded assignments are unsupported; reject rather than install digest keys." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json new file mode 100644 index 0000000000..336c33dbb7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -0,0 +1,39 @@ +[ + "cases/emissions/all-doLog-false.json", + "cases/emissions/duplicate-evaluation.json", + "cases/evaluator/all-types-success.json", + "cases/evaluator/defaults-and-emission-gates.json", + "cases/evaluator/empty-flags-response.json", + "cases/evaluator/falsy-values.json", + "cases/evaluator/integer-precision-native-only.json", + "cases/evaluator/multi-flag-set.json", + "cases/evaluator/multiple-reasons.json", + "cases/evaluator/numeric-float.json", + "cases/evaluator/numeric-integer.json", + "cases/evaluator/numeric-number.json", + "cases/evaluator/object-nested-deep.json", + "cases/evaluator/special-characters-in-keys.json", + "cases/evaluator/type-mismatch-all-types.json", + "cases/obfuscation/obfuscation-keys.json", + "cases/obfuscation/obfuscation-numeric-float.json", + "cases/obfuscation/obfuscation-numeric-integer.json", + "cases/obfuscation/obfuscation-numeric-number.json", + "cases/obfuscation/obfuscation-reject-false-with-descriptor.json", + "cases/obfuscation/obfuscation-reject-missing-descriptor.json", + "cases/obfuscation/obfuscation-reject-missing-marker.json", + "cases/obfuscation/obfuscation-reject-missing-salt.json", + "cases/obfuscation/obfuscation-reject-nonhex-digest.json", + "cases/obfuscation/obfuscation-reject-nonhex-salt.json", + "cases/obfuscation/obfuscation-reject-null-descriptor.json", + "cases/obfuscation/obfuscation-reject-null-marker.json", + "cases/obfuscation/obfuscation-reject-null-salt.json", + "cases/obfuscation/obfuscation-reject-plaintext.json", + "cases/obfuscation/obfuscation-reject-short-digest.json", + "cases/obfuscation/obfuscation-reject-short-salt.json", + "cases/obfuscation/obfuscation-reject-string-marker.json", + "cases/obfuscation/obfuscation-reject-unknown-scheme.json", + "cases/obfuscation/obfuscation-reject-uppercase-digest.json", + "cases/obfuscation/obfuscation-reject-uppercase-salt.json", + "cases/obfuscation/obfuscation-rotated-salt.json", + "cases/obfuscation/obfuscation-unsupported-consumer.json" +] diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/schema.json b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json new file mode 100644 index 0000000000..5aac5dce2f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json @@ -0,0 +1,400 @@ +{ + "$id": "precomputed-assignment", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "FLEX Precomputed Assignment Fixture", + "description": "Schema for precomputed assignment test fixtures consumed by client SDKs.", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "description", + "context", + "response", + "evaluations", + "expectations" + ], + "properties": { + "name": { + "type": "string", + "description": "Fixture identifier; should match the filename stem." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the fixture tests." + }, + "context": { + "$ref": "#/$defs/EvaluationContext" + }, + "response": { + "type": "object", + "description": "Mocked precomputed-assignments response. Open-ended; downstream code validates the payload shape." + }, + "evaluations": { + "type": "array", + "items": { + "$ref": "#/$defs/Evaluation" + } + }, + "expectations": { + "$ref": "#/$defs/Expectations" + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter", + "description": "Skip this entire fixture for listed platforms." + }, + "_include": { + "$ref": "#/$defs/PlatformFilter", + "description": "Run this fixture only for listed platforms." + }, + "expectedResponseError": { + "const": "INVALID_RESPONSE", + "description": "Reject the complete response before installation; run no evaluations and emit no events." + } + }, + "not": { + "required": [ + "_skip", + "_include" + ] + }, + "$defs": { + "PlatformFilterEntry": { + "type": "object", + "required": [ + "platform" + ], + "additionalProperties": false, + "properties": { + "platform": { + "type": "string", + "description": "Platform identifier (e.g., web, ios, android, flutter, react-native, maui, kotlin-multiplatform, electron).", + "enum": [ + "web", + "ios", + "android", + "react-native", + "flutter", + "unity", + "maui", + "kotlin-multiplatform", + "electron" + ] + }, + "reason": { + "type": "string", + "description": "Optional explanation for why this platform is skipped or included." + } + } + }, + "PlatformFilter": { + "type": "array", + "items": { + "$ref": "#/$defs/PlatformFilterEntry" + }, + "minItems": 1 + }, + "EvaluationContext": { + "type": "object", + "required": [ + "targetingKey", + "attributes" + ], + "additionalProperties": false, + "properties": { + "targetingKey": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": "object" + } + } + }, + "ExpectedResult": { + "type": "object", + "required": [ + "value" + ], + "additionalProperties": false, + "properties": { + "value": { + "description": "Expected flag value; must match the evaluation variationType." + }, + "reason": { + "type": [ + "string", + "null" + ], + "description": "Expected OpenFeature reason code." + }, + "errorCode": { + "type": [ + "string", + "null" + ], + "description": "Expected OpenFeature error code." + }, + "variantKey": { + "type": [ + "string", + "null" + ], + "description": "Expected variation key." + } + } + }, + "Evaluation": { + "type": "object", + "required": [ + "flag", + "variationType", + "defaultValue", + "expectedResult" + ], + "additionalProperties": false, + "properties": { + "flag": { + "type": "string", + "description": "Flag key to evaluate." + }, + "variationType": { + "type": "string", + "enum": [ + "boolean", + "string", + "integer", + "float", + "object" + ], + "description": "Type to request from the SDK getter." + }, + "defaultValue": { + "description": "Default value passed to the getter; must match variationType." + }, + "expectedResult": { + "$ref": "#/$defs/ExpectedResult" + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter" + }, + "_include": { + "$ref": "#/$defs/PlatformFilter" + } + }, + "allOf": [ + { + "if": { + "properties": { + "variationType": { + "const": "boolean" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "boolean" + }, + "expectedResult": { + "properties": { + "value": { + "type": "boolean" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "string" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "string" + }, + "expectedResult": { + "properties": { + "value": { + "type": "string" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "integer" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "integer" + }, + "expectedResult": { + "properties": { + "value": { + "type": "integer" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "float" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "number" + }, + "expectedResult": { + "properties": { + "value": { + "type": "number" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "object" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "object" + }, + "expectedResult": { + "properties": { + "value": { + "type": "object" + } + } + } + } + } + } + ], + "not": { + "required": [ + "_skip", + "_include" + ] + } + }, + "EventMatcher": { + "type": "object", + "required": [ + "_count" + ], + "description": "Matches received events by subset. All keys not prefixed with _ are match fields compared against event data. Control fields: _count (required), _skip, _include.", + "properties": { + "_count": { + "type": "integer", + "minimum": 1, + "description": "Number of events this matcher should consume." + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter" + }, + "_include": { + "$ref": "#/$defs/PlatformFilter" + } + }, + "not": { + "required": [ + "_skip", + "_include" + ] + } + }, + "Expectations": { + "type": "object", + "additionalProperties": false, + "properties": { + "noUnmatchedEvents": { + "type": "boolean", + "default": false, + "description": "When true, assert that all received events (exposures and evaluations) are consumed by matchers. No extra events allowed." + }, + "exposures": { + "type": "array", + "items": { + "$ref": "#/$defs/EventMatcher" + }, + "description": "Ordered, exclusive matchers for exposure events." + }, + "evaluationEvents": { + "type": "array", + "items": { + "$ref": "#/$defs/EventMatcher" + }, + "description": "Ordered, exclusive matchers for flagevaluation events." + } + } + } + }, + "allOf": [ + { + "if": { + "required": [ + "expectedResponseError" + ] + }, + "then": { + "properties": { + "evaluations": { + "maxItems": 0 + } + } + }, + "else": { + "properties": { + "evaluations": { + "minItems": 1 + } + } + } + } + ] +} From 3acee3462d2567d38d0dacc02979053c2e0336bb Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 14:34:03 -0500 Subject: [PATCH 37/44] FFL-3568: Import upstream serial-ID response coverage --- .../test/resources/precomputed/SOURCE.json | 5 +- .../emissions/serial-id-on-exposure.json | 109 ++++++++++++++++++ .../src/test/resources/precomputed/index.json | 1 + 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json index c4da2bf85a..f5ddb0506e 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -1,12 +1,13 @@ { "repository": "https://github.com/DataDog/ffe-system-test-data.git", - "commit": "e4f93740f783ddc0823be5f15daaf78c01869d48", + "commit": "23d948da7cd639be738c1a040264b0986574f973", "files": { "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", "README.md": "0c29b94146aa81088dc0e70435019f6d8bded371a068d566768ecdb2e7cb86e1", "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", + "cases/emissions/serial-id-on-exposure.json": "83a7504e6658b875ececdd1972c04386130feb31c607ccf7b61cd45ce46c1fc0", "cases/evaluator/all-types-success.json": "ff22e47d5bda8a1fef19892c853e4396ccdb8509a3741888eb69169847fc4fde", "cases/evaluator/defaults-and-emission-gates.json": "fd9c306086b7ce87cacd7f69d2f2c7af03921fffa9b43455dcc89dc17f3c3bfd", "cases/evaluator/empty-flags-response.json": "f9ddb39e6a425b2c3f7884c7bdf92516e5ebac5b0ed4d5b7be6aa5e38950ea99", @@ -42,7 +43,7 @@ "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", - "index.json": "07d8036a97955a47a5ea5e840c7f612f69b2f13bce67ddbd636d2a6dfdde6a43", + "index.json": "84c8751d9be27e2e86662a69de20b01ea273a472a13c6675d6c531af81f1cd8f", "schema.json": "0353d393b1b4193346b2fa907f500b5c60be51318759c3afe52e549f1662195d" } } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json new file mode 100644 index 0000000000..11e9bcfed6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json @@ -0,0 +1,109 @@ +{ + "name": "serial-id-on-exposure", + "description": "Validates that serialId from the precomputed assignment is forwarded as serial_id on the exposure event. Covers: present serialId, absent serialId, serialId of 0 (must not be treated as absent), and null serialId.", + "context": { + "targetingKey": "serial-id-test-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "flags": { + "serial.id.flag": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "no.serial.id.flag": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "zero.serial.id.flag": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "null.serial.id.flag": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + } + } + } + }, + "evaluations": [ + { + "flag": "serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "no.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "zero.serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "null.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + {"_count": 1, "serial_id": 340132}, + {"_count": 1, "serial_id": null}, + {"_count": 1, "serial_id": 0}, + {"_count": 1, "serial_id": null} + ], + "evaluationEvents": [ + {"_count": 4} + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json index 336c33dbb7..dd41792734 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/index.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -1,6 +1,7 @@ [ "cases/emissions/all-doLog-false.json", "cases/emissions/duplicate-evaluation.json", + "cases/emissions/serial-id-on-exposure.json", "cases/evaluator/all-types-success.json", "cases/evaluator/defaults-and-emission-gates.json", "cases/evaluator/empty-flags-response.json", From 1d772688fe1d6b91776057d9c15e9a09b6dc03ad Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 16:19:08 -0500 Subject: [PATCH 38/44] FFL-3568: Apply lossless numeric contract and shared boundaries --- detekt_custom_safe_calls_third_party.yml | 4 + detekt_custom_unsafe_calls.yml | 2 + features/dd-sdk-android-flags/README.md | 10 +- .../flags/internal/FlagValueConverter.kt | 101 +- .../flags/internal/DatadogFlagsClientTest.kt | 4 +- .../src/test/resources/precomputed/README.md | 15 +- .../test/resources/precomputed/SOURCE.json | 26 +- .../evaluator/numeric-boundaries-float.json | 1093 ++++++++++++++++ .../evaluator/numeric-boundaries-integer.json | 1093 ++++++++++++++++ .../evaluator/numeric-boundaries-number.json | 1093 ++++++++++++++++ .../cases/evaluator/numeric-float.json | 402 ++++-- .../cases/evaluator/numeric-integer.json | 430 ++++++- .../cases/evaluator/numeric-number.json | 358 +++++- .../evaluator/type-mismatch-all-types.json | 39 +- .../obfuscation-numeric-boundaries-float.json | 1094 +++++++++++++++++ ...bfuscation-numeric-boundaries-integer.json | 1094 +++++++++++++++++ ...obfuscation-numeric-boundaries-number.json | 1094 +++++++++++++++++ .../obfuscation-numeric-float.json | 410 ++++-- .../obfuscation-numeric-integer.json | 438 ++++++- .../obfuscation-numeric-number.json | 366 +++++- .../src/test/resources/precomputed/index.json | 6 + 21 files changed, 8647 insertions(+), 525 deletions(-) create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index 1b62929c05..5647735a35 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -367,6 +367,7 @@ datadog: - "java.lang.StringBuilder.append(kotlin.String?)" - "java.lang.StringBuilder.clear()" - "java.lang.StringBuilder.constructor()" + - "java.math.BigDecimal.compareTo(java.math.BigDecimal?)" - "java.math.BigInteger.toHexString()" - "java.math.BigInteger.toLong()" - "java.nio.ByteBuffer.duplicate()" @@ -435,6 +436,8 @@ datadog: - "kotlin.Result.getOrElse(kotlin.Function1)" - "kotlin.Result.map(kotlin.Function1)" - "kotlin.Result.onFailure(kotlin.Function1)" + - "kotlin.Result.success(kotlin.Double)" + - "kotlin.Result.success(kotlin.Int)" # endregion # region Kotlin Collections - "kotlin.Array.all(kotlin.Function1)" @@ -883,6 +886,7 @@ datadog: - "kotlin.Char.isLowSurrogate()" - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" + - "kotlin.Double.isFinite()" - "kotlin.Double.coerceAtMost(kotlin.Double)" - "kotlin.Double.coerceIn(kotlin.Double, kotlin.Double)" - "kotlin.Double.isNaN()" diff --git a/detekt_custom_unsafe_calls.yml b/detekt_custom_unsafe_calls.yml index aadb89f3b1..4b1ce65416 100644 --- a/detekt_custom_unsafe_calls.yml +++ b/detekt_custom_unsafe_calls.yml @@ -167,7 +167,9 @@ datadog: - "java.lang.System.loadLibrary(kotlin.String?):java.lang.SecurityException,java.lang.UnsatisfiedLinkError,java.lang.NullPointerException" - "java.lang.reflect.Field.get(kotlin.Any):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" - "java.lang.reflect.Field.get(kotlin.Any?):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" + - "java.math.BigDecimal.constructor(kotlin.Double):java.lang.NumberFormatException" - "java.math.BigDecimal.intValueExact():java.lang.ArithmeticException" + - "java.math.BigDecimal.valueOf(kotlin.Double):java.lang.NumberFormatException" - "java.math.BigInteger.and(java.math.BigInteger?):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.constructor(kotlin.String?, kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.shiftRight(kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index f5d8e8e163..311ffd1949 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -445,11 +445,11 @@ Adding a case requires an upstream fixture change and snapshot refresh. It requi Missing or empty fixtures fail the suite. Shared `_include` and `_skip` fields describe API applicability. Do not add local exclusions to hide failures. -Native Integer getters accept `integer`. Native Double getters accept `float`. -Legacy `number` accepts Double reads and representable integral Integer reads. -A whole-valued `float` still fails an Integer read with `TYPE_MISMATCH`. -React Native's Number API has no separate integer type. -These rules do not enable a new server wire format. Sending specific numeric types requires compatible capability negotiation. +Numeric getters use lossless conversion regardless of the `integer`, `float`, or `number` wire label. +Integer getters accept whole values within their native range. Double getters reject integer values that lose precision. +Fractional Integer reads, range overflow, and inexact conversions return `TYPE_MISMATCH`. +React Native exposes one Number getter. Its tests exclude cases that require a separate Integer getter or native integer precision. +The service keeps sending `number`. This work adds no numeric capability. Cases cover values, defaults, errors, reasons, variants, emission gates, duplicate evaluations, and numeric boundaries. Obfuscation cases cover fixed digests, salts, Unicode, whitespace, malformed metadata, and plaintext event identity. diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt index 404d8813da..89262ae994 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt @@ -8,6 +8,7 @@ package com.datadog.android.flags.internal import com.datadog.android.flags.internal.model.VariationType import org.json.JSONObject +import java.math.BigDecimal import java.util.Locale import kotlin.reflect.KClass @@ -48,44 +49,72 @@ internal object FlagValueConverter { ) } - return runCatching { - @Suppress("UNCHECKED_CAST") - val result: T? = when (targetType) { - Boolean::class -> variationValue.lowercase(Locale.US).toBooleanStrictOrNull() as? T - String::class -> variationValue as T - Int::class -> { - val number = variationValue.toBigDecimalOrNull() - val integer = try { - number?.intValueExact() - } catch (_: ArithmeticException) { - null + return when (targetType) { + Int::class -> losslessInt(variationValue) as Result + Double::class -> losslessDouble(variationValue) as Result + else -> runCatching { + @Suppress("UNCHECKED_CAST") + val result: T? = when (targetType) { + Boolean::class -> variationValue.lowercase(Locale.US).toBooleanStrictOrNull() as? T + String::class -> variationValue as T + Map::class -> variationValue.toMap() as? T + JSONObject::class -> { + @Suppress("UnsafeThirdPartyFunctionCall") // Safe: wrapped in runCatching + val json = JSONObject(variationValue) + @Suppress("UNCHECKED_CAST") + json as? T } - if (integer == null && number != null) { - throw TypeMismatchException("Flag value cannot be represented as an Int") - } - integer as? T - } - Double::class -> variationValue.toDoubleOrNull() as? T - Map::class -> variationValue.toMap() as? T - JSONObject::class -> { - @Suppress("UnsafeThirdPartyFunctionCall") // Safe: wrapped in runCatching - val json = JSONObject(variationValue) - @Suppress("UNCHECKED_CAST") - json as? T - } - else -> { - // Check if targetType is a Map implementation - // Safe: isAssignableFrom is a standard Java reflection call - @Suppress("UnsafeThirdPartyFunctionCall") - if (Map::class.java.isAssignableFrom(targetType.java)) { - variationValue.toMap() as? T - } else { - null + else -> { + // Check if targetType is a Map implementation + // Safe: isAssignableFrom is a standard Java reflection call + @Suppress("UnsafeThirdPartyFunctionCall") + if (Map::class.java.isAssignableFrom(targetType.java)) { + variationValue.toMap() as? T + } else { + null + } } } + + result ?: throw IllegalArgumentException("Failed to parse value '$variationValue'") } + } + } - result ?: throw IllegalArgumentException("Failed to parse value '$variationValue'") + private fun losslessInt(value: String): Result { + val number = value.toBigDecimalOrNull() + ?: return Result.failure(IllegalArgumentException("Failed to parse value '$value'")) + val integer = try { + number.intValueExact() + } catch (_: ArithmeticException) { + null + } + return if (integer != null) { + Result.success(integer) + } else { + Result.failure(TypeMismatchException("Flag value cannot be represented as an Int")) + } + } + + private fun losslessDouble(value: String): Result { + val number = value.toBigDecimalOrNull() + val double = value.toDoubleOrNull() + if (number == null || double == null) { + return Result.failure(IllegalArgumentException("Failed to parse value '$value'")) + } + // Accept the exact binary value or its standard JSON decimal round trip. + val exact = try { + double.isFinite() && ( + number.compareTo(BigDecimal(double)) == 0 || + number.compareTo(BigDecimal.valueOf(double)) == 0 + ) + } catch (_: NumberFormatException) { + false + } + return if (exact) { + Result.success(double) + } else { + Result.failure(TypeMismatchException("Flag value cannot be represented as a Double")) } } @@ -99,9 +128,9 @@ internal object FlagValueConverter { fun isTypeCompatible(variationType: String, targetType: KClass<*>): Boolean = when (targetType) { Boolean::class -> variationType == VariationType.BOOLEAN.value String::class -> variationType == VariationType.STRING.value - Int::class -> variationType == VariationType.INTEGER.value || variationType == VariationType.NUMBER.value - Double::class -> - variationType == VariationType.NUMBER.value || variationType == VariationType.FLOAT.value + Int::class, Double::class -> + variationType == VariationType.INTEGER.value || + variationType == VariationType.NUMBER.value || variationType == VariationType.FLOAT.value JSONObject::class -> variationType == VariationType.OBJECT.value Map::class -> variationType == VariationType.OBJECT.value else -> { diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt index ba30be9a69..ea34486017 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt @@ -1587,7 +1587,7 @@ internal class DatadogFlagsClientTest { } @Test - fun `M accept number and float but reject int W resolveDoubleValue() { declared types }`(forge: Forge) { + fun `M accept number, float and int W resolveDoubleValue() { compatible types }`(forge: Forge) { // Given val fakeFlagKey1 = forge.anAlphabeticalString() val fakeFlagKey2 = forge.anAlphabeticalString() @@ -1628,7 +1628,7 @@ internal class DatadogFlagsClientTest { // Then assertThat(result1).isEqualTo(fakeFlagValue) assertThat(result2).isEqualTo(fakeFlagValue) - assertThat(result3).isEqualTo(fakeDefaultValue) + assertThat(result3).isEqualTo(fakeFlagIntValue.toDouble()) } @Test diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/README.md b/reliability/single-fit/flags/src/test/resources/precomputed/README.md index 0a07911689..cf76ab45c6 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/README.md +++ b/reliability/single-fit/flags/src/test/resources/precomputed/README.md @@ -180,11 +180,16 @@ python3 ci/validate-precomputed-fixtures.py ## Mobile conformance and numeric types -Native Integer getters accept `integer`. Native Double getters accept `float`. -The declared type controls this decision even when a `float` value is whole. -Legacy `number` accepts Double reads and representable integral Integer reads. -Fractional Integer reads return the default with `TYPE_MISMATCH`. -Single-Number APIs use the explicit evaluation filters for cross-type tests. +Numeric reads depend on the value, not the `integer`, `float`, or `number` label. +Integer getters accept whole values within the accessor range. Double getters +accept numeric values without losing integer precision. Fractional Integer reads, +out-of-range reads, and inexact conversions return the default with `TYPE_MISMATCH`. +No new global safe-integer limit is imposed. React Native and web expose one Number +getter; Integer-only cases use explicit platform filters. Boundary fixtures cover +native 32-bit and 64-bit accessors separately. + +The precompute service retains `number`. No numeric capability is introduced. +See FFL-3568 and FFL-3564 for the lossless conversion decision. Discover cases recursively. Do not maintain a filename list. Fail when the corpus is missing or empty. Report the fixture revision, executed evaluations, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json index f5ddb0506e..eae45353c7 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -1,10 +1,10 @@ { "repository": "https://github.com/DataDog/ffe-system-test-data.git", - "commit": "23d948da7cd639be738c1a040264b0986574f973", + "commit": "0feda21aec6710a5243287fbc58a33ca2f658b16", "files": { "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", - "README.md": "0c29b94146aa81088dc0e70435019f6d8bded371a068d566768ecdb2e7cb86e1", + "README.md": "9d2d59f1d66752d0c0e3828217fc7593a6ffed6992e7cf6e417a61ca336b8fd1", "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", "cases/emissions/serial-id-on-exposure.json": "83a7504e6658b875ececdd1972c04386130feb31c607ccf7b61cd45ce46c1fc0", @@ -15,16 +15,22 @@ "cases/evaluator/integer-precision-native-only.json": "96c4bed183bfeeab7b59209dcdf5b36726e417f1035a28198ef95642ea673622", "cases/evaluator/multi-flag-set.json": "b1c76977764c1bc11f430a50d33a06bb33daa60ed0d184547250362559f34a0b", "cases/evaluator/multiple-reasons.json": "812b48f5131814fa7e13b3779b47f8805e59a7b063b5737b17139d01111c0134", - "cases/evaluator/numeric-float.json": "f8ce19ac8b0cf9e5750a9eb54975e056bfa53e358015e6b36e84025e3799eecd", - "cases/evaluator/numeric-integer.json": "9522fe4f68e3b8a48aac3cb2ed60fd254ced98f9b8d29eb61eeaa32ce521388c", - "cases/evaluator/numeric-number.json": "dc3b2b0086f9fb0d24e5e5f41249bbb78a77f5880adf22f05e288e9191a8077a", + "cases/evaluator/numeric-boundaries-float.json": "f986d58267e590f1be98dc9fe4b5151852c9565028469735b30885a6f369eaa9", + "cases/evaluator/numeric-boundaries-integer.json": "4569c35d977fff5c77134b9e0342b740b7a18e4cf01e461f0c8b5ed9c250708b", + "cases/evaluator/numeric-boundaries-number.json": "ab1e965ac0b335bf56442c0bfa262ba62214b3118a1c1cda6512f38b38717294", + "cases/evaluator/numeric-float.json": "d889bf0107d350e47fde325836caf6690c643b779c51fc3812f948e22a8c67ce", + "cases/evaluator/numeric-integer.json": "4d080f862f53c6d7fab3670a3793edb1ddd9e566a68e262e59abfaff689db39d", + "cases/evaluator/numeric-number.json": "cf2aafebf871b57f0f3a74abf0126d6a4f35cb18d78602611ee4419e6f9235d9", "cases/evaluator/object-nested-deep.json": "22f8f0c516cfe0456d34697f29bbf39d7736559e0b188de4671d0fee6545fd3d", "cases/evaluator/special-characters-in-keys.json": "07e24ba1a407ef9604af0bc5ab3f8faafe199dd83db94d96d0e3d689430054ce", - "cases/evaluator/type-mismatch-all-types.json": "1c4eb5a6407cca9ca9c8e7d5fd09d241cb8296cb5b42c9a56432cc278a86933a", + "cases/evaluator/type-mismatch-all-types.json": "0904425126ae64794a8c67d13d4bab759a88abc75c27c99f71c514babb9279a3", "cases/obfuscation/obfuscation-keys.json": "3af487d59d5a7064e0e8e6508343db21689180d7d6e862b055934036e1716643", - "cases/obfuscation/obfuscation-numeric-float.json": "92760c90a0d289fc9c8ae38323bc81b5c6a5608d43a05b7b9c4831c097b971bd", - "cases/obfuscation/obfuscation-numeric-integer.json": "eab193a1eb876522c37eb041e704efa91a00c0c92304d52a65897882e589dc35", - "cases/obfuscation/obfuscation-numeric-number.json": "d05e213350654a8f4fd08883316dd10ed7e2a5be51383b8a3c94fd025027833b", + "cases/obfuscation/obfuscation-numeric-boundaries-float.json": "56b4343a717161047ce32138f564c2558a1da49f37f26b7681f94b3cc0ef5473", + "cases/obfuscation/obfuscation-numeric-boundaries-integer.json": "3f2340a46e9cdecd48d07cc2aacdf66cba66fb192ed300e42dda4bbf50e415d7", + "cases/obfuscation/obfuscation-numeric-boundaries-number.json": "bca42ecb5cbb3a6ba08a6adbfa34690accaf986a07bf30a91b764871db673566", + "cases/obfuscation/obfuscation-numeric-float.json": "57ec0a367eb86a94269e353b7ceb28a83ac96a546a779b36efa115fd50464c69", + "cases/obfuscation/obfuscation-numeric-integer.json": "f3954848aa7776f4aed3d05e23445f0a66d5c5aaa461e526db335d7034dbc015", + "cases/obfuscation/obfuscation-numeric-number.json": "c429b7ed0a5e1131b25b1240739c1b5a17b046f833f7d54d74f60d90642c0c2d", "cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "452e57fbcec5a254c65295014ae11675d406a87567e6887933ba493a14109093", "cases/obfuscation/obfuscation-reject-missing-descriptor.json": "f0105da777c3b35ff10c7082e62ddd1f7ce6a4bc15dae27302f279b18b3bfc29", "cases/obfuscation/obfuscation-reject-missing-marker.json": "4fb270faacdcfe92477120f9bf9ea56f98e1503cf992c72da71ecc6fbca5e0dc", @@ -43,7 +49,7 @@ "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", - "index.json": "84c8751d9be27e2e86662a69de20b01ea273a472a13c6675d6c531af81f1cd8f", + "index.json": "e3fc034a98f756a4283e1a10251800fa30d20deac2268c0193a3682ceeb679e6", "schema.json": "0353d393b1b4193346b2fa907f500b5c60be51318759c3afe52e549f1662195d" } } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json new file mode 100644 index 0000000000..c7c4335c2b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-float", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json new file mode 100644 index 0000000000..4177018bd3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-integer", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json new file mode 100644 index 0000000000..4be914128c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-number", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json index 4bbd9d3a7c..dd8b3cb87b 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json @@ -1,6 +1,6 @@ { "name": "numeric-float", - "description": "numeric float.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,85 +10,101 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "float-0-as-integer": { + "numeric-float-0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float-0-as-float": { + "numeric-float-1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float-5-as-integer": { + "numeric-float-2": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float-5-as-float": { + "numeric-float-3": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float--5-as-integer": { + "numeric-float-4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float--5-as-float": { + "numeric-float-5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float-1.25-as-integer": { + "numeric-float-6": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float-1.25-as-float": { + "numeric-float-7": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float--1.25-as-integer": { + "numeric-float-8": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "float--1.25-as-float": { + "numeric-float-9": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "numeric-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } } } @@ -96,28 +112,18 @@ }, "evaluations": [ { - "flag": "float-0-as-integer", + "flag": "numeric-float-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { - "value": -99, - "reason": "ERROR", - "errorCode": "TYPE_MISMATCH", - "variantKey": null - }, - "_skip": [ - { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - }, - { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - } - ] + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } }, { - "flag": "float-0-as-float", + "flag": "numeric-float-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -128,28 +134,18 @@ } }, { - "flag": "float-5-as-integer", + "flag": "numeric-float-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { - "value": -99, - "reason": "ERROR", - "errorCode": "TYPE_MISMATCH", - "variantKey": null - }, - "_skip": [ - { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - }, - { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - } - ] + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } }, { - "flag": "float-5-as-float", + "flag": "numeric-float-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -160,7 +156,29 @@ } }, { - "flag": "float--5-as-integer", + "flag": "numeric-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -169,30 +187,38 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float--5-as-float", + "flag": "numeric-float-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -5, + "value": 1.25, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "float-1.25-as-integer", + "flag": "numeric-float-8", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -201,30 +227,38 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float-1.25-as-float", + "flag": "numeric-float-9", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": 1.25, + "value": -0.5, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "float--1.25-as-integer", + "flag": "numeric-float-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -233,23 +267,31 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float--1.25-as-float", + "flag": "numeric-float-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -1.25, + "value": 1.2, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" @@ -258,33 +300,203 @@ ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-float-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + } + } + ], "evaluationEvents": [ { - "_count": 10, - "_skip": [ + "_count": 1, + "flag": { + "key": "numeric-float-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "Single Number API." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 5, + "_count": 1, + "flag": { + "key": "numeric-float-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + }, + "evaluation_count": 1 } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json index 381cfa53a1..e1ce07409f 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json @@ -1,6 +1,6 @@ { "name": "numeric-integer", - "description": "numeric integer.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,53 +10,101 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "integer-0-as-integer": { + "numeric-integer-0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "integer-0-as-float": { + "numeric-integer-1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "integer-5-as-integer": { + "numeric-integer-2": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "integer-5-as-float": { + "numeric-integer-3": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "integer--5-as-integer": { + "numeric-integer-4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "integer--5-as-float": { + "numeric-integer-5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "numeric-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } } } @@ -64,7 +112,7 @@ }, "evaluations": [ { - "flag": "integer-0-as-integer", + "flag": "numeric-integer-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -75,120 +123,380 @@ } }, { - "flag": "integer-0-as-float", + "flag": "numeric-integer-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -99.5, + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "integer-5-as-integer", - "variationType": "integer", - "defaultValue": -99, + "flag": "numeric-integer-7", + "variationType": "float", + "defaultValue": -99.5, "expectedResult": { - "value": 5, + "value": 1.25, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "integer-5-as-float", - "variationType": "float", - "defaultValue": -99.5, + "flag": "numeric-integer-8", + "variationType": "integer", + "defaultValue": -99, "expectedResult": { - "value": -99.5, + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "integer--5-as-integer", - "variationType": "integer", - "defaultValue": -99, + "flag": "numeric-integer-9", + "variationType": "float", + "defaultValue": -99.5, "expectedResult": { - "value": -5, + "value": -0.5, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "integer--5-as-float", - "variationType": "float", - "defaultValue": -99.5, + "flag": "numeric-integer-10", + "variationType": "integer", + "defaultValue": -99, "expectedResult": { - "value": -99.5, + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] + }, + { + "flag": "numeric-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } } ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-integer-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + } + } + ], "evaluationEvents": [ { - "_count": 6, - "_skip": [ + "_count": 1, + "flag": { + "key": "numeric-integer-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-6" + }, + "evaluation_count": 1, + "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 3, + "_count": 1, + "flag": { + "key": "numeric-integer-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + }, + "evaluation_count": 1 } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json index 946151c604..c97208a848 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json @@ -1,6 +1,6 @@ { "name": "numeric-number", - "description": "numeric number.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,85 +10,101 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "number-0-as-integer": { + "numeric-number-0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number-0-as-float": { + "numeric-number-1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number-5-as-integer": { + "numeric-number-2": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number-5-as-float": { + "numeric-number-3": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number--5-as-integer": { + "numeric-number-4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number--5-as-float": { + "numeric-number-5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number-1.25-as-integer": { + "numeric-number-6": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number-1.25-as-float": { + "numeric-number-7": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number--1.25-as-integer": { + "numeric-number-8": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "number--1.25-as-float": { + "numeric-number-9": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "numeric-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } } } @@ -96,7 +112,7 @@ }, "evaluations": [ { - "flag": "number-0-as-integer", + "flag": "numeric-number-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -107,7 +123,7 @@ } }, { - "flag": "number-0-as-float", + "flag": "numeric-number-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -118,7 +134,7 @@ } }, { - "flag": "number-5-as-integer", + "flag": "numeric-number-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -129,7 +145,7 @@ } }, { - "flag": "number-5-as-float", + "flag": "numeric-number-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -140,7 +156,7 @@ } }, { - "flag": "number--5-as-integer", + "flag": "numeric-number-4", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -151,7 +167,7 @@ } }, { - "flag": "number--5-as-float", + "flag": "numeric-number-5", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -162,7 +178,7 @@ } }, { - "flag": "number-1.25-as-integer", + "flag": "numeric-number-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -171,19 +187,27 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "number-1.25-as-float", + "flag": "numeric-number-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -194,7 +218,47 @@ } }, { - "flag": "number--1.25-as-integer", + "flag": "numeric-number-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -203,23 +267,31 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "number--1.25-as-float", + "flag": "numeric-number-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -1.25, + "value": 1.2, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" @@ -228,33 +300,203 @@ ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-number-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + } + } + ], "evaluationEvents": [ { - "_count": 10, - "_skip": [ + "_count": 1, + "flag": { + "key": "numeric-number-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-6" + }, + "evaluation_count": 1, + "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 8, + "_count": 1, + "flag": { + "key": "numeric-number-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + }, + "evaluation_count": 1 } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json index 2fd77213f9..801ecd1a47 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json @@ -1,6 +1,6 @@ { "name": "type-mismatch-all-types", - "description": "Wrong typed getters return the caller default with TYPE_MISMATCH. APIs with one Number getter do not implement integer-versus-float mismatch.", + "description": "Wrong nonnumeric getters return TYPE_MISMATCH; an exact numeric conversion succeeds.", "context": { "targetingKey": "mismatch-user", "attributes": {} @@ -84,21 +84,11 @@ "variationType": "float", "defaultValue": 0.0, "expectedResult": { - "value": 0.0, - "reason": "ERROR", - "errorCode": "TYPE_MISMATCH", - "variantKey": null - }, - "_skip": [ - { - "platform": "web", - "reason": "The public API exposes one Number getter." - }, - { - "platform": "react-native", - "reason": "The public API exposes one Number getter." - } - ] + "value": 7, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "var-int" + } }, { "flag": "fixture.mismatch.float-as-bool", @@ -125,19 +115,14 @@ ], "expectations": { "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], "evaluationEvents": [ { - "_count": 5, - "_skip": [ - { - "platform": "web", - "reason": "Single Number API." - }, - { - "platform": "react-native", - "reason": "Single Number API." - } - ] + "_count": 5 } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json new file mode 100644 index 0000000000..4ad1d6942e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-float", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "2aec5b3d4477769702ec52c42349be07a31c413ebffb69196b762523adf205ab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ccd840e29b0846bbc8ace7da7bc20c300b2a14e1f5f68961f2fb76e69fb0ba04": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ebe608c5e7ab0bf15e0c6585810759616ab5881130d7ddb956ec8749c43a0939": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1f2de3a9ac37932ca8fbeaafe88046f8f7a49075c87eec5b805a0967706685a4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "431adeb8d8d06d55e943812fd5cf8bf6095858eb898fd3133999c3d163607b3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2c7494350d45235245c36589fe36990e14f5e3eca4522e7c93b05ec4e4e3de7c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "19b8019e6dc73fdabe4884fd109c0c0865eb682b5a6e9b2c098d8698cb8a25bf": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "05dce5b4f6ec9f670d767a9e2f3095c91dc5f947c0cf65f26ffe0d34d169c27a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b31d785097e2b21ff4203b7f42d9601f0fe117e04ae52fd41c38ec226ee7c9dd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2561c2c5e59e5ee958e65c5ffc5afdc82deab8caacc24b7fd955426d38f3f10a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a4378fa74480657ce942442b4755051dfe8254b3a869ef1d09e73b70905931ba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4c973da42e257593047a0e547423acae08bcb5d6529670d6917dc653d559878d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6291d2c93c31947490a82ac572b34ff0a2cfc6d1bf63ee929bd4c427e4c14e46": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ed9c529fd1165fc1f8560f540dd0741dd240473987473db250c027486ca54481": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "87217664c8cdba2054ae7ba5e0513f1fe0932a93deb52ee46c8c4856db50bfaf": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "435af2981e1d1cf3e2e762b9fbb1b99f4850d1b0d4bed9de0f44f0418b223506": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "608602ea98e8c47ffb9f86e51e8093daa81ff902841d774092b32df7f92421e2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ae43ae724ec81be659b9cbd89315102b127b1b9e24e26def87496eb4a8aa38ab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2358b34f81306a3acd892893294abea0c33721a65b06e355cda36c8e54739be5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9fc154f59fba732da1a2be979ec48044cd88f185fd90b9cfc306e53f3f6d6a88": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "350edcb8f9921229dea893c8a3948457281d0ef88537c3c8905a4dd924165228": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ff68a68d0c6c04348089c33052aad256a51086ae365496d7ecf89a55c9a4d70": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00112233445566778899aabbccddeeff" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json new file mode 100644 index 0000000000..9f6a4ef37d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-integer", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "7c5c55bbc1d947ebd29d4d22bbb8e854fb84a3168ec16c9b88ff1eb8048f1259": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1db93bef085224805fc0b94e254d8efea2ad144e255512c7822ba1941efd37ca": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c65f42d21a69e9e63e768bb7c353ccc659d8fa7fda19d552c46571fe747939a1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "57ea520510c0e0ffdf6c016032a1ad8fd7e930feddb56e512c5ba5e176bb747e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3bae488ac7f7c5e64e8d80c5e5d3a01fede98ee9dc1dbfaaa1602bdb834e3199": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d7e31a5af23eaed253fbab5d5ba775a49cc869265a762861623b9850ca45200e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e214fbcdc5fa2e6b53e80a737cf3b01bf720e1da6bdc8d1a8126c82fae131cc5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f87c7ffcbfc101c063bc2f6533d4a73054a4c3e0cc59b865b6d9a738cded6776": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "24e2195a49630a3b88efcb5227d7044651ffa1e483006af5ef3e41db492ca96a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "45316309ea93c9d4d63ebd704488bd9b862cb9d30303b54ccbc668a45f1a9cfd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "107e31005fc784e93f438721722b248ed66b8ed91d13eefca267786102fe5a81": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ca603c439f5fa9a01c76f4febf58bf0b1034e100dd37fd1731c05f667409846e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bf801a713f62854431659efc54a0cb312dd601a8459d4390b13840fd7f414a02": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6c6bc2ace96267f8a839f207457b3d12b93f8211e7ce9a2f4ef9b2ddfc70412b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cd01bfe31bd7117a646dddc5be9732400e8ee0076cafd79c189115b8b3081950": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fe4005d2eb3448257136861a88114cadcc85f06cb2ede14a36b581d4900e4a5e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fb7e52e4c272d1e4e9602339c4f6a7094257194ea28d274314b8ca0a4aec639b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "de7cefc4a649b9c39f7fe31b49d5fa5f8ac6b792e2227717e0130dd127bbfd70": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e084557c149a57fcf7e04041ad080c04586380baa9093a16dfee198a1ad51989": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f163106c94e153c44a0f9c882c90c6c5771e7df230fb847e3a9426bf18a8f312": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "40aa0011039c7b0946bb1264199ba576db480b932c341fa5a224340056ec0976": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "79ccc13c690d354291ea1fbee6c0b103d273130dd90bb6f3efe51840d1c562ae": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00112233445566778899aabbccddeeff" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json new file mode 100644 index 0000000000..96b428ade2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-number", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "b4ba8411e13132261ecb4f57a72e7ca330d2618c536f5546590abb0cc25b9c47": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "82796957263227e5d42ba913e864aca7aa0f6c06cf30ef1bd6a52cf771a5680e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9889dbdc0d0d8741c09f2d9e1f6b764e37995c66e76452502ff7a73398dc8fdb": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "20c38d5ef57bd5c41cc52d311ab1147afabfcdbb2abbdaf47f9c1d5f71736248": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a989158eea675eb49b1f62e15c2205b1a1168d9ed769e90e16ba8f43e65963c2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fd02ac0da0683586431856ba869c1ff07669b94244e6c8a58c7e1ae8dcb3a04d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0c4bdf647950f33d16dace93f197e1891b73d767a5bc3be0cf1e51a115775f63": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb03a68efa88e918862e8a481ee25893c059765384e7a0835b022f742ecffc05": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "596ef4687c078263b87dd17061e7471add9255b49814d72150bac75dce35787d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "920cb9612332743d61020ef481f4990bf0826040b3d087212a891115e9f30f22": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "62e88abd1cfc891df7f5703503077e82fb99db1e9be9b91d9a6b50333b4d1110": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a99b58377ef7b9e52d6e1c3befd549d61e1ca234eca3cd8d4720c16928d9a7a0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ff66a9c09d22765cf9397d3e156bce434a683325f21ab6df1c9b7359056d676a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9b15e9496c8eb9e19f2e8afc18ac298593ea8d45370e1fceb0f98b0f47e28fdb": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e0d4ba65f888bbf2b204e1ed4cc02d23b0e0c3f8a0f4f22b51501c7532d4781f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9b5089501d028a056aae6a5de88452f98f79340ba32f765a3950c6d8c0a82a9d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1706e7b05d435e118cb4dee1540e2109fd7a7b1b19b08fceec0cfebc8fc0abc1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3769e087d36053e2971961b47640ab9b7397f02b1eddda5d4e90819ef1b1f5bb": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e0cff618c57a7aca3a66fbbb36b72aa53a4d4b7a08a5c67a0e25ffd564cea395": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4d204756440205698d05663c5a511d991d8e7ad64f54b1a257034b8e2c52f80f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4c741509a1ac866498f61619846c2cdec7fd84bbc362ee6ff3f8d563f2156fc8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c9bbbec289c2c83e830e9bffd2ff577daf18bb71371cff7c08a5372ed860d957": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00112233445566778899aabbccddeeff" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json index 6547f39aaa..fdbedade46 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-float", - "description": "numeric float.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,119 +10,125 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "4fc70afd0edbe9f6c3605f50b73892f4167f395a93aa5c2e91c5380818270237": { + "43dcecc88807de972a354bd0b96946419ccf086a3cb1abcaef10a796129ffb7e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "01c7520392b183ba08426484c8981ef7b9574d8d5e1f0b5b0afc04945b1fdc65": { + "ad5896fa25ecdb06cc985d99e469f2b867419c296d99142ba53b7a4ab11d0167": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "30a0b6a28e18bbdd712d677df8b0a0012086219260528d4967214fa45f36f90b": { + "5f920ae1126d6bf5dd3c7cde59b1604dfbae53d4ad4686a548871a4baec00f22": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "b432d3d8d83d0f10acfb8d8099c7dd1238b96b45e12f2407ae6bfd938ad92354": { + "a710178c1d9213803a5ff5f56146c93908327ac97407d76c6139dfdcd9a4012f": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "26a957788e3e02fda8ace878f452615d85c2d80a539a19522b45c9fac903b905": { + "b81af3ac1aba6361e1eb0951f2e1babd617d2218827ce4a14f2c8a5f8005b885": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "fd7b22e26bc6227562127dc3945e30e159e1b522ba003b52706d5b33c8db0519": { + "de8b6921dbff87fdf7182b8a948b9644aa4db01a9dbbda9749a0774f189746cc": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "93cafc55f2224734ccd9a3a0030a8bd007dcded0f48320a16d2044744cc6a4f6": { + "9a8d577fc6f24f49f804fa068b9a7170c9328c0def19a2b2312c9f59b5dfab02": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "5ad25a909e7b8ef40cf13c71a7ec8f2ab4bb8a3ae4a5439029357f9982508bf8": { + "82077f26295c6ead94d7a7a7554ed4e143a4af711f8deb94b8a8a711d032c4f2": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "4d677ea458fe5ed9851dbdba4f92981a432b5b9b467445032483b9fa2d2f7368": { + "8bf2c56f7653627e757f8263815d7408dbcbb6fb86d71d55402e9800f185ed22": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "eb98f328fb6978a74722d54badcaf0335bc7433ca86e205ecca0be0ac86012c8": { + "cef1691308f6e520bbdbc8575683d17bc814f52af8ef42522d6a5bf0aee7e861": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "8b79dc5672bf5c07c2a64b68cd885ba18db98b52639d349283ac6888477326a6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c0b84d2e8690addb339fd433c9cc449d6da6632ea8398d47e9bfebde442247a0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } }, "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "000102030405060708090a0b0c0d0e0f" + "salt": "00112233445566778899aabbccddeeff" } } } }, "evaluations": [ { - "flag": "float-0-as-integer", + "flag": "obfuscation-numeric-float-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { - "value": -99, - "reason": "ERROR", - "errorCode": "TYPE_MISMATCH", - "variantKey": null - }, - "_skip": [ - { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - }, - { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - } - ] + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } }, { - "flag": "float-0-as-float", + "flag": "obfuscation-numeric-float-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -133,28 +139,18 @@ } }, { - "flag": "float-5-as-integer", + "flag": "obfuscation-numeric-float-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { - "value": -99, - "reason": "ERROR", - "errorCode": "TYPE_MISMATCH", - "variantKey": null - }, - "_skip": [ - { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - }, - { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." - } - ] + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } }, { - "flag": "float-5-as-float", + "flag": "obfuscation-numeric-float-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -165,7 +161,29 @@ } }, { - "flag": "float--5-as-integer", + "flag": "obfuscation-numeric-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-float-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -174,30 +192,38 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float--5-as-float", + "flag": "obfuscation-numeric-float-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -5, + "value": 1.25, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "float-1.25-as-integer", + "flag": "obfuscation-numeric-float-8", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -206,30 +232,38 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float-1.25-as-float", + "flag": "obfuscation-numeric-float-9", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": 1.25, + "value": -0.5, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "float--1.25-as-integer", + "flag": "obfuscation-numeric-float-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -238,23 +272,31 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "float--1.25-as-float", + "flag": "obfuscation-numeric-float-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -1.25, + "value": 1.2, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" @@ -263,48 +305,218 @@ ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-0" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-1" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-3" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-4" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-7" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-9" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-11" + } + } + ], "evaluationEvents": [ { - "_count": 10, - "_skip": [ + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "Single Number API." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 5, + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-float-11" + }, + "evaluation_count": 1 } ] }, "_include": [ { "platform": "ios", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "android", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "flutter", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json index 7380abfe85..4b638c4914 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-integer", - "description": "numeric integer.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,66 +10,114 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "3a56e171b0efb081585be54227ac1792ebae79900fb0e0ee2c3850ebc7dd7fd0": { + "72101f81c2d474893bb90b2e9fbece5750b83659171b75d423eba3c8caa09af8": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "3df507231bc602ed78f6e01db4497a88b33c086a455deba043f35dde999c0b7d": { + "158853f6fec44bdd14c7dcf732538d1932393333da0d38e5b1cf389febab173a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "58e7c159ddb85fe9250395e7797d80ecb7dca88e1a22767261a9482611baa358": { + "5e716e405677ef15a943d1e3ec8577a5436ad2b08b619801fa115e3f6056e7e1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "a4ef458efa0a3876a8509351d70fa007a68460b21c64cc64680c1099ec450dab": { + "55aa24315699bb2e6b22e1e60dbc59588628d4987393d1709e63283f740da782": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "01c7f586a7f7bd98d4cbc75760f506b6ff8a9ba7195e88f975a708cc7b6ebb30": { + "11dbbd2cc435f0d8521f492798f5cdeda39b44612afb5b2e8dbbca244eac8afb": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "14a559f9d240120a5038b7f3f6529a0cba11d3cbc84409c84678290f4116b809": { + "5f600bc0a426abe99c1d0f650b9b8eb243179105c4a73a59f696b99967088fcd": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "422fd4f38b4009a3443ac2334e4687d21e2eb02d7614a3eec83c7630585d7f4e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "77e8ec1779ffe7a8edd77b8c84358cae99993a9898b4c0cadf2336a88ae99ccd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6779f3038ba17b00bc22bd42f0fb7078c7a3cf7843adc7884e269ad0b33a0180": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7224ebe3cb4e571ec923f2624b68bd3e183c199258e5523e6456b62e9254f8e5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9483e1046878fedc712a9345a3e8406df5b24d42f7e0d4500ff8cc9fb1c9a161": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e3e9c957b3953d38da8ada7fadcb6bea2ce78691d7cc34dbc2c75297ee4c5a95": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } }, "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "000102030405060708090a0b0c0d0e0f" + "salt": "00112233445566778899aabbccddeeff" } } } }, "evaluations": [ { - "flag": "integer-0-as-integer", + "flag": "obfuscation-numeric-integer-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -80,135 +128,395 @@ } }, { - "flag": "integer-0-as-float", + "flag": "obfuscation-numeric-integer-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-integer-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-integer-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -99.5, + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "integer-5-as-integer", - "variationType": "integer", - "defaultValue": -99, + "flag": "obfuscation-numeric-integer-7", + "variationType": "float", + "defaultValue": -99.5, "expectedResult": { - "value": 5, + "value": 1.25, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "integer-5-as-float", - "variationType": "float", - "defaultValue": -99.5, + "flag": "obfuscation-numeric-integer-8", + "variationType": "integer", + "defaultValue": -99, "expectedResult": { - "value": -99.5, + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "integer--5-as-integer", - "variationType": "integer", - "defaultValue": -99, + "flag": "obfuscation-numeric-integer-9", + "variationType": "float", + "defaultValue": -99.5, "expectedResult": { - "value": -5, + "value": -0.5, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" } }, { - "flag": "integer--5-as-float", - "variationType": "float", - "defaultValue": -99.5, + "flag": "obfuscation-numeric-integer-10", + "variationType": "integer", + "defaultValue": -99, "expectedResult": { - "value": -99.5, + "value": -99, "reason": "ERROR", "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] + }, + { + "flag": "obfuscation-numeric-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } } ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-0" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-1" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-3" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-4" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-7" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-9" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-11" + } + } + ], "evaluationEvents": [ { - "_count": 6, - "_skip": [ + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-6" + }, + "evaluation_count": 1, + "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 3, + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-integer-11" + }, + "evaluation_count": 1 } ] }, "_include": [ { "platform": "ios", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "android", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "flutter", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json index 9e54ced94d..9253064971 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-number", - "description": "numeric number.", + "description": "Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,98 +10,114 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "085fa5b25a3d42952668a59d052deb6ee5bc2418a352b9887556fe862ef78ca8": { + "5d1d3c167fda7f3ecadb68da40228431ed0b74ec08597fc57398fbf7f253f0d6": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "66aeb85f02e96149bd485b28375161c4bc154e748545593a40da549d53f479c2": { + "4dcb0659aaeb33eb329143c5ed994c52800521968edafd58c95c50377873ccc4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 0, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "eef7048e9d655ec6f29019eec43986e1d4ba6ff42523ecd8f46b6ac0ae883050": { + "8023572892cfed9fb5ee411c451eab7480329f32331f3845e76234275bede04d": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "a46a4fdb29e1de92315dc1ee481f59a33455841d317e38191f13cdcdd9fd2eb0": { + "d15970ae43ef67b0a9844f04cd8680567400e12340d545aa608a0cc9b89bed95": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "5f25ccca362e35bf27475aa056da166b4e7ecf6ed66b65bc25f9d573717d2628": { + "4373eda204ffa94cc5c15d29bd5a33b828033f4d2382f708801a7ea5c298b286": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "914ae5836aae14f5bd8348d1669e008e400c43887d08cd6dc67b028d914e8c6a": { + "5a95ab1a979318a8d113fcdb44a9a5262f0f99a6fb58e0f782b1ac5dbc88f1ef": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": -5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "3cecd82b1b8e3c49bc3fe1886fdd0a57de07ac68a437f59bf7c6c6317d83b03b": { + "dceef8e665186596db59e1c8bf2887adf20969530b48091e1bcf3517bf804a7b": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "5a539f4ac96a34becffeed98230a919b3082e085bb79deed40d88600d5500636": { + "6b8f85c278e445167be847d3a94a91c1865221df7cc63dc8f31aacbb9454e430": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", "variationValue": 1.25, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "ec97872ee38dc677992d1df24af2093298d35a35d246ea3dd39df7e33279b358": { + "81da1069b7cb802d039c7cd27d4c0f4db2515a5c64dd411a522d6f300592c31c": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true }, - "4b33526589906dcabeefa0e880ad6697d4e6673af26187c67947124777a67bbc": { + "0a2bcee9cec623b02ca219d7b3f15415767bb630f6bcf9af75fda36c2a689f64": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": -1.25, + "variationValue": -0.5, "reason": "TARGETING_MATCH", - "doLog": false + "doLog": true + }, + "be4ecda1ccdb165dc68fcffc30f021c7e83a88b8a2283784498ac761385f26d0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c4f3ba7ee684fddd93335901ae9b0af3959704d06b5a028449b08854d5384e3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true } }, "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "000102030405060708090a0b0c0d0e0f" + "salt": "00112233445566778899aabbccddeeff" } } } }, "evaluations": [ { - "flag": "number-0-as-integer", + "flag": "obfuscation-numeric-number-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -112,7 +128,7 @@ } }, { - "flag": "number-0-as-float", + "flag": "obfuscation-numeric-number-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -123,7 +139,7 @@ } }, { - "flag": "number-5-as-integer", + "flag": "obfuscation-numeric-number-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -134,7 +150,7 @@ } }, { - "flag": "number-5-as-float", + "flag": "obfuscation-numeric-number-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -145,7 +161,7 @@ } }, { - "flag": "number--5-as-integer", + "flag": "obfuscation-numeric-number-4", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -156,7 +172,7 @@ } }, { - "flag": "number--5-as-float", + "flag": "obfuscation-numeric-number-5", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -167,7 +183,7 @@ } }, { - "flag": "number-1.25-as-integer", + "flag": "obfuscation-numeric-number-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -176,19 +192,27 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "number-1.25-as-float", + "flag": "obfuscation-numeric-number-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -199,7 +223,47 @@ } }, { - "flag": "number--1.25-as-integer", + "flag": "obfuscation-numeric-number-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "obfuscation-numeric-number-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "obfuscation-numeric-number-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -208,23 +272,31 @@ "errorCode": "TYPE_MISMATCH", "variantKey": null }, - "_skip": [ + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, { - "platform": "web", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "The public API exposes one Number getter, not distinct Integer and Double getters." + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } ] }, { - "flag": "number--1.25-as-float", + "flag": "obfuscation-numeric-number-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { - "value": -1.25, + "value": 1.2, "reason": "TARGETING_MATCH", "errorCode": null, "variantKey": "variant" @@ -233,48 +305,218 @@ ], "expectations": { "noUnmatchedEvents": true, - "exposures": [], + "exposures": [ + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-0" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-1" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-3" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-4" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-7" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-9" + } + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-11" + } + } + ], "evaluationEvents": [ { - "_count": 10, - "_skip": [ + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-6" + }, + "evaluation_count": 1, + "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true }, { - "_count": 8, + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-10" + }, + "evaluation_count": 1, "_include": [ { - "platform": "web", - "reason": "Single Number API." + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." }, { - "platform": "react-native", - "reason": "Single Number API." + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." } - ] + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "obfuscation-numeric-number-11" + }, + "evaluation_count": 1 } ] }, "_include": [ { "platform": "ios", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "android", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." }, { "platform": "flutter", - "reason": "Implements assignment-encoding-flag-key-256-v1." + "reason": "Requires native numeric precision or supported obfuscation." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json index dd41792734..d3d7c926ea 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/index.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -9,6 +9,9 @@ "cases/evaluator/integer-precision-native-only.json", "cases/evaluator/multi-flag-set.json", "cases/evaluator/multiple-reasons.json", + "cases/evaluator/numeric-boundaries-float.json", + "cases/evaluator/numeric-boundaries-integer.json", + "cases/evaluator/numeric-boundaries-number.json", "cases/evaluator/numeric-float.json", "cases/evaluator/numeric-integer.json", "cases/evaluator/numeric-number.json", @@ -16,6 +19,9 @@ "cases/evaluator/special-characters-in-keys.json", "cases/evaluator/type-mismatch-all-types.json", "cases/obfuscation/obfuscation-keys.json", + "cases/obfuscation/obfuscation-numeric-boundaries-float.json", + "cases/obfuscation/obfuscation-numeric-boundaries-integer.json", + "cases/obfuscation/obfuscation-numeric-boundaries-number.json", "cases/obfuscation/obfuscation-numeric-float.json", "cases/obfuscation/obfuscation-numeric-integer.json", "cases/obfuscation/obfuscation-numeric-number.json", From 15e1f1b2ed9c50d60f70107dfb74968db019e491 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 16:25:22 -0500 Subject: [PATCH 39/44] FFL-3568: Pin checkout and satisfy fixture importer lint --- .github/workflows/precomputed-fixtures.yml | 4 ++-- ci/sync-precomputed-fixtures.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 ci/sync-precomputed-fixtures.py diff --git a/.github/workflows/precomputed-fixtures.yml b/.github/workflows/precomputed-fixtures.yml index 365876893f..57e4e657b9 100644 --- a/.github/workflows/precomputed-fixtures.yml +++ b/.github/workflows/precomputed-fixtures.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - name: Verify the local snapshot @@ -38,7 +38,7 @@ jobs: with: scope: DataDog/dd-sdk-android policy: self.precomputed-fixtures - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - name: Import upstream fixtures diff --git a/ci/sync-precomputed-fixtures.py b/ci/sync-precomputed-fixtures.py old mode 100644 new mode 100755 index d47e01c1ff..e6ecb047ed --- a/ci/sync-precomputed-fixtures.py +++ b/ci/sync-precomputed-fixtures.py @@ -7,11 +7,11 @@ import hashlib import json import os -from pathlib import Path import re import subprocess import tempfile import uuid +from pathlib import Path REPOSITORY = 'https://github.com/DataDog/ffe-system-test-data.git' PREFIX = 'precomputed-assignments/' @@ -56,7 +56,7 @@ def snapshot(source, ref): if not isinstance(data, dict) or data.get('name') != Path(path).stem: raise ValueError('Invalid fixture identity: ' + path) if not isinstance(data.get('evaluations'), list): - raise ValueError('Missing evaluations: ' + path) + raise TypeError('Missing evaluations: ' + path) result['index.json'] = (json.dumps(cases, indent=2) + '\n').encode() result['SOURCE.json'] = (json.dumps({ 'repository': REPOSITORY, From dce73c235da72c28be2abb0a57717c056569f297 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 18:29:20 -0500 Subject: [PATCH 40/44] FFL-3568: Sync generated obfuscation coverage --- features/dd-sdk-android-flags/README.md | 2 + .../src/test/resources/precomputed/README.md | 34 ++ .../test/resources/precomputed/SOURCE.json | 31 +- .../obfuscation-all-doLog-false.json | 106 ++++ .../obfuscation-duplicate-evaluation.json | 82 +++ .../obfuscation-serial-id-on-exposure.json | 142 +++++ .../obfuscation-all-types-success.json | 161 ++++++ ...fuscation-defaults-and-emission-gates.json | 130 +++++ .../obfuscation-empty-flags-response.json | 83 +++ .../evaluator/obfuscation-falsy-values.json | 148 ++++++ ...scation-integer-precision-native-only.json | 66 +++ .../evaluator/obfuscation-multi-flag-set.json | 490 ++++++++++++++++++ .../obfuscation-multiple-reasons.json | 110 ++++ .../obfuscation-numeric-boundaries-float.json | 54 +- ...bfuscation-numeric-boundaries-integer.json | 54 +- ...obfuscation-numeric-boundaries-number.json | 54 +- .../evaluator}/obfuscation-numeric-float.json | 100 ++-- .../obfuscation-numeric-integer.json | 100 ++-- .../obfuscation-numeric-number.json | 100 ++-- .../obfuscation-object-nested-deep.json | 106 ++++ ...bfuscation-special-characters-in-keys.json | 127 +++++ .../obfuscation-type-mismatch-all-types.json | 148 ++++++ .../src/test/resources/precomputed/index.json | 25 +- 23 files changed, 2207 insertions(+), 246 deletions(-) create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-boundaries-float.json (93%) rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-boundaries-integer.json (93%) rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-boundaries-number.json (93%) rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-float.json (80%) rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-integer.json (79%) rename reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/{ => generated/evaluator}/obfuscation-numeric-number.json (79%) create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index 4ae92f59fe..c93a961923 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -471,6 +471,8 @@ Fractional Integer reads, range overflow, and inexact conversions return `TYPE_M React Native exposes one Number getter. Its tests exclude cases that require a separate Integer getter or native integer precision. The service keeps sending `number`. This work adds no numeric capability. +Upstream CI requires a generated obfuscated companion for each applicable plain fixture. It rejects missing, stale, or orphaned companions. Edit the plain case upstream and regenerate; do not maintain two sets of expectations. Dedicated malformed-encoding cases and fixed digest vectors remain separate. + Cases cover values, defaults, errors, reasons, variants, emission gates, duplicate evaluations, and numeric boundaries. Obfuscation cases cover fixed digests, salts, Unicode, whitespace, malformed metadata, and plaintext event identity. The suite uses the feature integration harness. Each valid fixture also runs after cache restoration without networking. It checks public getters, response rejection, and real exposure and evaluation records. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/README.md b/reliability/single-fit/flags/src/test/resources/precomputed/README.md index cf76ab45c6..380812a7f9 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/README.md +++ b/reliability/single-fit/flags/src/test/resources/precomputed/README.md @@ -213,6 +213,40 @@ emit one event with `evaluation_count: 2`. Exposure deduplication is independent Runners must inspect real event records, not synthesize them from expectations. Keep native bridge and deployed-backend proof separate from mocked conformance. +## Plain and obfuscated coverage + +Every plain response fixture has a generated obfuscated companion. This checks +that encoding preserves public evaluation results and emitted events. Dedicated +malformed-encoding cases, unsupported-consumer cases, and fixed hash vectors +remain separate. They do not require plain counterparts. + +Edit the plain fixture, then regenerate its companions: + +```bash +python3 ci/precomputed_obfuscation.py --write +python3 ci/precomputed_obfuscation.py --check +python3 ci/validate-precomputed-fixtures.py +``` + +The generator discovers plain JSON files recursively. It changes response-map +keys and encoding metadata. Public lookup keys, values, results, and event +expectations stay unchanged. The generated platform filter intersects the +source filter with consumers that support obfuscation. Evaluation and event +filters remain unchanged. + +Generated files are under `cases/obfuscation/generated/`, preserving source +categories and nested paths. Do not edit these files directly. CI compares them +with freshly generated content and rejects missing, stale, or orphaned files. +Adding a normal case needs no generator list or SDK runner change. + +When encoding does not apply, add the source path and a reason to +`obfuscation-exceptions.json`. CI rejects blank reasons and stale paths. An SDK +failure is not a reason for an exception. All current plain fixtures have +companions; the exception map is empty. + +The independent digest vectors remain fixed. Generator regression tests check +those known digests, including Unicode, whitespace, and embedded NUL keys. + ## Snapshot maintenance Consumers can copy `ci/sync-precomputed-fixtures.py` and configure the destination diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json index eae45353c7..13c31d7d3d 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -1,10 +1,10 @@ { "repository": "https://github.com/DataDog/ffe-system-test-data.git", - "commit": "0feda21aec6710a5243287fbc58a33ca2f658b16", + "commit": "8dcc09a53dfb654cb0b2de8cda96919710f074e6", "files": { "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", - "README.md": "9d2d59f1d66752d0c0e3828217fc7593a6ffed6992e7cf6e417a61ca336b8fd1", + "README.md": "ceb34cf7027004641182ec3327963adc1bd7b8a2b5f6faed94cd67f4a548d956", "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", "cases/emissions/serial-id-on-exposure.json": "83a7504e6658b875ececdd1972c04386130feb31c607ccf7b61cd45ce46c1fc0", @@ -24,13 +24,26 @@ "cases/evaluator/object-nested-deep.json": "22f8f0c516cfe0456d34697f29bbf39d7736559e0b188de4671d0fee6545fd3d", "cases/evaluator/special-characters-in-keys.json": "07e24ba1a407ef9604af0bc5ab3f8faafe199dd83db94d96d0e3d689430054ce", "cases/evaluator/type-mismatch-all-types.json": "0904425126ae64794a8c67d13d4bab759a88abc75c27c99f71c514babb9279a3", + "cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json": "daa6440ae7975580da89b38d71d9c60228858c022df3cd53ba0d906ef335881e", + "cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json": "8a98ce923ea0f56b6c1473a70c2cef2cd7b1892425b068a8e16ac127f3fe6089", + "cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json": "c7d24f386159b73bca86a2c10a58c0afc0fe6b17a557739d5b7f051c3a6aa205", + "cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json": "57bdf315572c3be3e2d997bc516a1332746a465c0e0a370d25f253947075b584", + "cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json": "8eff282ef31291bd85fe33d373711dc470adb450e11eac10da8c465f52a317b8", + "cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json": "cbf05f9e2d24d028a8fe9fceb5ad0d4d3e3162fdfdf51958662a7ef0943f4ce3", + "cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json": "54f2db7cc521fe33c21de4acb84d6dcbb6b625f2634c6fd7ccb7ec82121c8ff6", + "cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json": "f8f7470298fb30ee4566292ca9eb96c543b7009fec513199f5e676ab910efb88", + "cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json": "ffb46863e42fb3161e4c46c4c7c30f556c69c4c3f6172ff98541f3e37e34f1f7", + "cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json": "2955d89877e2a45a7301a7c00cadc2f7826b2e1fea083941a6928f88c171f48d", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "3ad1a96dffe7d376e8c381b6b089fb7cd30423b39d8f7f4006fa86fdab3a78bd", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "01269d0992978348fe0cba509978ba4b10e65a7bb03284847efb1be1a014d7da", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "df03044f0726dfe353b172ae75daf205fa4280a76ec05ac4dfc310f51ca186f6", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "702674897e5c457c070e0abdb112a38e5c1274d7e21ae314f5e2dfa805320a41", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "13a177de29ae708fd8361c095279d78166ae82d2fbe8d25804d5790d527bd3d1", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "3e8a78b7016acbcce301e1f437f145f9a24737989d70bda00fba2f9e8ffb2f11", + "cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json": "ff2ba07eb266db0669f0f86926060d268f3242d437264b5d4663f259317d6bc1", + "cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json": "fd4f9efb1eb115e505c81a30ae82c51766747cbc26ae5a36dda961a4cfeb2cd8", + "cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json": "6dc2320ceb7013825dbda712ed789689a0bd3b36294a741a8bc23371bbe7db91", "cases/obfuscation/obfuscation-keys.json": "3af487d59d5a7064e0e8e6508343db21689180d7d6e862b055934036e1716643", - "cases/obfuscation/obfuscation-numeric-boundaries-float.json": "56b4343a717161047ce32138f564c2558a1da49f37f26b7681f94b3cc0ef5473", - "cases/obfuscation/obfuscation-numeric-boundaries-integer.json": "3f2340a46e9cdecd48d07cc2aacdf66cba66fb192ed300e42dda4bbf50e415d7", - "cases/obfuscation/obfuscation-numeric-boundaries-number.json": "bca42ecb5cbb3a6ba08a6adbfa34690accaf986a07bf30a91b764871db673566", - "cases/obfuscation/obfuscation-numeric-float.json": "57ec0a367eb86a94269e353b7ceb28a83ac96a546a779b36efa115fd50464c69", - "cases/obfuscation/obfuscation-numeric-integer.json": "f3954848aa7776f4aed3d05e23445f0a66d5c5aaa461e526db335d7034dbc015", - "cases/obfuscation/obfuscation-numeric-number.json": "c429b7ed0a5e1131b25b1240739c1b5a17b046f833f7d54d74f60d90642c0c2d", "cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "452e57fbcec5a254c65295014ae11675d406a87567e6887933ba493a14109093", "cases/obfuscation/obfuscation-reject-missing-descriptor.json": "f0105da777c3b35ff10c7082e62ddd1f7ce6a4bc15dae27302f279b18b3bfc29", "cases/obfuscation/obfuscation-reject-missing-marker.json": "4fb270faacdcfe92477120f9bf9ea56f98e1503cf992c72da71ecc6fbca5e0dc", @@ -49,7 +62,7 @@ "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", - "index.json": "e3fc034a98f756a4283e1a10251800fa30d20deac2268c0193a3682ceeb679e6", + "index.json": "de2340499732d87bb4eda3fc0c0e3e2f188bda6f105bcf659c5d8824e9ca9d48", "schema.json": "0353d393b1b4193346b2fa907f500b5c60be51318759c3afe52e549f1662195d" } } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json new file mode 100644 index 0000000000..a8fb1e7fa3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json @@ -0,0 +1,106 @@ +{ + "name": "obfuscation-all-doLog-false", + "description": "Generated obfuscated companion: All flags have doLog=false. Evaluations succeed but zero exposure events should be emitted.", + "context": { + "targetingKey": "silent-user", + "attributes": { + "plan": "internal" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "84a2ac6cb08b06570e965cb98d55a0a83b415e1810ef3f4e103cd8fcde14ad7d": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "2fa983b9b25a656f30c3c1ca3547327d0c91abb8dd5a968cbf737fe8b4def524": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "94501ab4c2a58e69a11081f73e5f61a1f37c9c88fa95e61555a9d14ee1dd4024": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.silent.bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "fixture.silent.string", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "hello", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "greeting" + } + }, + { + "flag": "fixture.silent.int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 10, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-ten" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json new file mode 100644 index 0000000000..c5a6e355d6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json @@ -0,0 +1,82 @@ +{ + "name": "obfuscation-duplicate-evaluation", + "description": "Generated obfuscated companion: The same flag is evaluated twice. Validates that the SDK returns consistent values and that exposure dedup produces only one exposure event. Web SDK's FlagEvaluationAggregator aggregates duplicate evaluations into one event with evaluation_count=2; the web override and expectations.events matcher verify this. Non-aggregating platforms emit two separate events.", + "context": { + "targetingKey": "dedup-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "491646f7138ee482b2ace2d2834ff8ff34b20c8ba8cd329156857ab1be6154a6": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + }, + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1, + "evaluation_count": 2 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json new file mode 100644 index 0000000000..4c038b0492 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json @@ -0,0 +1,142 @@ +{ + "name": "obfuscation-serial-id-on-exposure", + "description": "Generated obfuscated companion: Validates that serialId from the precomputed assignment is forwarded as serial_id on the exposure event. Covers: present serialId, absent serialId, serialId of 0 (must not be treated as absent), and null serialId.", + "context": { + "targetingKey": "serial-id-test-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "flags": { + "8c7c04076517d2c3c0633fda9a6664d4b114cb359cc512dd38c4ef69373032fd": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "36d69e5a88de3b456cd6bfca1070ee06c667c9c94d7de9022616661c71928e50": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c7eac87f0bcefab56284d70561f954ffccd2f88fcd895067ffc94832023d3be": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "6597def4cb332745af4ade37ee996a8958091ccadc58082c2c49c4db66c24e98": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "no.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "zero.serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "null.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "serial_id": 340132 + }, + { + "_count": 1, + "serial_id": null + }, + { + "_count": 1, + "serial_id": 0 + }, + { + "_count": 1, + "serial_id": null + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json new file mode 100644 index 0000000000..8b20e3f036 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json @@ -0,0 +1,161 @@ +{ + "name": "obfuscation-all-types-success", + "description": "Generated obfuscated companion: Successful precomputed assignments for all supported Flutter value types.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "pro", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "657bbcc45263793d67e4f7967fdd257d6e1896cc095d758c520ca3f52beca003": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "abf42b7b192cf3a719bb8ea77c9ff652c82bd4c7aba54561e374efeb2fbbb9b7": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "820dab9a17046e7bf33d080bcb6f44547e1b92c31b36e7d9dcfde96c46bb5d56": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f4d559502705ccaba0a55b0defc37636fe7ed397a477a07389771c07183f139b": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41057f3677d1a6c229d214c630b71e9f499abbb68116d50594561663f6ec09d9": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "enabled" + } + }, + { + "flag": "flutter.fixture.title", + "variationType": "string", + "defaultValue": "Fallback title", + "expectedResult": { + "value": "Datadog Flags", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "copy-a" + } + }, + { + "flag": "flutter.fixture.limit", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-five" + } + }, + { + "flag": "flutter.fixture.ratio", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "half" + } + }, + { + "flag": "flutter.fixture.config", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "object-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json new file mode 100644 index 0000000000..b9a7541d60 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json @@ -0,0 +1,130 @@ +{ + "name": "obfuscation-defaults-and-emission-gates", + "description": "Generated obfuscated companion: Precomputed assignments that validate exposure gates, unknown variation isolation, and default/error flagevaluation payloads. Unsupported variation types are omitted; lookup returns FLAG_NOT_FOUND.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "free", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9176965ed24adf9f9e56f8918f3e4f7d5178ab57ca0fdb3e32266a4203930e49": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "f83c743fa9fc2faf137298f504966877bbe7d23e7cb9380c4b73188b764b0a0c": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1345780c7d221b7c1798057d61113f43466cb92b31ecb05fa74ee4b511d7c482": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.silent", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "silent-on" + } + }, + { + "flag": "flutter.fixture.string", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.unknown", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 4, + "_skip": [ + { + "platform": "web" + } + ] + }, + { + "_count": 1, + "_include": [ + { + "platform": "web" + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json new file mode 100644 index 0000000000..c685167982 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json @@ -0,0 +1,83 @@ +{ + "name": "obfuscation-empty-flags-response", + "description": "Generated obfuscated companion: Server returns an empty flags object. All evaluations must return their default values with a flagNotFound error.", + "context": { + "targetingKey": "empty-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.bool.missing", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.string.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.int.missing", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 0, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 3, + "_skip": [ + { + "platform": "web" + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json new file mode 100644 index 0000000000..e6bd5b9e43 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json @@ -0,0 +1,148 @@ +{ + "name": "obfuscation-falsy-values", + "description": "Generated obfuscated companion: Flags whose variation values are falsy (false, 0, 0.0, empty string, empty object). SDK must return these as real values, not fall back to defaults.", + "context": { + "targetingKey": "falsy-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fc0d169622e17ab8efff1da7fe0d0ff124de893d2e723fb9a8c632a87f5617e2": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a856abf0d6b1d857471bdfe3f81e7e0c08a1c7da13315ffd303a60e40032c8d": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8e902056a9d96a536e0300af89c407afe670d48fac2ae3ed9c4f0805579011f0": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7aa973f2d2a8f5ff69c54faad155bbad70b0c2733f20d10c0ed170749ce60f3c": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5273f6912bea9ecac5cd7d11674e39b208fa0bc796a3379e5b46138a4d2771a3": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.falsy.bool", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "fixture.falsy.int", + "variationType": "integer", + "defaultValue": 99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero" + } + }, + { + "flag": "fixture.falsy.float", + "variationType": "float", + "defaultValue": 1.0, + "expectedResult": { + "value": 0.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero-f" + } + }, + { + "flag": "fixture.falsy.string", + "variationType": "string", + "defaultValue": "non-empty", + "expectedResult": { + "value": "", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty" + } + }, + { + "flag": "fixture.falsy.object", + "variationType": "object", + "defaultValue": { + "fallback": true + }, + "expectedResult": { + "value": {}, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty-obj" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json new file mode 100644 index 0000000000..8ab272bb1e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json @@ -0,0 +1,66 @@ +{ + "name": "obfuscation-integer-precision-native-only", + "description": "Generated obfuscated companion: Validates that SDKs with 64-bit integer flag evaluation preserve values outside JavaScript's safe integer range (Number.MAX_SAFE_INTEGER = 2^53-1).", + "context": { + "targetingKey": "precomputed-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "60c239d3d5039428009ffc2a7918ccbd9c6485ee75e7d3130b148103d3e784ae": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large-int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "large" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json new file mode 100644 index 0000000000..0883414b59 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json @@ -0,0 +1,490 @@ +{ + "name": "obfuscation-multi-flag-set", + "description": "Generated obfuscated companion: Precomputed response with 50 flags of mixed types. Validates that SDK parsers handle moderate flag sets without truncation or memory errors. Only a sample of flags are evaluated; the remainder are parsed but not asserted.", + "context": { + "targetingKey": "large-set-user", + "attributes": { + "plan": "enterprise" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bf9be622ca7517e32cd02c784169beb8e5102892504fdad8384e348d48e36879": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "75dcd247147ab21d09a4fc73051324b2000bd0bf97d82d35ad6d80eaa6d07973": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32681eee3e6ebf4ebaa0b03b91638cbae0920b26239bdea4fa2b7d0bb3a46df3": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "792f76037d724266f9c00e971967bbfe62cba43274b31f652b9768d6381e7ace": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "59bf45e79a60fa4a33f2d4c639ef910982d3d3e29ca2fb9c147fb4977569f35a": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fce91fe1f119ec27d34a9cb77ab5014d49153c8afa8162b282c9c595591ff130": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "086529987ece3ae0319c12332405bbec6e6eb913266ade8c8fb33b2b7b89ab5f": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1d89f79a25e4eae7f080cb86d35af66fd2147336f6c450dcd40d23333f83d888": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0fe82d095e9c1dbefdca57b310ab4c19c80439c87a41b0eef548685914df239d": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "1ac48586eced16b0aff76e17312da68f2c43fd3cf9ca33d76f3e427f93797c4b": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6234a3430646b1df0c9e2bcca80dfe16b05abcca88d58f5f97c19a14a9a320": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "215f50d3c5e5908c31d2bed80414ce26ba68b264ea5baa4ab5b743e1833c1945": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "88c23388c4cec50c57429698b96c975be9f8915f88308fba6bda54acc6d01998": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "b66bc4f899fa5ad230fa050a3ba35ac5c5cda22bebecbb44e6c63afddbaf7b5b": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "83ab766d8522b069512a37e2a5bd8bd8f941fbeef306eab09f6247e8db3b725b": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "db1c7d8f6ac65f0e493bb4c3a87f153ec6cac18bd0c984b20ff09619ff79cce9": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "4d23aa03aec589f62d46d15642b79f6e3c87e3aefa5b49cd319304f4a013ac95": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "023b47247033e8bea815ff0cc16255bebdff082b3cfb1eeb4a9b55b3004276e1": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "0b2b70d8c98c0731692191e78b527512e548190d97d710d7f0fc8f19cd05913d": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c1796f3cb195313519a837ead5d54e083195b6206e189bbdd0220561b4ca766": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f73dce8e960f6d67023c407ac490ce3605b279288b56f78031bfed36f0014935": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "c078d7577eff8d7a6fc8963d0bd16fbb172c3a4a3aeb9e714c75a2574d7967e1": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6326ec0ed3c2465e7cc807f46c743037e7095d3f8e9d1d56dbaea76b2b00452b": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "3d3d982d7910fda9264321566138165779600cddbf42d215d689080b617b3fd0": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "5a80d2d736721d87db810627614f4f6b127ee0a332f5fdea8550a78e3ec4e699": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "86edc8f5b58892c55f5fde77e66cdc7b6d0a9b071e2677d8fa16dbb8f409f380": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "cc6b19cce7d683983134195f55a0c6bf3a5f844944e283cda1c58485b5a2dd2a": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "4c2f7e12bea6b91c5d5c9862de0b24418f6fde074eb5e017c93293cfabf60060": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5a48e1a5c7c8e185b62cd259351f4abe443771f07292eefbd49f0599881e35ce": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "cf6321f08df13c06652debb11c4cb9ea67e215e5a364d845f30d865554876b4d": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "1660da21238865ae03e7c5c4959d02a34bc210101d7608f7221f97f254b801d0": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "34529807d59d7bf904c56e17e112ed555687ee05a332a9ef957d7f029870ed57": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "f8fcf4b3a145194a19daabe0521e6cfea4bac6b20a6fd729f4c8c3ad6ac3e6f6": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "210a549229e78c66c588dffc37d99918e7968a04876fa87cd1b719a5dfe1edf2": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ffd5bbbf90dd7c225ddc95b0c1134cee255c5d1ee7f7889bf2c239b99dd43db4": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "929200b0fd1248b310ac0f0785a5a3ee3b1aa40cbd4cc7d7ee7c141bd57c839b": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "4aefe68d39be982605014ac90b746de9be85b4bea79cacc26459cdc06b96f48f": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ba21a1d31e1f9074d322885263107157affa2265582bf6a05f84441b8c118eb": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "77581b9776c4d5afa1919a1e0c758067d07c3968afa2201870cea6dae6579aa3": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "03461ca38265adbdcede804d0ba7a544dce9c4de6296eb32d8a6683096a9b1ce": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "92325ac72dff844afbdc8a8a8ca939b673fbb452cd67d7b970990c58596baf57": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "934025c0b96e851d348e09166362375a53e2776f595da60d4e2fde30f1503baf": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "f6d270502e6f9fc1f4cfa7fbcc78e2b3377d9fb480f23e864cd4be0c96ec3467": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8bb8e9b84c7ab5397d4012c8d58cb797a8fac7562c86ede747d91f5971bfef10": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "81be2644335d094121dc49dafd671c0c212980253962dd2f24986736cdaf9583": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "b4fa3c55cb8c05baba5ca43eac146e6036b0028e2fb07eafd6819d3935c9e4bc": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a56d4714d20368ac54afa5225b1dec452d9bc102f604fd54bd338e4b693bda38": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "e5b919481d265c3573d94171004324cff98c4e223efd7b568b028ffcd4314a6e": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "370b0fcd16be8003ef1d1e1f39e7e19b3698f17a24957ed654420be0f81eddc5": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0721adc36960473f00919092dc06adc2988d409a72f5244bc47a4b11de285660": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large.flag-01", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v1" + } + }, + { + "flag": "fixture.large.flag-10", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v10" + } + }, + { + "flag": "fixture.large.flag-20", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v20" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json new file mode 100644 index 0000000000..4002b72470 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json @@ -0,0 +1,110 @@ +{ + "name": "obfuscation-multiple-reasons", + "description": "Generated obfuscated companion: Flags with different evaluation reasons in a single response: TARGETING_MATCH, DEFAULT, and SPLIT.", + "context": { + "targetingKey": "reason-user", + "attributes": { + "plan": "pro" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "19f9605f249b328efe6542c0a39acdaa7a0b39c61daccb3883f2cf3c5705e9a3": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e433cf607656e33013abf122d4f7576389f0acf3ea88f085fd069bd839390a4f": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "212fded283b3498c3742391c9db4843ff32aad02ded7646c68dd14bc008f3f03": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.reason.targeted", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "targeted-on" + } + }, + { + "flag": "fixture.reason.default", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "baseline", + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "default-val" + } + }, + { + "flag": "fixture.reason.split", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 42, + "reason": "SPLIT", + "errorCode": null, + "variantKey": "bucket-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json similarity index 93% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json index 4ad1d6942e..90483ddba5 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-boundaries-float", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "2aec5b3d4477769702ec52c42349be07a31c413ebffb69196b762523adf205ab": { + "35dd7544a6b9a875c5074a893aa3d5c95f015b6cff7f07f30390bc90b939839e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ccd840e29b0846bbc8ace7da7bc20c300b2a14e1f5f68961f2fb76e69fb0ba04": { + "54b3e4e77e354d4e049991350fe68ce1a90ba4e874933a9e269d5b99cb3ad115": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ebe608c5e7ab0bf15e0c6585810759616ab5881130d7ddb956ec8749c43a0939": { + "6cec42afe677ad01472e4754a611106037ddb8e786570bf9a730630b8d2f7f30": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "1f2de3a9ac37932ca8fbeaafe88046f8f7a49075c87eec5b805a0967706685a4": { + "8de4314e7ce11a9f64a3490e59968a1130f56b15c39276bdff9ebc344402101d": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "431adeb8d8d06d55e943812fd5cf8bf6095858eb898fd3133999c3d163607b3d": { + "4182dc078ece15bc1c45cabf6e2ab2ad0b7acaf7f42eaa3ad63fe3956beb1db7": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "2c7494350d45235245c36589fe36990e14f5e3eca4522e7c93b05ec4e4e3de7c": { + "ec7fd610e215fd5d177ffa68bec26d50a871bc1cf0114636a734e1660bd6411e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "19b8019e6dc73fdabe4884fd109c0c0865eb682b5a6e9b2c098d8698cb8a25bf": { + "53d34567f5337f1104d86a9e821df4511fa74a9cc03516f426c9ab4c7af0ac52": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "05dce5b4f6ec9f670d767a9e2f3095c91dc5f947c0cf65f26ffe0d34d169c27a": { + "3e69e701cc02087f7897b6f4832e8be0be9ca155876f8cb95597e6ad32b1e2ab": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "b31d785097e2b21ff4203b7f42d9601f0fe117e04ae52fd41c38ec226ee7c9dd": { + "bd1a3e85e06626dbb1c56432a8dea7a0128284f1d781013835a2564e70f87d98": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "2561c2c5e59e5ee958e65c5ffc5afdc82deab8caacc24b7fd955426d38f3f10a": { + "a212f6d288564e940e009835514eaad55a3d62caada53dd1fdda1b2475efed0a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "a4378fa74480657ce942442b4755051dfe8254b3a869ef1d09e73b70905931ba": { + "f8dc9d43242ec89a4db2212c3a70ee9c938b0cb36afac45b2000cd7c77bf7609": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "4c973da42e257593047a0e547423acae08bcb5d6529670d6917dc653d559878d": { + "b1e20092aa2b67b3e8ed9d5533a2260ff177207d9b86fac2151864b2d7af492e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -106,7 +106,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "6291d2c93c31947490a82ac572b34ff0a2cfc6d1bf63ee929bd4c427e4c14e46": { + "45f0d04656e98eb32b8221714cf169d57479d2be4af6b96c5ffe0f9fd589ad0a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -114,7 +114,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ed9c529fd1165fc1f8560f540dd0741dd240473987473db250c027486ca54481": { + "680bb5138700a197f7896709b20d6a451854dfaf7fd2016374154dc214101f96": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -122,7 +122,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "87217664c8cdba2054ae7ba5e0513f1fe0932a93deb52ee46c8c4856db50bfaf": { + "5f0e609737393c76014f125cfe1d8b685065b3f8bb2833e393f4de4bb3f271fd": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -130,7 +130,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "435af2981e1d1cf3e2e762b9fbb1b99f4850d1b0d4bed9de0f44f0418b223506": { + "6de38d27b71396e8169bdf012bf53a9f6112b4f9533e492d1d0c372aa5b2b89a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -138,7 +138,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "608602ea98e8c47ffb9f86e51e8093daa81ff902841d774092b32df7f92421e2": { + "e68a8c78bf6d545e6c949e88209611b6615ddeea9817440de9fd3fb7a289c335": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -146,7 +146,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ae43ae724ec81be659b9cbd89315102b127b1b9e24e26def87496eb4a8aa38ab": { + "a6d6386fa18188d57ad46d118b937d77a1a8d814e3ff723871452b9d5f6abd03": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -154,7 +154,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "2358b34f81306a3acd892893294abea0c33721a65b06e355cda36c8e54739be5": { + "9d28b9ca14aed8cd8edab7144dc09ff50ff608f1676df0c92be1a225fc4c95b4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -162,7 +162,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9fc154f59fba732da1a2be979ec48044cd88f185fd90b9cfc306e53f3f6d6a88": { + "e026139b2c1ef067fcd194745a316b948ca17d4224a2c6ef0f54e42739a6f9e5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -170,7 +170,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "350edcb8f9921229dea893c8a3948457281d0ef88537c3c8905a4dd924165228": { + "9ad722f4aa0e6cbe77da0e6406a3f40ac0dbe3f556a524d1d504115896235fed": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -178,7 +178,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "3ff68a68d0c6c04348089c33052aad256a51086ae365496d7ecf89a55c9a4d70": { + "56cd8c449d33e0a19ca9b0d300b40bd994dfd1f4f3b16d093e453d8a97a3645f": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -190,7 +190,7 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } @@ -1080,15 +1080,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json similarity index 93% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json index 9f6a4ef37d..4acc8be2aa 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-boundaries-integer", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "7c5c55bbc1d947ebd29d4d22bbb8e854fb84a3168ec16c9b88ff1eb8048f1259": { + "dba4f4d05c42d94f2f0fe7fb4c5622432cbace9983c4f2bcd81dba39a6327250": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "1db93bef085224805fc0b94e254d8efea2ad144e255512c7822ba1941efd37ca": { + "c053d531267e94db3094b514ac132333832cf45cc102899e23a317cfd1b86c7c": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "c65f42d21a69e9e63e768bb7c353ccc659d8fa7fda19d552c46571fe747939a1": { + "cc6b41d8abf103fc2c0903beb89ea4916203e65e34f56f229bd4bfd30fca2b44": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "57ea520510c0e0ffdf6c016032a1ad8fd7e930feddb56e512c5ba5e176bb747e": { + "2cd758e8f6b3d6069426eaf4fe83cad350062d6f3cf9fdff78af6814151ff35f": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "3bae488ac7f7c5e64e8d80c5e5d3a01fede98ee9dc1dbfaaa1602bdb834e3199": { + "be1e08347c503a3116b8872a98f762ce80f8b2d5d9712855f553e9a8e41a0e24": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "d7e31a5af23eaed253fbab5d5ba775a49cc869265a762861623b9850ca45200e": { + "439b8c652a5b3d6bb6a8b5601f86c09f38f1ca344dfa0cd10904c41d3a0089ba": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "e214fbcdc5fa2e6b53e80a737cf3b01bf720e1da6bdc8d1a8126c82fae131cc5": { + "52c0be60636eb11b987e1729b894a63f02463a408660d632004cd68b66c17966": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "f87c7ffcbfc101c063bc2f6533d4a73054a4c3e0cc59b865b6d9a738cded6776": { + "32ac57434b06ca630219caa81b6d6c685138920a8a9d1e6205055cff023f3179": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "24e2195a49630a3b88efcb5227d7044651ffa1e483006af5ef3e41db492ca96a": { + "21bae751d95202679767297ac5a668ff2588c260e0c1d93e0ddb494a3c6c44d9": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "45316309ea93c9d4d63ebd704488bd9b862cb9d30303b54ccbc668a45f1a9cfd": { + "ceb204983395689386573ade9d3496ac4c07fde14fd7dbc965b51654af834df1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "107e31005fc784e93f438721722b248ed66b8ed91d13eefca267786102fe5a81": { + "c2b69fdede340468e31ce79ee511016e1f3f15fceaf2228fb6bde3c034cb14d9": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ca603c439f5fa9a01c76f4febf58bf0b1034e100dd37fd1731c05f667409846e": { + "0a1f9c3aedb3a5721f4215c26cd403a7bca6d5fcba088d6ff8c5eb58a4168903": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -106,7 +106,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "bf801a713f62854431659efc54a0cb312dd601a8459d4390b13840fd7f414a02": { + "cf564971ee029d99592fad8d056f2d1cb13ff91e3e283dc2c16b3a644db90734": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -114,7 +114,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "6c6bc2ace96267f8a839f207457b3d12b93f8211e7ce9a2f4ef9b2ddfc70412b": { + "482ab354d08783942a1cc1613d261773481b27647304272c9a895b3f82aef81b": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -122,7 +122,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "cd01bfe31bd7117a646dddc5be9732400e8ee0076cafd79c189115b8b3081950": { + "8ae623d95ec069f4263033a5c3c63cc41c8369a4ada5c2df1e72feb3472b3298": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -130,7 +130,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "fe4005d2eb3448257136861a88114cadcc85f06cb2ede14a36b581d4900e4a5e": { + "3ee628175449c6db9aeeb2b77f155f2af7f566ae1209ed32f564b4e74d37ffbe": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -138,7 +138,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "fb7e52e4c272d1e4e9602339c4f6a7094257194ea28d274314b8ca0a4aec639b": { + "1aaaeb79098a7feede64e211e53f192295e0d97db557223f5945bd869a9ec82c": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -146,7 +146,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "de7cefc4a649b9c39f7fe31b49d5fa5f8ac6b792e2227717e0130dd127bbfd70": { + "1a9a9de8e21c848b2bfec22d14c770d8a3a176dd1ce39955a5b165bf1f851590": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -154,7 +154,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "e084557c149a57fcf7e04041ad080c04586380baa9093a16dfee198a1ad51989": { + "c2dca5f665b034d94acdc589d0b6646e583561b3f05aeba0a0b6399297c8ccf1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -162,7 +162,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "f163106c94e153c44a0f9c882c90c6c5771e7df230fb847e3a9426bf18a8f312": { + "a014f0066d631022d1c7628bd1b990291f772710900955e0aab48543db7c3b2a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -170,7 +170,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "40aa0011039c7b0946bb1264199ba576db480b932c341fa5a224340056ec0976": { + "05ab2bff8b65224aff72672e380d66e022ff5f0c1b76e3231aaf172f6c82d467": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -178,7 +178,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "79ccc13c690d354291ea1fbee6c0b103d273130dd90bb6f3efe51840d1c562ae": { + "a10340fc40240dde6ba540065e6526b8c66b6fd78d116451b42201a9ba4c665d": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -190,7 +190,7 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } @@ -1080,15 +1080,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json similarity index 93% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json index 96b428ade2..27bfa4511b 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-boundaries-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-boundaries-number", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "b4ba8411e13132261ecb4f57a72e7ca330d2618c536f5546590abb0cc25b9c47": { + "4151b6612247033495b8370b980c847c1efe5dc3ddb008a57aaf9e8afb97694d": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "82796957263227e5d42ba913e864aca7aa0f6c06cf30ef1bd6a52cf771a5680e": { + "c3c464e47d3e87d433d2a0b6a5ea9e9cea9ffb65c31a7022fc4be4c51a3e3663": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9889dbdc0d0d8741c09f2d9e1f6b764e37995c66e76452502ff7a73398dc8fdb": { + "d9adf2748e7c2bb330f6b321204a4ab3d61aa555dbeaddcb3d79fb20383e3840": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "20c38d5ef57bd5c41cc52d311ab1147afabfcdbb2abbdaf47f9c1d5f71736248": { + "a3e931eb60a9b70284b948912691bbf4e1f757d1f52c90d1623413a4ed05eaf7": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "a989158eea675eb49b1f62e15c2205b1a1168d9ed769e90e16ba8f43e65963c2": { + "56a5f99336ac7daaa327d074e3c1cf1389f3b0e198d8b6b0dc4a0773f37466b0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "fd02ac0da0683586431856ba869c1ff07669b94244e6c8a58c7e1ae8dcb3a04d": { + "996e91f621b0091b21aaceea06330f3bcc8a4639939a0b3cafd38cdd2d732bde": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "0c4bdf647950f33d16dace93f197e1891b73d767a5bc3be0cf1e51a115775f63": { + "5b95fc3592f62934e978d8f18e998120d1012da96a4ae3c7a3bc832442914bce": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "bb03a68efa88e918862e8a481ee25893c059765384e7a0835b022f742ecffc05": { + "fb8ab09c443a1fef7f78aa00069193131bdddd5f754c69837da77dde8bd53ab4": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "596ef4687c078263b87dd17061e7471add9255b49814d72150bac75dce35787d": { + "13bc44aee0e07075a5f61e06bea48f9c7d7cfe240bbcf45d9922127c5fcbf57a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "920cb9612332743d61020ef481f4990bf0826040b3d087212a891115e9f30f22": { + "2b8ee396fbeba43a943135d2ea6962f4eee41f3af704609313b6241bf4911f20": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "62e88abd1cfc891df7f5703503077e82fb99db1e9be9b91d9a6b50333b4d1110": { + "9dec15ccf9649edff360ec3a223801aa6f804087a939b1d14b686a7fe34a0cbc": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "a99b58377ef7b9e52d6e1c3befd549d61e1ca234eca3cd8d4720c16928d9a7a0": { + "b1aedbf12de210a49f962e5e8db09a065c2b328a7d2c48ce4c6e919325b9c0c5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -106,7 +106,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ff66a9c09d22765cf9397d3e156bce434a683325f21ab6df1c9b7359056d676a": { + "4a989c369519e210c4857cd50fd525e571cc89b7da322276ba05487c15b4849f": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -114,7 +114,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9b15e9496c8eb9e19f2e8afc18ac298593ea8d45370e1fceb0f98b0f47e28fdb": { + "313de806bdcd55e96b13d12c28260bc5238057d4ed2a8ecc0294f8c7e234fa01": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -122,7 +122,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "e0d4ba65f888bbf2b204e1ed4cc02d23b0e0c3f8a0f4f22b51501c7532d4781f": { + "1f5409b64eb99b424819230ddfb333b5f399a911840ab6b36f0a0938a438af25": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -130,7 +130,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9b5089501d028a056aae6a5de88452f98f79340ba32f765a3950c6d8c0a82a9d": { + "61f7deca99e29b35ab18cdf82001dcf64ea3b04caaa9fc751d835f1c70d25fd2": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -138,7 +138,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "1706e7b05d435e118cb4dee1540e2109fd7a7b1b19b08fceec0cfebc8fc0abc1": { + "06503c386b2c84e0299e7a692baecdecf2b19698b90f13b89df7d4e309d43870": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -146,7 +146,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "3769e087d36053e2971961b47640ab9b7397f02b1eddda5d4e90819ef1b1f5bb": { + "6d9de4417c04ad9908dc846b1a9ae5c6c2fe214e7aca7e2dd3480d82abd46a5f": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -154,7 +154,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "e0cff618c57a7aca3a66fbbb36b72aa53a4d4b7a08a5c67a0e25ffd564cea395": { + "1b553f209415af2ef3dd59e7f7383e270e5090b9811e319c39645a51d58ec26c": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -162,7 +162,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "4d204756440205698d05663c5a511d991d8e7ad64f54b1a257034b8e2c52f80f": { + "4ccf6d4d18107b748ae8ec301e66adb190f15bd77e3bf23246d8994787c8a40c": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -170,7 +170,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "4c741509a1ac866498f61619846c2cdec7fd84bbc362ee6ff3f8d563f2156fc8": { + "9de54af153484d9db86d593c9a0c9443f7c5a88881964452b69b7f2d8a878c01": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -178,7 +178,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "c9bbbec289c2c83e830e9bffd2ff577daf18bb71371cff7c08a5372ed860d957": { + "52b56cf56113aee71339a1256252ae52741aa5e666d2a5ca3ae3ad194cbb83f5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -190,7 +190,7 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } @@ -1080,15 +1080,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json similarity index 80% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json index fdbedade46..cb5d7f4057 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-float", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "43dcecc88807de972a354bd0b96946419ccf086a3cb1abcaef10a796129ffb7e": { + "6711bb1e8b093b846ea439322c667bce9ca0568b0f0d4320e05c236c9ead11ce": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "ad5896fa25ecdb06cc985d99e469f2b867419c296d99142ba53b7a4ab11d0167": { + "dd28ccc7dc267c0927246a422010adbf6e81bb5b0e8fa6a3f9c5c999dcfa8cac": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "5f920ae1126d6bf5dd3c7cde59b1604dfbae53d4ad4686a548871a4baec00f22": { + "9662b5e2c026477337a6a1b952ca1da98116ea3393c14ad81bf3b0e6f7705eae": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "a710178c1d9213803a5ff5f56146c93908327ac97407d76c6139dfdcd9a4012f": { + "6a126c45f099dc1465fb20627b24f64632aae36028757b78b361873c454d566e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "b81af3ac1aba6361e1eb0951f2e1babd617d2218827ce4a14f2c8a5f8005b885": { + "7a331b89b73d723922e64da680ad3048d381db62fd6f9139eb9e57ea43569156": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "de8b6921dbff87fdf7182b8a948b9644aa4db01a9dbbda9749a0774f189746cc": { + "e152b1e1e80ca9edabd8cfd196b0084ee4728b8eb6b85c1de743705b8ec3f1c5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9a8d577fc6f24f49f804fa068b9a7170c9328c0def19a2b2312c9f59b5dfab02": { + "968e09a8af812ed8d7fc18b21f59d5b7623d5e1ae6fc8f5a141443f026b11aa7": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "82077f26295c6ead94d7a7a7554ed4e143a4af711f8deb94b8a8a711d032c4f2": { + "b4b714689b3211e62d2fbde91099071f193e00c955f5a96a6ffa087b2e6bfa98": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "8bf2c56f7653627e757f8263815d7408dbcbb6fb86d71d55402e9800f185ed22": { + "a978ec7a2487b240242a05119affc348f41d0d9fbdce334665c4090c2f4ab1fd": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "cef1691308f6e520bbdbc8575683d17bc814f52af8ef42522d6a5bf0aee7e861": { + "3860419b6dc9dc69573f100ccbf51c008e45a8a9b6a62243ff02b869f4e86b47": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "8b79dc5672bf5c07c2a64b68cd885ba18db98b52639d349283ac6888477326a6": { + "a303e6fa7efedf997c87edfc2d79872c95d3e1a6c5310ccfe65d0826f700ab55": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "c0b84d2e8690addb339fd433c9cc449d6da6632ea8398d47e9bfebde442247a0": { + "41a0cf77c6270e0e6f3941ab2fa80f712b1c916d13793ddd36a99363c07ad885": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", @@ -110,14 +110,14 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } }, "evaluations": [ { - "flag": "obfuscation-numeric-float-0", + "flag": "numeric-float-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -128,7 +128,7 @@ } }, { - "flag": "obfuscation-numeric-float-1", + "flag": "numeric-float-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -139,7 +139,7 @@ } }, { - "flag": "obfuscation-numeric-float-2", + "flag": "numeric-float-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -150,7 +150,7 @@ } }, { - "flag": "obfuscation-numeric-float-3", + "flag": "numeric-float-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -161,7 +161,7 @@ } }, { - "flag": "obfuscation-numeric-float-4", + "flag": "numeric-float-4", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -172,7 +172,7 @@ } }, { - "flag": "obfuscation-numeric-float-5", + "flag": "numeric-float-5", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -183,7 +183,7 @@ } }, { - "flag": "obfuscation-numeric-float-6", + "flag": "numeric-float-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -212,7 +212,7 @@ ] }, { - "flag": "obfuscation-numeric-float-7", + "flag": "numeric-float-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -223,7 +223,7 @@ } }, { - "flag": "obfuscation-numeric-float-8", + "flag": "numeric-float-8", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -252,7 +252,7 @@ ] }, { - "flag": "obfuscation-numeric-float-9", + "flag": "numeric-float-9", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -263,7 +263,7 @@ } }, { - "flag": "obfuscation-numeric-float-10", + "flag": "numeric-float-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -292,7 +292,7 @@ ] }, { - "flag": "obfuscation-numeric-float-11", + "flag": "numeric-float-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -309,55 +309,55 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-0" + "key": "numeric-float-0" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-1" + "key": "numeric-float-1" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-2" + "key": "numeric-float-2" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-3" + "key": "numeric-float-3" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-4" + "key": "numeric-float-4" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-5" + "key": "numeric-float-5" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-7" + "key": "numeric-float-7" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-9" + "key": "numeric-float-9" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-11" + "key": "numeric-float-11" } } ], @@ -365,49 +365,49 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-0" + "key": "numeric-float-0" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-1" + "key": "numeric-float-1" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-2" + "key": "numeric-float-2" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-3" + "key": "numeric-float-3" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-4" + "key": "numeric-float-4" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-5" + "key": "numeric-float-5" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-6" + "key": "numeric-float-6" }, "evaluation_count": 1, "_include": [ @@ -433,14 +433,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-7" + "key": "numeric-float-7" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-8" + "key": "numeric-float-8" }, "evaluation_count": 1, "_include": [ @@ -466,14 +466,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-9" + "key": "numeric-float-9" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-10" + "key": "numeric-float-10" }, "evaluation_count": 1, "_include": [ @@ -499,7 +499,7 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-float-11" + "key": "numeric-float-11" }, "evaluation_count": 1 } @@ -508,15 +508,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json similarity index 79% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json index 4b638c4914..b1ec8e6115 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-integer", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "72101f81c2d474893bb90b2e9fbece5750b83659171b75d423eba3c8caa09af8": { + "6f6faa00700939aa0c395e059d8dea1cc48b2c5a20eb2ef5bc7abfeb551e2d6a": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "158853f6fec44bdd14c7dcf732538d1932393333da0d38e5b1cf389febab173a": { + "6418f8b5f4df9814bfd25f30a8ef6865929075706f260ca4f48f5334f5541c89": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "5e716e405677ef15a943d1e3ec8577a5436ad2b08b619801fa115e3f6056e7e1": { + "0f2ba6c096112e6b701607e345fc3a06a2a0d93710715cea905546b69b536ec0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "55aa24315699bb2e6b22e1e60dbc59588628d4987393d1709e63283f740da782": { + "ddd11d6e8fd3dfc9734f307630745484bb411a3278c36ad0dd7c681f91edcde5": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "11dbbd2cc435f0d8521f492798f5cdeda39b44612afb5b2e8dbbca244eac8afb": { + "b1108b8a690b2ca3dd55e74294ed8bfdf1496b88ed4b3cb87abf27199c6b819e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "5f600bc0a426abe99c1d0f650b9b8eb243179105c4a73a59f696b99967088fcd": { + "01ede755b925df7f56ae33a0bcc4d822a0495b602eea740c5b8570c025527afa": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "422fd4f38b4009a3443ac2334e4687d21e2eb02d7614a3eec83c7630585d7f4e": { + "fc310ab0e0bd16d08c9b769db925a177fa89b3f539d75b01ef8dd319c427fccf": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "77e8ec1779ffe7a8edd77b8c84358cae99993a9898b4c0cadf2336a88ae99ccd": { + "ad9ca46d7714846bd2a813e74d2af97e1cd25df3b0242f1575db2083f6f9d8b1": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "6779f3038ba17b00bc22bd42f0fb7078c7a3cf7843adc7884e269ad0b33a0180": { + "35eb2ca6e144c429db174c281e9e55d2b9b4e9d819a159203bbdf577354e4033": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "7224ebe3cb4e571ec923f2624b68bd3e183c199258e5523e6456b62e9254f8e5": { + "27bc83275f0f9a5e871d2e4c5af47829d9f7b72c3e5f1f224fbafe3cef986c81": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "9483e1046878fedc712a9345a3e8406df5b24d42f7e0d4500ff8cc9fb1c9a161": { + "19415991350c00c660f9d74c206db9ec7d9fbde7c8a8cbdcfd6775911e40be6b": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "e3e9c957b3953d38da8ada7fadcb6bea2ce78691d7cc34dbc2c75297ee4c5a95": { + "a556037a239ae8b7c3789f59013e015ebf29686de47bce8aa1bb17f99569a4e8": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", @@ -110,14 +110,14 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } }, "evaluations": [ { - "flag": "obfuscation-numeric-integer-0", + "flag": "numeric-integer-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -128,7 +128,7 @@ } }, { - "flag": "obfuscation-numeric-integer-1", + "flag": "numeric-integer-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -139,7 +139,7 @@ } }, { - "flag": "obfuscation-numeric-integer-2", + "flag": "numeric-integer-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -150,7 +150,7 @@ } }, { - "flag": "obfuscation-numeric-integer-3", + "flag": "numeric-integer-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -161,7 +161,7 @@ } }, { - "flag": "obfuscation-numeric-integer-4", + "flag": "numeric-integer-4", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -172,7 +172,7 @@ } }, { - "flag": "obfuscation-numeric-integer-5", + "flag": "numeric-integer-5", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -183,7 +183,7 @@ } }, { - "flag": "obfuscation-numeric-integer-6", + "flag": "numeric-integer-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -212,7 +212,7 @@ ] }, { - "flag": "obfuscation-numeric-integer-7", + "flag": "numeric-integer-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -223,7 +223,7 @@ } }, { - "flag": "obfuscation-numeric-integer-8", + "flag": "numeric-integer-8", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -252,7 +252,7 @@ ] }, { - "flag": "obfuscation-numeric-integer-9", + "flag": "numeric-integer-9", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -263,7 +263,7 @@ } }, { - "flag": "obfuscation-numeric-integer-10", + "flag": "numeric-integer-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -292,7 +292,7 @@ ] }, { - "flag": "obfuscation-numeric-integer-11", + "flag": "numeric-integer-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -309,55 +309,55 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-0" + "key": "numeric-integer-0" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-1" + "key": "numeric-integer-1" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-2" + "key": "numeric-integer-2" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-3" + "key": "numeric-integer-3" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-4" + "key": "numeric-integer-4" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-5" + "key": "numeric-integer-5" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-7" + "key": "numeric-integer-7" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-9" + "key": "numeric-integer-9" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-11" + "key": "numeric-integer-11" } } ], @@ -365,49 +365,49 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-0" + "key": "numeric-integer-0" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-1" + "key": "numeric-integer-1" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-2" + "key": "numeric-integer-2" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-3" + "key": "numeric-integer-3" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-4" + "key": "numeric-integer-4" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-5" + "key": "numeric-integer-5" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-6" + "key": "numeric-integer-6" }, "evaluation_count": 1, "_include": [ @@ -433,14 +433,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-7" + "key": "numeric-integer-7" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-8" + "key": "numeric-integer-8" }, "evaluation_count": 1, "_include": [ @@ -466,14 +466,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-9" + "key": "numeric-integer-9" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-10" + "key": "numeric-integer-10" }, "evaluation_count": 1, "_include": [ @@ -499,7 +499,7 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-integer-11" + "key": "numeric-integer-11" }, "evaluation_count": 1 } @@ -508,15 +508,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json similarity index 79% rename from reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json rename to reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json index 9253064971..7df21eac5d 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-numeric-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json @@ -1,6 +1,6 @@ { "name": "obfuscation-numeric-number", - "description": "Lossless numeric reads are independent of the numeric wire label.", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", "context": { "targetingKey": "fixture-subject", "attributes": {} @@ -10,7 +10,7 @@ "attributes": { "createdAt": "2026-01-01T00:00:00Z", "flags": { - "5d1d3c167fda7f3ecadb68da40228431ed0b74ec08597fc57398fbf7f253f0d6": { + "5a1d7afff3fd522b50e8891f7af14267bdd253a1455ed12bc92188f78e12033d": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -18,7 +18,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "4dcb0659aaeb33eb329143c5ed994c52800521968edafd58c95c50377873ccc4": { + "cbc15a556f7e611fa26f06f6a6334ee91ad5d6a344496452557db41ed992bc25": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -26,7 +26,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "8023572892cfed9fb5ee411c451eab7480329f32331f3845e76234275bede04d": { + "f1c431305b153a3030fadebc0057e634b6162cd6acda9744a763082b1197c740": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -34,7 +34,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "d15970ae43ef67b0a9844f04cd8680567400e12340d545aa608a0cc9b89bed95": { + "7b3cbab857c42d32035705aa891dd7a2fbadcbe1fc0f1e28f3611076a6aa7945": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -42,7 +42,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "4373eda204ffa94cc5c15d29bd5a33b828033f4d2382f708801a7ea5c298b286": { + "acdfe2ec44b3eb617b86da9a49166e3abecbc85c00951e548516cfd748056403": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -50,7 +50,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "5a95ab1a979318a8d113fcdb44a9a5262f0f99a6fb58e0f782b1ac5dbc88f1ef": { + "3af8ba8e93c58c248ed0d943b8bfbc630236bbbeca96be121efc46ed8da4b9b0": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -58,7 +58,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "dceef8e665186596db59e1c8bf2887adf20969530b48091e1bcf3517bf804a7b": { + "efb49302883b084e49d51cb2f41fd795224a571d182536e203f6ab28e6648b77": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -66,7 +66,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "6b8f85c278e445167be847d3a94a91c1865221df7cc63dc8f31aacbb9454e430": { + "c38a503ec9a1c032f0a073e23d6d0ede2eccf245aac33f9db02a5374728c810e": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -74,7 +74,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "81da1069b7cb802d039c7cd27d4c0f4db2515a5c64dd411a522d6f300592c31c": { + "499aef4e9d9d7df120a79816e9406b5925664e851fd638a129f525588e8a9385": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -82,7 +82,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "0a2bcee9cec623b02ca219d7b3f15415767bb630f6bcf9af75fda36c2a689f64": { + "b004cb3fb7dbb1846ba17f461b1a0931cd52d9ffde2c5419d51c0b47e42baf99": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -90,7 +90,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "be4ecda1ccdb165dc68fcffc30f021c7e83a88b8a2283784498ac761385f26d0": { + "d858ab64de6a97983f95792030011b2dd635938aaefd90fee12b37ef67e9dac3": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -98,7 +98,7 @@ "reason": "TARGETING_MATCH", "doLog": true }, - "c4f3ba7ee684fddd93335901ae9b0af3959704d06b5a028449b08854d5384e3d": { + "3ece2688265c5c214d2904619cf0294042830dc10d72dae32e50050c7cded8c3": { "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", @@ -110,14 +110,14 @@ "obfuscated": true, "obfuscation": { "scheme": "flag-key-sha256-v1", - "salt": "00112233445566778899aabbccddeeff" + "salt": "000102030405060708090a0b0c0d0e0f" } } } }, "evaluations": [ { - "flag": "obfuscation-numeric-number-0", + "flag": "numeric-number-0", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -128,7 +128,7 @@ } }, { - "flag": "obfuscation-numeric-number-1", + "flag": "numeric-number-1", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -139,7 +139,7 @@ } }, { - "flag": "obfuscation-numeric-number-2", + "flag": "numeric-number-2", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -150,7 +150,7 @@ } }, { - "flag": "obfuscation-numeric-number-3", + "flag": "numeric-number-3", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -161,7 +161,7 @@ } }, { - "flag": "obfuscation-numeric-number-4", + "flag": "numeric-number-4", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -172,7 +172,7 @@ } }, { - "flag": "obfuscation-numeric-number-5", + "flag": "numeric-number-5", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -183,7 +183,7 @@ } }, { - "flag": "obfuscation-numeric-number-6", + "flag": "numeric-number-6", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -212,7 +212,7 @@ ] }, { - "flag": "obfuscation-numeric-number-7", + "flag": "numeric-number-7", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -223,7 +223,7 @@ } }, { - "flag": "obfuscation-numeric-number-8", + "flag": "numeric-number-8", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -252,7 +252,7 @@ ] }, { - "flag": "obfuscation-numeric-number-9", + "flag": "numeric-number-9", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -263,7 +263,7 @@ } }, { - "flag": "obfuscation-numeric-number-10", + "flag": "numeric-number-10", "variationType": "integer", "defaultValue": -99, "expectedResult": { @@ -292,7 +292,7 @@ ] }, { - "flag": "obfuscation-numeric-number-11", + "flag": "numeric-number-11", "variationType": "float", "defaultValue": -99.5, "expectedResult": { @@ -309,55 +309,55 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-0" + "key": "numeric-number-0" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-1" + "key": "numeric-number-1" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-2" + "key": "numeric-number-2" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-3" + "key": "numeric-number-3" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-4" + "key": "numeric-number-4" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-5" + "key": "numeric-number-5" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-7" + "key": "numeric-number-7" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-9" + "key": "numeric-number-9" } }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-11" + "key": "numeric-number-11" } } ], @@ -365,49 +365,49 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-0" + "key": "numeric-number-0" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-1" + "key": "numeric-number-1" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-2" + "key": "numeric-number-2" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-3" + "key": "numeric-number-3" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-4" + "key": "numeric-number-4" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-5" + "key": "numeric-number-5" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-6" + "key": "numeric-number-6" }, "evaluation_count": 1, "_include": [ @@ -433,14 +433,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-7" + "key": "numeric-number-7" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-8" + "key": "numeric-number-8" }, "evaluation_count": 1, "_include": [ @@ -466,14 +466,14 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-9" + "key": "numeric-number-9" }, "evaluation_count": 1 }, { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-10" + "key": "numeric-number-10" }, "evaluation_count": 1, "_include": [ @@ -499,7 +499,7 @@ { "_count": 1, "flag": { - "key": "obfuscation-numeric-number-11" + "key": "numeric-number-11" }, "evaluation_count": 1 } @@ -508,15 +508,15 @@ "_include": [ { "platform": "ios", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "android", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." }, { "platform": "flutter", - "reason": "Requires native numeric precision or supported obfuscation." + "reason": "Supports obfuscation and the source fixture." } ] } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json new file mode 100644 index 0000000000..eb4655e1ea --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json @@ -0,0 +1,106 @@ +{ + "name": "obfuscation-object-nested-deep", + "description": "Generated obfuscated companion: Object flag with deeply nested structure. SDK must preserve full object hierarchy without flattening or truncating.", + "context": { + "targetingKey": "nested-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "a75e90efc57ed0e56bd11c3a9b17a2623dcc2a412fa52f3ffa742139fa6b93af": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.config.deep", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "deep-config" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json new file mode 100644 index 0000000000..7862719d2d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json @@ -0,0 +1,127 @@ +{ + "name": "obfuscation-special-characters-in-keys", + "description": "Generated obfuscated companion: Flag keys contain dots, hyphens, and underscores. SDK must handle these without mangling the key during lookup or emission.", + "context": { + "targetingKey": "special-key-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5ed289266d5a488851241b3122aab121698b75a75da9fb687c671d606a099d99": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a5bcd95f28f8cf915795582c1edd5b263d2f42cd8a513eb6d69aeef26c39ead0": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e4fb9f7c538eac04c76c225b60d43a48b5f514a112a148d95d25754ad14cc8a8": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "dfd73ab3f42a47bd441de225f613432120e3eb38a6f00a21cb19a45a9de23c70": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "com.example.feature.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "my-team--feature-toggle", + "variationType": "string", + "defaultValue": "no", + "expectedResult": { + "value": "yes", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "active" + } + }, + { + "flag": "legacy_flag_name_v2", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 100, + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "v2-val" + } + }, + { + "flag": "mix.ed-key_style.v1", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 2.718, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "mixed-val" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 4 + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json new file mode 100644 index 0000000000..d7950c8e68 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json @@ -0,0 +1,148 @@ +{ + "name": "obfuscation-type-mismatch-all-types", + "description": "Generated obfuscated companion: Wrong nonnumeric getters return TYPE_MISMATCH; an exact numeric conversion succeeds.", + "context": { + "targetingKey": "mismatch-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "937d54653333211a63876dd0e042edb2ef108105fb918420af28ff03f620a982": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ed1838c062426321f2125dd154000611b46339c11bbbab4228df3fad88007f6c": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7ba0090b5e66bc7319950479a1e905c26fedd82e605530cdb4eb0adb4c7d5515": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1fa87b21c5821755d962ce404e18a06c5ae2656dd33d4e915e80e425fc77c190": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9a172110c6d75715ead4e47f3063d7ab4d92141de6f83681141130da17f9b743": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.mismatch.bool-as-string", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.string-as-int", + "variationType": "integer", + "defaultValue": -1, + "expectedResult": { + "value": -1, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.int-as-float", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 7, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "var-int" + } + }, + { + "flag": "fixture.mismatch.float-as-bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.object-as-string", + "variationType": "string", + "defaultValue": "none", + "expectedResult": { + "value": "none", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json index d3d7c926ea..6037badcb4 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/index.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -18,13 +18,26 @@ "cases/evaluator/object-nested-deep.json", "cases/evaluator/special-characters-in-keys.json", "cases/evaluator/type-mismatch-all-types.json", + "cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json", + "cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json", + "cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json", + "cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json", + "cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json", + "cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json", + "cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json", + "cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json", + "cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json", + "cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json", + "cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json", + "cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json", "cases/obfuscation/obfuscation-keys.json", - "cases/obfuscation/obfuscation-numeric-boundaries-float.json", - "cases/obfuscation/obfuscation-numeric-boundaries-integer.json", - "cases/obfuscation/obfuscation-numeric-boundaries-number.json", - "cases/obfuscation/obfuscation-numeric-float.json", - "cases/obfuscation/obfuscation-numeric-integer.json", - "cases/obfuscation/obfuscation-numeric-number.json", "cases/obfuscation/obfuscation-reject-false-with-descriptor.json", "cases/obfuscation/obfuscation-reject-missing-descriptor.json", "cases/obfuscation/obfuscation-reject-missing-marker.json", From 5ff5ae5bf735f1eaf7a4fe1420b514d6d640f883 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 3 Nov 2026 19:05:19 -0500 Subject: [PATCH 41/44] FFL-3568: Refresh integral floating-value fixtures Ref: FFL-3568 --- .../src/test/resources/precomputed/SOURCE.json | 14 +++++++------- .../precomputed/cases/evaluator/numeric-float.json | 2 +- .../cases/evaluator/numeric-integer.json | 2 +- .../cases/evaluator/numeric-number.json | 2 +- .../evaluator/obfuscation-numeric-float.json | 2 +- .../evaluator/obfuscation-numeric-integer.json | 2 +- .../evaluator/obfuscation-numeric-number.json | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json index 13c31d7d3d..cd406db832 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -1,6 +1,6 @@ { "repository": "https://github.com/DataDog/ffe-system-test-data.git", - "commit": "8dcc09a53dfb654cb0b2de8cda96919710f074e6", + "commit": "faa89edca7043428824ef65050e3293113100624", "files": { "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", @@ -18,9 +18,9 @@ "cases/evaluator/numeric-boundaries-float.json": "f986d58267e590f1be98dc9fe4b5151852c9565028469735b30885a6f369eaa9", "cases/evaluator/numeric-boundaries-integer.json": "4569c35d977fff5c77134b9e0342b740b7a18e4cf01e461f0c8b5ed9c250708b", "cases/evaluator/numeric-boundaries-number.json": "ab1e965ac0b335bf56442c0bfa262ba62214b3118a1c1cda6512f38b38717294", - "cases/evaluator/numeric-float.json": "d889bf0107d350e47fde325836caf6690c643b779c51fc3812f948e22a8c67ce", - "cases/evaluator/numeric-integer.json": "4d080f862f53c6d7fab3670a3793edb1ddd9e566a68e262e59abfaff689db39d", - "cases/evaluator/numeric-number.json": "cf2aafebf871b57f0f3a74abf0126d6a4f35cb18d78602611ee4419e6f9235d9", + "cases/evaluator/numeric-float.json": "8a61435912d1ad7c25dc34bfecb030f4d7583f5b4c40ad4f8659b9cce50d93fc", + "cases/evaluator/numeric-integer.json": "b90b486688aa5462028175fe661d4d90bcf933089b63b8092c0004cf181f895f", + "cases/evaluator/numeric-number.json": "36401b78a6ddb09e4fb81c93729325330d090ef58e4be2dc3531e30b8d8da43d", "cases/evaluator/object-nested-deep.json": "22f8f0c516cfe0456d34697f29bbf39d7736559e0b188de4671d0fee6545fd3d", "cases/evaluator/special-characters-in-keys.json": "07e24ba1a407ef9604af0bc5ab3f8faafe199dd83db94d96d0e3d689430054ce", "cases/evaluator/type-mismatch-all-types.json": "0904425126ae64794a8c67d13d4bab759a88abc75c27c99f71c514babb9279a3", @@ -37,9 +37,9 @@ "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "3ad1a96dffe7d376e8c381b6b089fb7cd30423b39d8f7f4006fa86fdab3a78bd", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "01269d0992978348fe0cba509978ba4b10e65a7bb03284847efb1be1a014d7da", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "df03044f0726dfe353b172ae75daf205fa4280a76ec05ac4dfc310f51ca186f6", - "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "702674897e5c457c070e0abdb112a38e5c1274d7e21ae314f5e2dfa805320a41", - "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "13a177de29ae708fd8361c095279d78166ae82d2fbe8d25804d5790d527bd3d1", - "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "3e8a78b7016acbcce301e1f437f145f9a24737989d70bda00fba2f9e8ffb2f11", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "c79fcdca8b38fc24098489de8c85ba78412a8fccbe731b89eae6ac885642bb22", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "aa1936cf515645f546de081c9769f9f67b99ff68a178eaa50df482a1b7ecf1d0", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "50aee440e18d7192fe2550dc9782229edcb22535122cca84e3891def76594b0e", "cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json": "ff2ba07eb266db0669f0f86926060d268f3242d437264b5d4663f259317d6bc1", "cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json": "fd4f9efb1eb115e505c81a30ae82c51766747cbc26ae5a36dda961a4cfeb2cd8", "cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json": "6dc2320ceb7013825dbda712ed789689a0bd3b36294a741a8bc23371bbe7db91", diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json index dd8b3cb87b..43e5f87663 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json index e1ce07409f..2b4e63cfb3 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json index c97208a848..498ff4afe8 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json index cb5d7f4057..bf65bb4b90 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "float", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json index b1ec8e6115..e61afa1d8a 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "integer", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json index 7df21eac5d..6ea6bdf921 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json @@ -30,7 +30,7 @@ "allocationKey": "allocation", "variationKey": "variant", "variationType": "number", - "variationValue": 5, + "variationValue": 5.0, "reason": "TARGETING_MATCH", "doLog": true }, From c84ae6142eca67f09d2ffd4245d8684fb838e03e Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Nov 2026 18:14:35 +0000 Subject: [PATCH 42/44] FFL-3568: Apply rounded numeric reads with canonical fixtures Preserve original numeric precision for typed reads and cache restoration. Ref: FFL-3568 --- ci/sync-precomputed-fixtures.py | 45 +- .../flags/internal/FlagValueConverter.kt | 12 +- .../flags/internal/DatadogFlagsClientTest.kt | 2 +- .../src/test/resources/precomputed/README.md | 65 ++- .../test/resources/precomputed/SOURCE.json | 77 ++- .../evaluator/numeric-decimal-float.json | 487 +++++++++++++++++ .../evaluator/numeric-decimal-integer.json | 487 +++++++++++++++++ .../evaluator/numeric-decimal-number.json | 487 +++++++++++++++++ .../obfuscation-numeric-decimal-float.json | 506 ++++++++++++++++++ .../obfuscation-numeric-decimal-integer.json | 506 ++++++++++++++++++ .../obfuscation-numeric-decimal-number.json | 506 ++++++++++++++++++ .../src/test/resources/precomputed/index.json | 6 + .../cases/emissions/all-doLog-false.json | 33 ++ .../cases/emissions/duplicate-evaluation.json | 17 + .../emissions/serial-id-on-exposure.json | 43 ++ .../cases/evaluator/all-types-success.json | 55 ++ .../defaults-and-emission-gates.json | 33 ++ .../cases/evaluator/empty-flags-response.json | 8 + .../cases/evaluator/falsy-values.json | 49 ++ .../integer-precision-native-only.json | 17 + .../cases/evaluator/multi-flag-set.json | 411 ++++++++++++++ .../cases/evaluator/multiple-reasons.json | 33 ++ .../evaluator/numeric-boundaries-float.json | 185 +++++++ .../evaluator/numeric-boundaries-integer.json | 185 +++++++ .../evaluator/numeric-boundaries-number.json | 185 +++++++ .../evaluator/numeric-decimal-float.json | 97 ++++ .../evaluator/numeric-decimal-integer.json | 97 ++++ .../evaluator/numeric-decimal-number.json | 97 ++++ .../cases/evaluator/numeric-float.json | 105 ++++ .../cases/evaluator/numeric-integer.json | 105 ++++ .../cases/evaluator/numeric-number.json | 105 ++++ .../cases/evaluator/object-nested-deep.json | 35 ++ .../evaluator/special-characters-in-keys.json | 41 ++ .../evaluator/type-mismatch-all-types.json | 51 ++ .../obfuscation-all-doLog-false.json | 38 ++ .../obfuscation-duplicate-evaluation.json | 22 + .../obfuscation-serial-id-on-exposure.json | 48 ++ .../obfuscation-all-types-success.json | 60 +++ ...fuscation-defaults-and-emission-gates.json | 38 ++ .../obfuscation-empty-flags-response.json | 13 + .../evaluator/obfuscation-falsy-values.json | 54 ++ ...scation-integer-precision-native-only.json | 22 + .../evaluator/obfuscation-multi-flag-set.json | 416 ++++++++++++++ .../obfuscation-multiple-reasons.json | 38 ++ .../obfuscation-numeric-boundaries-float.json | 190 +++++++ ...bfuscation-numeric-boundaries-integer.json | 190 +++++++ ...obfuscation-numeric-boundaries-number.json | 190 +++++++ .../obfuscation-numeric-decimal-float.json | 102 ++++ .../obfuscation-numeric-decimal-integer.json | 102 ++++ .../obfuscation-numeric-decimal-number.json | 102 ++++ .../evaluator/obfuscation-numeric-float.json | 110 ++++ .../obfuscation-numeric-integer.json | 110 ++++ .../evaluator/obfuscation-numeric-number.json | 110 ++++ .../obfuscation-object-nested-deep.json | 40 ++ ...bfuscation-special-characters-in-keys.json | 46 ++ .../obfuscation-type-mismatch-all-types.json | 56 ++ .../cases/obfuscation/obfuscation-keys.json | 78 +++ ...uscation-reject-false-with-descriptor.json | 13 + ...obfuscation-reject-missing-descriptor.json | 9 + .../obfuscation-reject-missing-marker.json | 12 + .../obfuscation-reject-missing-salt.json | 12 + .../obfuscation-reject-nonhex-digest.json | 22 + .../obfuscation-reject-nonhex-salt.json | 13 + .../obfuscation-reject-null-descriptor.json | 10 + .../obfuscation-reject-null-marker.json | 9 + .../obfuscation-reject-null-salt.json | 13 + .../obfuscation-reject-plaintext.json | 22 + .../obfuscation-reject-short-digest.json | 22 + .../obfuscation-reject-short-salt.json | 13 + .../obfuscation-reject-string-marker.json | 13 + .../obfuscation-reject-unknown-scheme.json | 13 + .../obfuscation-reject-uppercase-digest.json | 22 + .../obfuscation-reject-uppercase-salt.json | 13 + .../obfuscation/obfuscation-rotated-salt.json | 78 +++ .../obfuscation-unsupported-consumer.json | 22 + .../test/resources/precomputed/schema.json | 16 + 76 files changed, 7671 insertions(+), 24 deletions(-) create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json create mode 100644 reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json diff --git a/ci/sync-precomputed-fixtures.py b/ci/sync-precomputed-fixtures.py index e6ecb047ed..0ad58fb521 100755 --- a/ci/sync-precomputed-fixtures.py +++ b/ci/sync-precomputed-fixtures.py @@ -27,6 +27,30 @@ def git(source, *args): return subprocess.check_output(['git', '-C', str(source), *args]) +def response_bytes(content): + """Extract the top-level response without re-encoding any numeric token.""" + text = content.decode('utf-8') + decoder = json.JSONDecoder() + position = text.index('{') + 1 + while True: + while text[position].isspace() or text[position] == ',': + position += 1 + if text[position] == '}': + raise ValueError('Fixture is missing response') + key, position = decoder.raw_decode(text, position) + while text[position].isspace(): + position += 1 + if text[position] != ':': + raise ValueError('Invalid fixture object') + position += 1 + while text[position].isspace(): + position += 1 + start = position + _, position = decoder.raw_decode(text, position) + if key == 'response': + return text[start:position].encode('utf-8') + b'\n' + + def snapshot(source, ref): if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', ref) or '..' in ref: raise ValueError('Invalid fixture revision') @@ -57,6 +81,7 @@ def snapshot(source, ref): raise ValueError('Invalid fixture identity: ' + path) if not isinstance(data.get('evaluations'), list): raise TypeError('Missing evaluations: ' + path) + result['responses/' + path] = response_bytes(result[path]) result['index.json'] = (json.dumps(cases, indent=2) + '\n').encode() result['SOURCE.json'] = (json.dumps({ 'repository': REPOSITORY, @@ -117,8 +142,26 @@ def main(): if relative.is_absolute() or '..' in relative.parts: raise ValueError('Destination must be inside the repository') destination = root / relative - if not destination.resolve().is_relative_to(root): + resolved = destination.resolve() + if not resolved.is_relative_to(root): raise ValueError('Destination escapes the repository') + if resolved == root or '.git' in relative.parts: + raise ValueError('Destination must be a dedicated directory inside the repository') + if resolved != destination: + raise ValueError('Destination must not contain symbolic links') + if destination.exists(): + if not destination.is_dir(): + raise ValueError('Destination must be a directory') + if any(destination.iterdir()): + provenance = destination / 'SOURCE.json' + if not provenance.is_file(): + raise ValueError('Destination must be empty or contain a previous snapshot') + metadata = json.loads(provenance.read_text()) + if metadata.get('repository') != REPOSITORY or not isinstance(metadata.get('files'), dict): + raise ValueError('Destination must contain valid snapshot provenance') + extra = read_snapshot(destination).keys() - metadata['files'].keys() - {'SOURCE.json'} + if extra: + raise ValueError('Destination contains files outside the previous snapshot') if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', args.ref) or '..' in args.ref: raise ValueError('Invalid fixture revision') if args.verify: diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt index 89262ae994..9e41a76782 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt @@ -102,14 +102,16 @@ internal object FlagValueConverter { if (number == null || double == null) { return Result.failure(IllegalArgumentException("Failed to parse value '$value'")) } - // Accept the exact binary value or its standard JSON decimal round trip. + // Fractional values round normally; original whole values must stay exact. + @Suppress("UnsafeThirdPartyFunctionCall") // BigDecimal arithmetic failures are handled below. val exact = try { - double.isFinite() && ( - number.compareTo(BigDecimal(double)) == 0 || - number.compareTo(BigDecimal.valueOf(double)) == 0 - ) + double.isFinite() && + (double != 0.0 || number.signum() == 0) && + (number.stripTrailingZeros().scale() > 0 || number.compareTo(BigDecimal(double)) == 0) } catch (_: NumberFormatException) { false + } catch (_: ArithmeticException) { + false } return if (exact) { Result.success(double) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt index fd925bc316..2a3223629c 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt @@ -1593,7 +1593,7 @@ internal class DatadogFlagsClientTest { val fakeFlagKey1 = forge.anAlphabeticalString() val fakeFlagKey2 = forge.anAlphabeticalString() val fakeFlagKey3 = forge.anAlphabeticalString() - val fakeFlagValue = forge.aDouble() + val fakeFlagValue = forge.aDouble(min = -1_000_000.0, max = 1_000_000.0) val fakeFlagIntValue = forge.anInt() val fakeDefaultValue = forge.aDouble() diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/README.md b/reliability/single-fit/flags/src/test/resources/precomputed/README.md index 380812a7f9..1bf7d8c89f 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/README.md +++ b/reliability/single-fit/flags/src/test/resources/precomputed/README.md @@ -82,8 +82,7 @@ exposure and evaluation events. {"_count": 1, "flag": "my-flag"} ], "evaluationEvents": [ - {"_count": 2, "_skip": [{"platform": "web"}]}, - {"_count": 1, "_include": [{"platform": "web"}], "evaluation_count": 2} + {"_count": 1, "evaluation_count": 2} ] } ``` @@ -148,7 +147,8 @@ Valid platform identifiers: `web`, `ios`, `android`, `react-native`, `flutter`, 1. Recursively discover `*.json` files under `precomputed-assignments/cases/`. 2. For each fixture, check `_skip`/`_include` at case level. Skip if the current platform is excluded. -3. Initialize the SDK with `context`. Feed `response` as the mocked server +3. Initialize the SDK with `context`. Preserve every numeric literal in `response` + exactly as written in the fixture. Feed `response` as the mocked server payload. 4. Walk `evaluations` in order. For each evaluation not skipped for the current platform, call the typed getter (e.g., `getBooleanValue`) with `flag`, @@ -181,15 +181,23 @@ python3 ci/validate-precomputed-fixtures.py ## Mobile conformance and numeric types Numeric reads depend on the value, not the `integer`, `float`, or `number` label. -Integer getters accept whole values within the accessor range. Double getters -accept numeric values without losing integer precision. Fractional Integer reads, -out-of-range reads, and inexact conversions return the default with `TYPE_MISMATCH`. -No new global safe-integer limit is imposed. React Native and web expose one Number -getter; Integer-only cases use explicit platform filters. Boundary fixtures cover -native 32-bit and 64-bit accessors separately. +Integer getters accept only mathematically whole original JSON values within the +accessor range. A fractional token must return the default with `TYPE_MISMATCH`, +even if a JSON decoder would round it to an integer (for example, +`1.00000000000000001` or `4503599627370497.5`). + +Double getters round fractional values to the nearest finite Double: for example, +`1.20000000000000001` resolves to `1.2`. Whole values succeed only if the Double +represents that original integer exactly. Overflow and nonzero values that +underflow to zero return the default with `TYPE_MISMATCH`. Signed zero is +numerically equivalent to zero. These rules apply before and after cache restore. + +No global safe-integer limit is imposed on native Integer getters. React Native +and web expose one Number getter; native Integer cases use explicit platform +filters. Boundary fixtures cover native 32-bit and 64-bit accessors separately. The precompute service retains `number`. No numeric capability is introduced. -See FFL-3568 and FFL-3564 for the lossless conversion decision. +See FFL-3568 and FFL-3564 for the numeric conversion decision. Discover cases recursively. Do not maintain a filename list. Fail when the corpus is missing or empty. Report the fixture revision, executed evaluations, @@ -198,7 +206,8 @@ The generated `index.json` supports environments that cannot enumerate packaged resources. It must be regenerated from the complete directory, never edited. `expectedResponseError: "INVALID_RESPONSE"` requires rejection of the complete -response before installation. Such a case has no evaluations. Assert rejection +response before installation. Such a case has no evaluations or emitted events; +require `noUnmatchedEvents: true` and empty event matchers. Assert rejection explicitly; do not catch every exception as a successful expected rejection. The obfuscation cases use `flag-key-sha256-v1`. Hash the UTF-8 domain prefix @@ -208,9 +217,12 @@ digest using the SDK being tested. Lookup uses the caller's plaintext key; emitted events retain that plaintext identity. Missing encoded lookups must not retry plaintext lookup. Unsupported consumers must reject encoded responses. -The corpus checks duplicate evaluations after one flush. Aggregating consumers -emit one event with `evaluation_count: 2`. Exposure deduplication is independent. +The corpus checks duplicate evaluations after one flush. Every consumer must +aggregate them into one event with `evaluation_count: 2`. Exposure deduplication is independent. Runners must inspect real event records, not synthesize them from expectations. +Feed `response` to the SDK with every number spelled as in the fixture file. +Parsing and re-encoding through floating-point values can change `5.0` to `5` +or `9007199254740993.5` to `9007199254740994`, hiding decoder defects. Keep native bridge and deployed-backend proof separate from mocked conformance. ## Plain and obfuscated coverage @@ -249,9 +261,36 @@ those known digests, including Unicode, whitespace, and embedded NUL keys. ## Snapshot maintenance +### Preview client compatibility in fixture PRs + +The [mobile compatibility workflow](../.github/workflows/mobile-compatibility.yml) +runs the iOS canonical XCTest suite on an iOS Simulator and the Android canonical +JVM unit suite on Linux. Each job checks out its SDK once, records the resolved +commit, and runs that same code against the fixture merge base and proposed head. +Snapshots are imported into disposable CI checkouts; SDK repositories are not +updated or published. The SDK refs temporarily select the development branches +containing the canonical runners. After those land, change the workflow refs to +the SDKs' default branches (`develop`). + +The PR's Actions summaries show both fixture revisions, the SDK commit, test +counts, and failures. Artifacts contain separate logs, JUnit XML or Xcode result +bundles, and `comparison.json`. Test mismatches are advisory. Missing tests, +snapshot failures, and build/runner failures make the preview incomplete and are +reported separately. When both revisions fail, the result is inconclusive, not +proof that the proposed fixtures introduce no additional failures. + +These jobs run the checked-out SDK runners unchanged. Their coverage and numeric +parsing limitations still apply: a green run does not prove preservation of raw +numeric literals, native-device behavior, or deployed-backend compatibility. + +### Refresh consumer snapshots + Consumers can copy `ci/sync-precomputed-fixtures.py` and configure the destination in `ci/precomputed-fixtures.json`. The importer copies committed source bytes, records the full SHA and file digests in `SOURCE.json`, generates `index.json`, +and writes byte-preserving `responses/cases/...json` payloads for native runners. +The response files preserve original numeric tokens, including overflow values. +The importer refreshes the complete snapshot and removes deleted cases. `--check` compares the complete snapshot with its recorded source commit. `--source` permits testing a committed local fixture branch before publication. Tests use the checked-in snapshot without networking. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json index cd406db832..72902e8581 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -1,10 +1,10 @@ { "repository": "https://github.com/DataDog/ffe-system-test-data.git", - "commit": "faa89edca7043428824ef65050e3293113100624", + "commit": "f2e3dc49927e8290554cc7e2d7bc2bfdc8a9c2fa", "files": { "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", - "README.md": "ceb34cf7027004641182ec3327963adc1bd7b8a2b5f6faed94cd67f4a548d956", + "README.md": "fa341eba8a1bd3dc5c20e167193b2b080e1bb0a6b709a5a74f8e89020b1b57a1", "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", "cases/emissions/serial-id-on-exposure.json": "83a7504e6658b875ececdd1972c04386130feb31c607ccf7b61cd45ce46c1fc0", @@ -18,6 +18,9 @@ "cases/evaluator/numeric-boundaries-float.json": "f986d58267e590f1be98dc9fe4b5151852c9565028469735b30885a6f369eaa9", "cases/evaluator/numeric-boundaries-integer.json": "4569c35d977fff5c77134b9e0342b740b7a18e4cf01e461f0c8b5ed9c250708b", "cases/evaluator/numeric-boundaries-number.json": "ab1e965ac0b335bf56442c0bfa262ba62214b3118a1c1cda6512f38b38717294", + "cases/evaluator/numeric-decimal-float.json": "b761ce8c7581c96799e30a07f13f5e7492e4610d9270eaf3cfbf276bca6c66cd", + "cases/evaluator/numeric-decimal-integer.json": "ddd1cb6c938c0c63effbb672ea7bef979eed3d424cd156d512b71b50091cc246", + "cases/evaluator/numeric-decimal-number.json": "1bd3efccd7a25667e17048868577180b78e51024af1036cbb39cdf79f30f4499", "cases/evaluator/numeric-float.json": "8a61435912d1ad7c25dc34bfecb030f4d7583f5b4c40ad4f8659b9cce50d93fc", "cases/evaluator/numeric-integer.json": "b90b486688aa5462028175fe661d4d90bcf933089b63b8092c0004cf181f895f", "cases/evaluator/numeric-number.json": "36401b78a6ddb09e4fb81c93729325330d090ef58e4be2dc3531e30b8d8da43d", @@ -37,6 +40,9 @@ "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "3ad1a96dffe7d376e8c381b6b089fb7cd30423b39d8f7f4006fa86fdab3a78bd", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "01269d0992978348fe0cba509978ba4b10e65a7bb03284847efb1be1a014d7da", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "df03044f0726dfe353b172ae75daf205fa4280a76ec05ac4dfc310f51ca186f6", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json": "d59ce0b4a9600afe68bf7e834dbd0cae7c1db07f147aaf217c148c31e4c0de5b", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json": "bd009c03bf86286d6d613854a2e2472f99e4ef24daf6a7c806cdf01c3e69a20d", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json": "6d2d9129357f864048532ee17d30af88aac4f522aabce844af7fc1c38ae72ff0", "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "c79fcdca8b38fc24098489de8c85ba78412a8fccbe731b89eae6ac885642bb22", "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "aa1936cf515645f546de081c9769f9f67b99ff68a178eaa50df482a1b7ecf1d0", "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "50aee440e18d7192fe2550dc9782229edcb22535122cca84e3891def76594b0e", @@ -62,7 +68,70 @@ "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", - "index.json": "de2340499732d87bb4eda3fc0c0e3e2f188bda6f105bcf659c5d8824e9ca9d48", - "schema.json": "0353d393b1b4193346b2fa907f500b5c60be51318759c3afe52e549f1662195d" + "index.json": "70bc5b6ec0800a5bddb9b92eb5b57ec4a89fb2492489dd4fd8759d02015222a6", + "responses/cases/emissions/all-doLog-false.json": "89c1d35aeade0441f7686c355f5871bf1730807e0146a99dcd2e65ae3fe05599", + "responses/cases/emissions/duplicate-evaluation.json": "177cd825091d152be16510ed54fcdd35c8b50b96dbc52f57220bd470d5a9c703", + "responses/cases/emissions/serial-id-on-exposure.json": "99da3ed72a4884137614540a3023d02c6e0dc38f277786d3b377a02465fc0a9e", + "responses/cases/evaluator/all-types-success.json": "5d291b27cca2bc090fcd0c261b3ac601057d1cf210da1b24f57dc12c3e2c7541", + "responses/cases/evaluator/defaults-and-emission-gates.json": "6f77d538af3d603fd33e056a2c777854a01a8fbb8b960baf32005e37cbe488ef", + "responses/cases/evaluator/empty-flags-response.json": "ed293d7bbd20289fd8c2391172cd969e0491c14b0e7dba4060a044a56c1e4248", + "responses/cases/evaluator/falsy-values.json": "f661eec595c65d62ad460bf33c86034cbdd2ff8b8e1e059535c1b025fc6baa7e", + "responses/cases/evaluator/integer-precision-native-only.json": "77aae5ed5dd4c657731b96b750d597624c5d6795d3e397138fae490b4aa03b87", + "responses/cases/evaluator/multi-flag-set.json": "91ebff0413103b47c79279c1d4eab1c6faf7fc173de4ba5c7bb8a7b94364b0c9", + "responses/cases/evaluator/multiple-reasons.json": "5aba3636f4b45ed7da75ea911df19761ad85c9afa35c1eab224a26eab78c1e6b", + "responses/cases/evaluator/numeric-boundaries-float.json": "aa829adbbc19079c36116edf4fde5f8ec1948b082053bf318e7e283784ac0334", + "responses/cases/evaluator/numeric-boundaries-integer.json": "0546104138d058f1b637cbeac016c2e89ab90a9da70ec569e30b234c83f6d96f", + "responses/cases/evaluator/numeric-boundaries-number.json": "055efaaa62bca30b42a8592bf5e58e7b33ef9656435dd877274164f738fb7e48", + "responses/cases/evaluator/numeric-decimal-float.json": "6eebc0d9901064acf93819c951ee3069b3988e3ed3520be6c201d4a3dbff6392", + "responses/cases/evaluator/numeric-decimal-integer.json": "59c1608fb24f99161e65083f97535eb6aba8f3ec5b0a01141803af5fde87d479", + "responses/cases/evaluator/numeric-decimal-number.json": "42d570275fcdd93fd176b51ddf3750770e7378d875fe0bea3f422de4ea000356", + "responses/cases/evaluator/numeric-float.json": "1f4044ba42e73dc10d414fe7ebe8bcdc689ea3c7d6f8bbf0e33cfd3ac2959a59", + "responses/cases/evaluator/numeric-integer.json": "dc09c454e1032e2a6d4e39275c3e1f53ee1f49a79477948bb2eb057059acd60e", + "responses/cases/evaluator/numeric-number.json": "abd8f7d330f539461a0831d5bc8e9ccf5131f82ecfb62486f19fd0fa3345ed6b", + "responses/cases/evaluator/object-nested-deep.json": "4640392a8ae063d097aff33c91a2dddbb0e9061326913e4cad517b8e55fe834c", + "responses/cases/evaluator/special-characters-in-keys.json": "ec8fd2f9a8e8e3006c0330bd2904876805351644ca2e7daec50b0a398cb92739", + "responses/cases/evaluator/type-mismatch-all-types.json": "56f5847e621556e8a5cd531c1fa972dd71a83a9ecfe9cbcf59a7dfa345bccdad", + "responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json": "c148ca5cd2fb46458ea2e194c4049f67a460866061bc5b0f18c68f139b613559", + "responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json": "be559d52b7968786708fd85714c29d085d10f446a43228ba6560593df0a179c7", + "responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json": "d85bef9f0499c391a3c49c1c905f340070f28b6ad2c5e71870113af474a2caf4", + "responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json": "e978fea7702d085debc89e87c0fe9fc446dededf55ecc2fa1780ec5a68dbb5c0", + "responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json": "8e4d4cf240791dec6e558fdf69e23be8dd6a390ae0b914deeacfbd609ea53c4a", + "responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json": "0171cf6510766ffc8d2a2f46bbdf63e4e50fd52e4cf562b1e94f979092ea2c2f", + "responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json": "041147351c6425b41cb45bad5e2b5756b9df2f6b1744c1017353efd97124c167", + "responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json": "9f10390cd804cee826ae5c3afb5255696d8923618166674c132937f9899a8443", + "responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json": "39939e1f0c0546f77e0b7e31f33e95394a92930d52140ff3fa39c4fe432856ae", + "responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json": "430c59292f0477f35803a06f0c36073dadb5e8798f22236a1073d0a09a23524d", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "990785d2552476e95123605287e48d29f2f359643bcda9613ab41038061fd3db", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "49009b3df826c49e944a0eb3445e5ea0f962e92a9b5a1d6149008dd87a1ed6a4", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "657761bef657fd4e659a57f7c8444b602f20f8e1da138057ff6c49b1d7e39e54", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json": "fa89b884af2932d9e4bb7511ed25b2dbd64d2d1e4bae89bddda45d8efeda3337", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json": "d06a3937bf86f7b72598ae04fa2270415ba743442d6e998bf50d9488fc0ee6cd", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json": "8799cd38515b94f6a5ef65069e082e17e4b1a7fc34724f3bcc0958ba45c82f1e", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "293398fae9b54a63119bafe0ed0b57734142d2b770510d32c06e17fbb247a7c3", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "38d2d7308b4b53f1c481b38222169213ea8bcfd0cacb82352082ccdbd9325138", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "0647d09fd2d5f855260807de730efb9f04b5ffd050a18e2ed81b59e2ba45af48", + "responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json": "46816998e484b406fa07926dd5d176918dadfc9d45130682a20a066566e0b0c3", + "responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json": "3e413184859e660e92d85d945001ed10e20b8849e672286c3335220dcdd61598", + "responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json": "bf80790c19597c6b9b53a365363bab1c2b68ae226567e3e11f037dc8fbe39747", + "responses/cases/obfuscation/obfuscation-keys.json": "6a448f288efbd6046cf99abb7f37c9ec17d6de33f35d572df5b3ccb6b4b56a91", + "responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "5591fc19a3c234d1bd8b338a130d01a4fac0fc61751bbac58b6da884b30bfe57", + "responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json": "d68791162facae870c8137c709a74b3d50ba6a6cce886cad2c1c514daabc62ae", + "responses/cases/obfuscation/obfuscation-reject-missing-marker.json": "a4c992b2b5b84c158172b351589078367ad6cca8e1f8a5401bc769414428b378", + "responses/cases/obfuscation/obfuscation-reject-missing-salt.json": "8ab59be9c51554b478bd19c801a2eacdf835f61cd271385554952fcf58cd25ac", + "responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json": "3821060eced10838b7853dad305a1e3d42e497276763d503558dfa54a4af4ca4", + "responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json": "f63a963101303f8853c302970190898d578ece967e3152691b31259c1c55ce5c", + "responses/cases/obfuscation/obfuscation-reject-null-descriptor.json": "7dc0425326051934b462745496647f8b58db9508f9230571bf5e7a2eb17c55da", + "responses/cases/obfuscation/obfuscation-reject-null-marker.json": "ca2784e1b92713cb783a806691573661efe012e6a1c55affc6683211cc7c4949", + "responses/cases/obfuscation/obfuscation-reject-null-salt.json": "5cd3e4586774d1647f3c61fa8c69e6e1f5bd69118895df8a67e085eb8872a1df", + "responses/cases/obfuscation/obfuscation-reject-plaintext.json": "f1c6acea2f48e670695a8027aa9b63caed06ae2f78638ce9bb465a38865d5cb6", + "responses/cases/obfuscation/obfuscation-reject-short-digest.json": "eb1975ac97f4a783a9a28ef45f320dc074de260b3aad183b5008cc5776f8e0b9", + "responses/cases/obfuscation/obfuscation-reject-short-salt.json": "42ace4d0cb12d42ea45d0e2564202e95165010a505657b3c8e0f392a409d803e", + "responses/cases/obfuscation/obfuscation-reject-string-marker.json": "6db02000ec578fb36a22d61377b543f5a47827f4890f07ba80ec1a93212c3a5e", + "responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json": "8480004116f463a8003b9de2eeb799f9aed19996f51190464b4213bf48503f97", + "responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json": "18251f8d995ba1ac1ac18a045f37207a6ef047f915af503f710c101d4654bee7", + "responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json": "6bc9c2051caeaf47ef04ae5516da71d732aaa24cf1b99ac617bad83a7f76009d", + "responses/cases/obfuscation/obfuscation-rotated-salt.json": "5611a85ccd64179d38a6fc18bfd70704eabea85d87e16ab14b7dafc31bfcfbc8", + "responses/cases/obfuscation/obfuscation-unsupported-consumer.json": "3cc8fd716bdc34bf00526461f97366f9278fdab0a9340b1f62ea1ac133d89070", + "schema.json": "cb3575d02c1d9cc8f85847b86436cc0aa38e1426c78f797ed28352ffe7ac6db7" } } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json new file mode 100644 index 0000000000..a32ef2e084 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-float", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json new file mode 100644 index 0000000000..b7710857dc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-integer", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json new file mode 100644 index 0000000000..a0a3b234cc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-number", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json new file mode 100644 index 0000000000..fa184c7665 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-float", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json new file mode 100644 index 0000000000..ae7e9126bf --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-integer", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json new file mode 100644 index 0000000000..a7e078942a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-number", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json index 6037badcb4..03d2dbe3b9 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/index.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -12,6 +12,9 @@ "cases/evaluator/numeric-boundaries-float.json", "cases/evaluator/numeric-boundaries-integer.json", "cases/evaluator/numeric-boundaries-number.json", + "cases/evaluator/numeric-decimal-float.json", + "cases/evaluator/numeric-decimal-integer.json", + "cases/evaluator/numeric-decimal-number.json", "cases/evaluator/numeric-float.json", "cases/evaluator/numeric-integer.json", "cases/evaluator/numeric-number.json", @@ -31,6 +34,9 @@ "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json", "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json", "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json", "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json", "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json", diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json new file mode 100644 index 0000000000..11f45b7b77 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.silent.bool": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.string": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.int": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json new file mode 100644 index 0000000000..fe351c47c7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json @@ -0,0 +1,17 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.dedup.flag": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json new file mode 100644 index 0000000000..fec74dab07 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json @@ -0,0 +1,43 @@ +{ + "data": { + "attributes": { + "flags": { + "serial.id.flag": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "no.serial.id.flag": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "zero.serial.id.flag": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "null.serial.id.flag": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json new file mode 100644 index 0000000000..1151852b05 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json @@ -0,0 +1,55 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.enabled": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.title": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.limit": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.ratio": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.config": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json new file mode 100644 index 0000000000..5a3b68e179 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.silent": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "flutter.fixture.string": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.unknown": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json new file mode 100644 index 0000000000..001a3f4efa --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json @@ -0,0 +1,8 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {} + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json new file mode 100644 index 0000000000..88766371b0 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json @@ -0,0 +1,49 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.falsy.bool": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.int": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.float": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.string": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.object": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json new file mode 100644 index 0000000000..6e173e5408 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json @@ -0,0 +1,17 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large-int": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json new file mode 100644 index 0000000000..1d718813c5 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json @@ -0,0 +1,411 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large.flag-01": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-02": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-03": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-04": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-05": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-06": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.large.flag-07": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-08": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-09": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-10": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-11": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-12": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-13": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "fixture.large.flag-14": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-15": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-16": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-17": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-18": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-19": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-20": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.021": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.022": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.023": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.024": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.025": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.026": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.027": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.028": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.029": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.030": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.031": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.032": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.033": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.034": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.035": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.036": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.037": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.038": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.039": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.040": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.041": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.042": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.043": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.044": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.045": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.046": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.047": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.048": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.049": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.050": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json new file mode 100644 index 0000000000..5d144285a4 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.reason.targeted": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.reason.default": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "fixture.reason.split": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json new file mode 100644 index 0000000000..393544ac6b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json new file mode 100644 index 0000000000..f44c18a9f4 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json new file mode 100644 index 0000000000..52081f2990 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json new file mode 100644 index 0000000000..fbbedcdf95 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json new file mode 100644 index 0000000000..466c9658df --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json new file mode 100644 index 0000000000..c38c806da6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json new file mode 100644 index 0000000000..f7c0609836 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json new file mode 100644 index 0000000000..caf983bdb3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json new file mode 100644 index 0000000000..e984d6e70e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json new file mode 100644 index 0000000000..3cc336b08e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json @@ -0,0 +1,35 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.config.deep": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json new file mode 100644 index 0000000000..288615a148 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json @@ -0,0 +1,41 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "com.example.feature.enabled": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "my-team--feature-toggle": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "legacy_flag_name_v2": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "mix.ed-key_style.v1": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json new file mode 100644 index 0000000000..e0a504bedf --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json @@ -0,0 +1,51 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.mismatch.bool-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.string-as-int": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.int-as-float": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.float-as-bool": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.object-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json new file mode 100644 index 0000000000..3d88efdbe7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "84a2ac6cb08b06570e965cb98d55a0a83b415e1810ef3f4e103cd8fcde14ad7d": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "2fa983b9b25a656f30c3c1ca3547327d0c91abb8dd5a968cbf737fe8b4def524": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "94501ab4c2a58e69a11081f73e5f61a1f37c9c88fa95e61555a9d14ee1dd4024": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json new file mode 100644 index 0000000000..fed8a7520b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "491646f7138ee482b2ace2d2834ff8ff34b20c8ba8cd329156857ab1be6154a6": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json new file mode 100644 index 0000000000..d77ab75ac2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json @@ -0,0 +1,48 @@ +{ + "data": { + "attributes": { + "flags": { + "8c7c04076517d2c3c0633fda9a6664d4b114cb359cc512dd38c4ef69373032fd": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "36d69e5a88de3b456cd6bfca1070ee06c667c9c94d7de9022616661c71928e50": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c7eac87f0bcefab56284d70561f954ffccd2f88fcd895067ffc94832023d3be": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "6597def4cb332745af4ade37ee996a8958091ccadc58082c2c49c4db66c24e98": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json new file mode 100644 index 0000000000..cba53f8d01 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json @@ -0,0 +1,60 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "657bbcc45263793d67e4f7967fdd257d6e1896cc095d758c520ca3f52beca003": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "abf42b7b192cf3a719bb8ea77c9ff652c82bd4c7aba54561e374efeb2fbbb9b7": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "820dab9a17046e7bf33d080bcb6f44547e1b92c31b36e7d9dcfde96c46bb5d56": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f4d559502705ccaba0a55b0defc37636fe7ed397a477a07389771c07183f139b": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41057f3677d1a6c229d214c630b71e9f499abbb68116d50594561663f6ec09d9": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json new file mode 100644 index 0000000000..f2421d04e3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9176965ed24adf9f9e56f8918f3e4f7d5178ab57ca0fdb3e32266a4203930e49": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "f83c743fa9fc2faf137298f504966877bbe7d23e7cb9380c4b73188b764b0a0c": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1345780c7d221b7c1798057d61113f43466cb92b31ecb05fa74ee4b511d7c482": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json new file mode 100644 index 0000000000..9d945d60c2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json new file mode 100644 index 0000000000..dd29ce980e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json @@ -0,0 +1,54 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fc0d169622e17ab8efff1da7fe0d0ff124de893d2e723fb9a8c632a87f5617e2": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a856abf0d6b1d857471bdfe3f81e7e0c08a1c7da13315ffd303a60e40032c8d": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8e902056a9d96a536e0300af89c407afe670d48fac2ae3ed9c4f0805579011f0": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7aa973f2d2a8f5ff69c54faad155bbad70b0c2733f20d10c0ed170749ce60f3c": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5273f6912bea9ecac5cd7d11674e39b208fa0bc796a3379e5b46138a4d2771a3": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json new file mode 100644 index 0000000000..b5faca838c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "60c239d3d5039428009ffc2a7918ccbd9c6485ee75e7d3130b148103d3e784ae": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json new file mode 100644 index 0000000000..cadee14bfd --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json @@ -0,0 +1,416 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bf9be622ca7517e32cd02c784169beb8e5102892504fdad8384e348d48e36879": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "75dcd247147ab21d09a4fc73051324b2000bd0bf97d82d35ad6d80eaa6d07973": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32681eee3e6ebf4ebaa0b03b91638cbae0920b26239bdea4fa2b7d0bb3a46df3": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "792f76037d724266f9c00e971967bbfe62cba43274b31f652b9768d6381e7ace": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "59bf45e79a60fa4a33f2d4c639ef910982d3d3e29ca2fb9c147fb4977569f35a": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fce91fe1f119ec27d34a9cb77ab5014d49153c8afa8162b282c9c595591ff130": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "086529987ece3ae0319c12332405bbec6e6eb913266ade8c8fb33b2b7b89ab5f": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1d89f79a25e4eae7f080cb86d35af66fd2147336f6c450dcd40d23333f83d888": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0fe82d095e9c1dbefdca57b310ab4c19c80439c87a41b0eef548685914df239d": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "1ac48586eced16b0aff76e17312da68f2c43fd3cf9ca33d76f3e427f93797c4b": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6234a3430646b1df0c9e2bcca80dfe16b05abcca88d58f5f97c19a14a9a320": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "215f50d3c5e5908c31d2bed80414ce26ba68b264ea5baa4ab5b743e1833c1945": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "88c23388c4cec50c57429698b96c975be9f8915f88308fba6bda54acc6d01998": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "b66bc4f899fa5ad230fa050a3ba35ac5c5cda22bebecbb44e6c63afddbaf7b5b": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "83ab766d8522b069512a37e2a5bd8bd8f941fbeef306eab09f6247e8db3b725b": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "db1c7d8f6ac65f0e493bb4c3a87f153ec6cac18bd0c984b20ff09619ff79cce9": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "4d23aa03aec589f62d46d15642b79f6e3c87e3aefa5b49cd319304f4a013ac95": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "023b47247033e8bea815ff0cc16255bebdff082b3cfb1eeb4a9b55b3004276e1": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "0b2b70d8c98c0731692191e78b527512e548190d97d710d7f0fc8f19cd05913d": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c1796f3cb195313519a837ead5d54e083195b6206e189bbdd0220561b4ca766": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f73dce8e960f6d67023c407ac490ce3605b279288b56f78031bfed36f0014935": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "c078d7577eff8d7a6fc8963d0bd16fbb172c3a4a3aeb9e714c75a2574d7967e1": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6326ec0ed3c2465e7cc807f46c743037e7095d3f8e9d1d56dbaea76b2b00452b": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "3d3d982d7910fda9264321566138165779600cddbf42d215d689080b617b3fd0": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "5a80d2d736721d87db810627614f4f6b127ee0a332f5fdea8550a78e3ec4e699": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "86edc8f5b58892c55f5fde77e66cdc7b6d0a9b071e2677d8fa16dbb8f409f380": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "cc6b19cce7d683983134195f55a0c6bf3a5f844944e283cda1c58485b5a2dd2a": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "4c2f7e12bea6b91c5d5c9862de0b24418f6fde074eb5e017c93293cfabf60060": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5a48e1a5c7c8e185b62cd259351f4abe443771f07292eefbd49f0599881e35ce": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "cf6321f08df13c06652debb11c4cb9ea67e215e5a364d845f30d865554876b4d": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "1660da21238865ae03e7c5c4959d02a34bc210101d7608f7221f97f254b801d0": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "34529807d59d7bf904c56e17e112ed555687ee05a332a9ef957d7f029870ed57": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "f8fcf4b3a145194a19daabe0521e6cfea4bac6b20a6fd729f4c8c3ad6ac3e6f6": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "210a549229e78c66c588dffc37d99918e7968a04876fa87cd1b719a5dfe1edf2": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ffd5bbbf90dd7c225ddc95b0c1134cee255c5d1ee7f7889bf2c239b99dd43db4": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "929200b0fd1248b310ac0f0785a5a3ee3b1aa40cbd4cc7d7ee7c141bd57c839b": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "4aefe68d39be982605014ac90b746de9be85b4bea79cacc26459cdc06b96f48f": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ba21a1d31e1f9074d322885263107157affa2265582bf6a05f84441b8c118eb": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "77581b9776c4d5afa1919a1e0c758067d07c3968afa2201870cea6dae6579aa3": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "03461ca38265adbdcede804d0ba7a544dce9c4de6296eb32d8a6683096a9b1ce": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "92325ac72dff844afbdc8a8a8ca939b673fbb452cd67d7b970990c58596baf57": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "934025c0b96e851d348e09166362375a53e2776f595da60d4e2fde30f1503baf": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "f6d270502e6f9fc1f4cfa7fbcc78e2b3377d9fb480f23e864cd4be0c96ec3467": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8bb8e9b84c7ab5397d4012c8d58cb797a8fac7562c86ede747d91f5971bfef10": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "81be2644335d094121dc49dafd671c0c212980253962dd2f24986736cdaf9583": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "b4fa3c55cb8c05baba5ca43eac146e6036b0028e2fb07eafd6819d3935c9e4bc": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a56d4714d20368ac54afa5225b1dec452d9bc102f604fd54bd338e4b693bda38": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "e5b919481d265c3573d94171004324cff98c4e223efd7b568b028ffcd4314a6e": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "370b0fcd16be8003ef1d1e1f39e7e19b3698f17a24957ed654420be0f81eddc5": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0721adc36960473f00919092dc06adc2988d409a72f5244bc47a4b11de285660": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json new file mode 100644 index 0000000000..83d8cda9af --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "19f9605f249b328efe6542c0a39acdaa7a0b39c61daccb3883f2cf3c5705e9a3": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e433cf607656e33013abf122d4f7576389f0acf3ea88f085fd069bd839390a4f": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "212fded283b3498c3742391c9db4843ff32aad02ded7646c68dd14bc008f3f03": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json new file mode 100644 index 0000000000..fae798790f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "35dd7544a6b9a875c5074a893aa3d5c95f015b6cff7f07f30390bc90b939839e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "54b3e4e77e354d4e049991350fe68ce1a90ba4e874933a9e269d5b99cb3ad115": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6cec42afe677ad01472e4754a611106037ddb8e786570bf9a730630b8d2f7f30": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8de4314e7ce11a9f64a3490e59968a1130f56b15c39276bdff9ebc344402101d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4182dc078ece15bc1c45cabf6e2ab2ad0b7acaf7f42eaa3ad63fe3956beb1db7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ec7fd610e215fd5d177ffa68bec26d50a871bc1cf0114636a734e1660bd6411e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "53d34567f5337f1104d86a9e821df4511fa74a9cc03516f426c9ab4c7af0ac52": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3e69e701cc02087f7897b6f4832e8be0be9ca155876f8cb95597e6ad32b1e2ab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd1a3e85e06626dbb1c56432a8dea7a0128284f1d781013835a2564e70f87d98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a212f6d288564e940e009835514eaad55a3d62caada53dd1fdda1b2475efed0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f8dc9d43242ec89a4db2212c3a70ee9c938b0cb36afac45b2000cd7c77bf7609": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1e20092aa2b67b3e8ed9d5533a2260ff177207d9b86fac2151864b2d7af492e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "45f0d04656e98eb32b8221714cf169d57479d2be4af6b96c5ffe0f9fd589ad0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "680bb5138700a197f7896709b20d6a451854dfaf7fd2016374154dc214101f96": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5f0e609737393c76014f125cfe1d8b685065b3f8bb2833e393f4de4bb3f271fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6de38d27b71396e8169bdf012bf53a9f6112b4f9533e492d1d0c372aa5b2b89a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e68a8c78bf6d545e6c949e88209611b6615ddeea9817440de9fd3fb7a289c335": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6d6386fa18188d57ad46d118b937d77a1a8d814e3ff723871452b9d5f6abd03": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9d28b9ca14aed8cd8edab7144dc09ff50ff608f1676df0c92be1a225fc4c95b4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e026139b2c1ef067fcd194745a316b948ca17d4224a2c6ef0f54e42739a6f9e5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9ad722f4aa0e6cbe77da0e6406a3f40ac0dbe3f556a524d1d504115896235fed": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56cd8c449d33e0a19ca9b0d300b40bd994dfd1f4f3b16d093e453d8a97a3645f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json new file mode 100644 index 0000000000..8ec4120f92 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "dba4f4d05c42d94f2f0fe7fb4c5622432cbace9983c4f2bcd81dba39a6327250": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c053d531267e94db3094b514ac132333832cf45cc102899e23a317cfd1b86c7c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6b41d8abf103fc2c0903beb89ea4916203e65e34f56f229bd4bfd30fca2b44": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2cd758e8f6b3d6069426eaf4fe83cad350062d6f3cf9fdff78af6814151ff35f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "be1e08347c503a3116b8872a98f762ce80f8b2d5d9712855f553e9a8e41a0e24": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "439b8c652a5b3d6bb6a8b5601f86c09f38f1ca344dfa0cd10904c41d3a0089ba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52c0be60636eb11b987e1729b894a63f02463a408660d632004cd68b66c17966": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32ac57434b06ca630219caa81b6d6c685138920a8a9d1e6205055cff023f3179": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "21bae751d95202679767297ac5a668ff2588c260e0c1d93e0ddb494a3c6c44d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ceb204983395689386573ade9d3496ac4c07fde14fd7dbc965b51654af834df1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2b69fdede340468e31ce79ee511016e1f3f15fceaf2228fb6bde3c034cb14d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0a1f9c3aedb3a5721f4215c26cd403a7bca6d5fcba088d6ff8c5eb58a4168903": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cf564971ee029d99592fad8d056f2d1cb13ff91e3e283dc2c16b3a644db90734": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "482ab354d08783942a1cc1613d261773481b27647304272c9a895b3f82aef81b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8ae623d95ec069f4263033a5c3c63cc41c8369a4ada5c2df1e72feb3472b3298": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ee628175449c6db9aeeb2b77f155f2af7f566ae1209ed32f564b4e74d37ffbe": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1aaaeb79098a7feede64e211e53f192295e0d97db557223f5945bd869a9ec82c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1a9a9de8e21c848b2bfec22d14c770d8a3a176dd1ce39955a5b165bf1f851590": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2dca5f665b034d94acdc589d0b6646e583561b3f05aeba0a0b6399297c8ccf1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a014f0066d631022d1c7628bd1b990291f772710900955e0aab48543db7c3b2a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "05ab2bff8b65224aff72672e380d66e022ff5f0c1b76e3231aaf172f6c82d467": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a10340fc40240dde6ba540065e6526b8c66b6fd78d116451b42201a9ba4c665d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json new file mode 100644 index 0000000000..b84498290c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "4151b6612247033495b8370b980c847c1efe5dc3ddb008a57aaf9e8afb97694d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c3c464e47d3e87d433d2a0b6a5ea9e9cea9ffb65c31a7022fc4be4c51a3e3663": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d9adf2748e7c2bb330f6b321204a4ab3d61aa555dbeaddcb3d79fb20383e3840": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a3e931eb60a9b70284b948912691bbf4e1f757d1f52c90d1623413a4ed05eaf7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56a5f99336ac7daaa327d074e3c1cf1389f3b0e198d8b6b0dc4a0773f37466b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "996e91f621b0091b21aaceea06330f3bcc8a4639939a0b3cafd38cdd2d732bde": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5b95fc3592f62934e978d8f18e998120d1012da96a4ae3c7a3bc832442914bce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fb8ab09c443a1fef7f78aa00069193131bdddd5f754c69837da77dde8bd53ab4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "13bc44aee0e07075a5f61e06bea48f9c7d7cfe240bbcf45d9922127c5fcbf57a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2b8ee396fbeba43a943135d2ea6962f4eee41f3af704609313b6241bf4911f20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9dec15ccf9649edff360ec3a223801aa6f804087a939b1d14b686a7fe34a0cbc": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1aedbf12de210a49f962e5e8db09a065c2b328a7d2c48ce4c6e919325b9c0c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4a989c369519e210c4857cd50fd525e571cc89b7da322276ba05487c15b4849f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "313de806bdcd55e96b13d12c28260bc5238057d4ed2a8ecc0294f8c7e234fa01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1f5409b64eb99b424819230ddfb333b5f399a911840ab6b36f0a0938a438af25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "61f7deca99e29b35ab18cdf82001dcf64ea3b04caaa9fc751d835f1c70d25fd2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "06503c386b2c84e0299e7a692baecdecf2b19698b90f13b89df7d4e309d43870": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6d9de4417c04ad9908dc846b1a9ae5c6c2fe214e7aca7e2dd3480d82abd46a5f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b553f209415af2ef3dd59e7f7383e270e5090b9811e319c39645a51d58ec26c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4ccf6d4d18107b748ae8ec301e66adb190f15bd77e3bf23246d8994787c8a40c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9de54af153484d9db86d593c9a0c9443f7c5a88881964452b69b7f2d8a878c01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52b56cf56113aee71339a1256252ae52741aa5e666d2a5ca3ae3ad194cbb83f5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json new file mode 100644 index 0000000000..c919307c75 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json new file mode 100644 index 0000000000..17a5312cc3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json new file mode 100644 index 0000000000..a94212a030 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json new file mode 100644 index 0000000000..556d44392d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6711bb1e8b093b846ea439322c667bce9ca0568b0f0d4320e05c236c9ead11ce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "dd28ccc7dc267c0927246a422010adbf6e81bb5b0e8fa6a3f9c5c999dcfa8cac": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9662b5e2c026477337a6a1b952ca1da98116ea3393c14ad81bf3b0e6f7705eae": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6a126c45f099dc1465fb20627b24f64632aae36028757b78b361873c454d566e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a331b89b73d723922e64da680ad3048d381db62fd6f9139eb9e57ea43569156": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e152b1e1e80ca9edabd8cfd196b0084ee4728b8eb6b85c1de743705b8ec3f1c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "968e09a8af812ed8d7fc18b21f59d5b7623d5e1ae6fc8f5a141443f026b11aa7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b4b714689b3211e62d2fbde91099071f193e00c955f5a96a6ffa087b2e6bfa98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a978ec7a2487b240242a05119affc348f41d0d9fbdce334665c4090c2f4ab1fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3860419b6dc9dc69573f100ccbf51c008e45a8a9b6a62243ff02b869f4e86b47": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a303e6fa7efedf997c87edfc2d79872c95d3e1a6c5310ccfe65d0826f700ab55": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41a0cf77c6270e0e6f3941ab2fa80f712b1c916d13793ddd36a99363c07ad885": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json new file mode 100644 index 0000000000..31926b49cc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6f6faa00700939aa0c395e059d8dea1cc48b2c5a20eb2ef5bc7abfeb551e2d6a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6418f8b5f4df9814bfd25f30a8ef6865929075706f260ca4f48f5334f5541c89": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0f2ba6c096112e6b701607e345fc3a06a2a0d93710715cea905546b69b536ec0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ddd11d6e8fd3dfc9734f307630745484bb411a3278c36ad0dd7c681f91edcde5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1108b8a690b2ca3dd55e74294ed8bfdf1496b88ed4b3cb87abf27199c6b819e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "01ede755b925df7f56ae33a0bcc4d822a0495b602eea740c5b8570c025527afa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fc310ab0e0bd16d08c9b769db925a177fa89b3f539d75b01ef8dd319c427fccf": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ad9ca46d7714846bd2a813e74d2af97e1cd25df3b0242f1575db2083f6f9d8b1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "35eb2ca6e144c429db174c281e9e55d2b9b4e9d819a159203bbdf577354e4033": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "27bc83275f0f9a5e871d2e4c5af47829d9f7b72c3e5f1f224fbafe3cef986c81": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "19415991350c00c660f9d74c206db9ec7d9fbde7c8a8cbdcfd6775911e40be6b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a556037a239ae8b7c3789f59013e015ebf29686de47bce8aa1bb17f99569a4e8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json new file mode 100644 index 0000000000..743c0c21bd --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5a1d7afff3fd522b50e8891f7af14267bdd253a1455ed12bc92188f78e12033d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cbc15a556f7e611fa26f06f6a6334ee91ad5d6a344496452557db41ed992bc25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f1c431305b153a3030fadebc0057e634b6162cd6acda9744a763082b1197c740": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7b3cbab857c42d32035705aa891dd7a2fbadcbe1fc0f1e28f3611076a6aa7945": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "acdfe2ec44b3eb617b86da9a49166e3abecbc85c00951e548516cfd748056403": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3af8ba8e93c58c248ed0d943b8bfbc630236bbbeca96be121efc46ed8da4b9b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "efb49302883b084e49d51cb2f41fd795224a571d182536e203f6ab28e6648b77": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c38a503ec9a1c032f0a073e23d6d0ede2eccf245aac33f9db02a5374728c810e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "499aef4e9d9d7df120a79816e9406b5925664e851fd638a129f525588e8a9385": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b004cb3fb7dbb1846ba17f461b1a0931cd52d9ffde2c5419d51c0b47e42baf99": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d858ab64de6a97983f95792030011b2dd635938aaefd90fee12b37ef67e9dac3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ece2688265c5c214d2904619cf0294042830dc10d72dae32e50050c7cded8c3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json new file mode 100644 index 0000000000..09caa7005a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json @@ -0,0 +1,40 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "a75e90efc57ed0e56bd11c3a9b17a2623dcc2a412fa52f3ffa742139fa6b93af": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json new file mode 100644 index 0000000000..5d48cc3da0 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json @@ -0,0 +1,46 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5ed289266d5a488851241b3122aab121698b75a75da9fb687c671d606a099d99": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a5bcd95f28f8cf915795582c1edd5b263d2f42cd8a513eb6d69aeef26c39ead0": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e4fb9f7c538eac04c76c225b60d43a48b5f514a112a148d95d25754ad14cc8a8": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "dfd73ab3f42a47bd441de225f613432120e3eb38a6f00a21cb19a45a9de23c70": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json new file mode 100644 index 0000000000..0180dd326a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json @@ -0,0 +1,56 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "937d54653333211a63876dd0e042edb2ef108105fb918420af28ff03f620a982": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ed1838c062426321f2125dd154000611b46339c11bbbab4228df3fad88007f6c": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7ba0090b5e66bc7319950479a1e905c26fedd82e605530cdb4eb0adb4c7d5515": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1fa87b21c5821755d962ce404e18a06c5ae2656dd33d4e915e80e425fc77c190": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9a172110c6d75715ead4e47f3063d7ab4d92141de6f83681141130da17f9b743": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json new file mode 100644 index 0000000000..de793f1519 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json @@ -0,0 +1,78 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "adca75d2141c51b0c0f084c1058edfb8e6e763f91aaf54ca06326d9847bd9586": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1a5f851cc82a3fdf03a461d72a2241584dcf455df1d384e02a937901b3bc80b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3bfa8c3c17c1b61035b98ecf14007cdf5cba5ce61a48e8cfb65f8106541892d3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1e8b7ec5e8028a1ec96b38dd37f6ccea041c0a90358904af40ac5810c23c8765": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "94b611e0d3b26b52f6ad66013d1c72f8a92ea109390049759e75d3c8d3aa4dab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bac134d201be5e7f28fc7019248f0809c2a013b137e866446ee71add2bc344c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "072d985b427f536ad0a11b2d4c5e0f7e6f0ff6d23e779e082f75599ce3fe3eba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json new file mode 100644 index 0000000000..5ce3250c47 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": false, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json new file mode 100644 index 0000000000..0f57503574 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json @@ -0,0 +1,9 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json new file mode 100644 index 0000000000..eac48a73a5 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json @@ -0,0 +1,12 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json new file mode 100644 index 0000000000..0acbfbe83c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json @@ -0,0 +1,12 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json new file mode 100644 index 0000000000..37c42110ad --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json new file mode 100644 index 0000000000..524af52021 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json new file mode 100644 index 0000000000..aef8d0fe1a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json @@ -0,0 +1,10 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": null + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json new file mode 100644 index 0000000000..cbe2dacb61 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json @@ -0,0 +1,9 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": null + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json new file mode 100644 index 0000000000..effbc83c55 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": null + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json new file mode 100644 index 0000000000..0b52c3be42 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flag": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json new file mode 100644 index 0000000000..e5d562c805 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json new file mode 100644 index 0000000000..a3c39c5dc6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json new file mode 100644 index 0000000000..e04be21bef --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": "true", + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json new file mode 100644 index 0000000000..f1bd297feb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "unknown", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json new file mode 100644 index 0000000000..9109f07782 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json new file mode 100644 index 0000000000..0704f7c6eb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090A0B0C0D0E0F" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json new file mode 100644 index 0000000000..53f58a2cdc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json @@ -0,0 +1,78 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb0951b2113f60f04611546a5ec4c7e7e12cabc968eff59362aaeecc6d683fa9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd79ba8d71066ff40da24e6ed335f7669113d17fef540cde96044c03411af1c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a2d89601f57c605c641d06d9bbe736d7e7873744ada2beecff36c051d67c1114": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "65675bf54578b33d06926dc6dbb43abe39d12458b7f7202d72d22d3cf294e63d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2d84852642a347b30811707354eed8d3ea564967f46d549be94b4c58e53c78c8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd982551b4c9c73d5a4d857288e0889e2641a71ea2be43767284d29348399139": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f43ee46b2cdf25f71c542e857876b885b68295fe1845d7f3ea83f32f334c276c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "ffffffffffffffffffffffffffffffff" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json new file mode 100644 index 0000000000..0c9c4991f7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/schema.json b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json index 5aac5dce2f..98d67165ef 100644 --- a/reliability/single-fit/flags/src/test/resources/precomputed/schema.json +++ b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json @@ -385,6 +385,22 @@ "properties": { "evaluations": { "maxItems": 0 + }, + "expectations": { + "required": [ + "noUnmatchedEvents" + ], + "properties": { + "noUnmatchedEvents": { + "const": true + }, + "exposures": { + "maxItems": 0 + }, + "evaluationEvents": { + "maxItems": 0 + } + } } } }, From 96b2bf6f228d5646312f7945f67e50267326b623 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Nov 2026 18:51:46 +0000 Subject: [PATCH 43/44] FFL-3568: Align numeric API documentation with the shared contract --- features/dd-sdk-android-flags/README.md | 2 +- .../com/datadog/android/flags/internal/FlagValueConverter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index 2113ef6eaa..bf6be3c354 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -290,7 +290,7 @@ val maxRetries = client.resolveIntValue("max-retry-count", 3) val discountPercentage = client.resolveDoubleValue("discount-rate", 0.0) ``` -Any numeric flag can be read with either method. `resolveIntValue` returns the value only when it is a whole number within `Int` range, such as `5` or `5.0`. Otherwise, it returns the default value: `resolve()`, reports `TYPE_MISMATCH` when the value is fractional, such as `1.2`, or outside `Int` range, such as `2147483648`, and `PARSE_ERROR` when the value can't be parsed. To bound parsing work, a value longer than 64 characters is not parsed unless it is a plain integer within `Int` range, so it reports `PARSE_ERROR`. `resolveDoubleValue` returns a fractional value as the nearest `Double`, such as `1.2`. It returns a whole value only when the `Double` has exactly that value, or prints as that value, such as `1e300`. Otherwise, `resolve()` reports `TYPE_MISMATCH`, such as for `9007199254740993`. `NaN` and `Infinity` report `PARSE_ERROR`. The same applies to a value longer than 64 characters whose `Double` is 2^53 or more in magnitude. +Any numeric flag can be read with either method. `resolveIntValue` accepts whole original values within `Int` range, such as `5` or `5.0`. Fractional or out-of-range values return the default with `TYPE_MISMATCH`. `resolveDoubleValue` rounds fractional values to the nearest finite `Double`, such as `1.20000000000000001` to `1.2`; whole values must be represented exactly. `9007199254740993` and `1e300` therefore return `TYPE_MISMATCH`, as do overflow and nonzero underflow. Malformed numeric text, including `NaN` and `Infinity`, reports `PARSE_ERROR`. Decimal parsing is bounded to 64 characters per value; longer values report `PARSE_ERROR`. These rules apply to `integer`, `float`, and `number` flags, including cached assignments. **Structured flags** ```kotlin diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt index b81f7dfa6b..fdb0cc3181 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt @@ -32,7 +32,7 @@ internal object FlagValueConverter { private val INT_MIN = BigDecimal(Int.MIN_VALUE) private val INT_MAX = BigDecimal(Int.MAX_VALUE) - // The longest decimal value that Int reads, and Double reads at or above 2^53, parse with BigDecimal. + // The longest decimal value that numeric reads parse with BigDecimal. // This bounds the BigDecimal work on each read. private const val MAX_DECIMAL_LENGTH = 64 From 878dd3fb00771a5cd2fbb6f5840324aa45eba473 Mon Sep 17 00:00:00 2001 From: Tyler Potter Date: Thu, 5 Nov 2026 10:07:51 -0600 Subject: [PATCH 44/44] Test legacy parsing of dependent flag errors Verify legacy Android accepts encrypted dependency metadata and a sibling error map while preserving otherwise valid assignments. Environment: Datadog workspace Co-Authored-By: Codex GPT-5 --- .../flags/integration/FlagsEvaluationTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt index 5af4a80210..6c62c70e5b 100644 --- a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt +++ b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt @@ -30,6 +30,7 @@ import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.AutoClose import org.junit.jupiter.api.BeforeEach @@ -150,6 +151,51 @@ class FlagsEvaluationTest { .containsEntry(fakeStructureKey, fakeStructureValue) } + @Test + fun `M ignore dependent fields W resolve() {legacy client}`( + @BoolForgery fakeValue: Boolean + ) { + // Given + val response = JSONObject( + precomputedAssignmentsBody(mapOf(fakeFlagKey to assignment("boolean", fakeValue))) + ) + val attributes = response.getJSONObject("data").getJSONObject("attributes") + val dependencyEvaluation = JSONObject() + .put("flagKey", ENCRYPTED_DEPENDENCY_KEY) + .put("isPresentInAssignment", true) + attributes.getJSONObject("flags").getJSONObject(fakeFlagKey) + .put("dependencyEvaluations", JSONArray().put(dependencyEvaluation)) + attributes.put( + "flagEvaluationErrors", + JSONObject().put( + "failed-flag", + JSONObject() + .put("reason", "ERROR") + .put("errorCode", "GENERAL") + .put("dependencyEvaluations", JSONArray().put(dependencyEvaluation)) + ) + ) + stubSdkCore.stubNetworkResponses { httpResponse(it, body = response.toString()) } + val client = enableFlagsAndBuildClient() + client.setEvaluationContext(EvaluationContext(fakeTargetingKey)) + + // When + val details = client.resolve(fakeFlagKey, !fakeValue) + stubSdkCore.stopFeatures() + + // Then + assertThat(details.value).isEqualTo(fakeValue) + assertThat(details.errorCode).isNull() + val exposureEvents = stubSdkCore.eventsWritten(Feature.FLAGS_FEATURE_NAME) + val evaluationEvents = stubSdkCore.eventsWritten(Feature.FLAGS_EVALUATIONS_FEATURE_NAME) + assertThat(exposureEvents).hasSize(1) + assertThat(evaluationEvents).hasSize(1) + assertThat(JSONObject(exposureEvents.single().eventData).getJSONObject("flag").getString("key")) + .isEqualTo(fakeFlagKey) + assertThat(JSONObject(evaluationEvents.single().eventData).getJSONObject("flag").getString("key")) + .isEqualTo(fakeFlagKey) + } + @Test fun `M provide resolution details W resolve()`( @BoolForgery fakeValue: Boolean, @@ -480,5 +526,9 @@ class FlagsEvaluationTest { ) } + private companion object { + const val ENCRYPTED_DEPENDENCY_KEY = "Eb_cST-0-FZKdvcpKLHbDDG_CX2hK4Xfrw7h5o-47Gp6GyFntOGTZO3wmLg" + } + // endregion }