diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index 44cd1fc9efe..c33c2ebfb45 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -14,6 +14,7 @@ import datadog.trace.api.featureflag.ufc.v1.Shard; import datadog.trace.api.featureflag.ufc.v1.ShardRange; import datadog.trace.api.featureflag.ufc.v1.Split; +import datadog.trace.api.featureflag.ufc.v1.ValueType; import datadog.trace.api.featureflag.ufc.v1.Variant; import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; @@ -343,6 +344,31 @@ private static ProviderEvaluation resolveVariant( .build(); } + if (!isTypeCompatible(target, flag.variationType)) { + return error( + defaultValue, + ErrorCode.TYPE_MISMATCH, + "Requested type " + + target.getSimpleName() + + " does not match flag variationType " + + flag.variationType.name()); + } + + final T mappedValue; + try { + mappedValue = mapValue(target, variant.value); + } catch (final NumberFormatException e) { + return error( + defaultValue, + ErrorCode.PARSE_ERROR, + "Variant '" + + variant.key + + "' value does not match declared type " + + flag.variationType.name() + + ": " + + e.getMessage()); + } + final ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder() .addString("flagKey", flag.key) @@ -350,7 +376,7 @@ private static ProviderEvaluation resolveVariant( .addString("allocationKey", allocation.key); final ProviderEvaluation result = ProviderEvaluation.builder() - .value(mapValue(target, variant.value)) + .value(mappedValue) .reason(Reason.TARGETING_MATCH.name()) .variant(variant.key) .flagMetadata(metadataBuilder.build()) @@ -371,6 +397,26 @@ private static Object resolveAttribute(final String name, final EvaluationContex return context.convertValue(resolved); } + private static boolean isTypeCompatible(final Class target, final ValueType variationType) { + if (variationType == null) { + return true; // No type info — allow any + } + switch (variationType) { + case BOOLEAN: + return target == Boolean.class; + case STRING: + return target == String.class; + case INTEGER: + return target == Integer.class; + case NUMERIC: + return target == Double.class; + case JSON: + return target == Value.class; + default: + return true; // Unknown types pass through — mapValue errors caught as GENERAL + } + } + @SuppressWarnings("unchecked") static T mapValue(final Class target, final Object value) { if (value == null) { diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java index 3df51d1bde3..1df5082e142 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java @@ -288,10 +288,37 @@ private static List> evaluateTestCases() { new Result<>("default") // Result depends on shard calculation - either match or default .reason(TARGETING_MATCH.name(), DEFAULT.name())), + // Type mismatch: STRING flag evaluated as Integer new TestCase<>(0) .flag("string-number-flag") .targetingKey("user-123") - .result(new Result<>(123).reason(TARGETING_MATCH.name()).variant("string-num")), + .result(new Result<>(0).reason(ERROR.name()).errorCode(ErrorCode.TYPE_MISMATCH)), + // Type mismatch: STRING flag evaluated as Boolean + new TestCase<>(false) + .flag("simple-string") + .targetingKey("user-123") + .result(new Result<>(false).reason(ERROR.name()).errorCode(ErrorCode.TYPE_MISMATCH)), + // Type mismatch: BOOLEAN flag evaluated as String + new TestCase<>("default") + .flag("boolean-flag") + .targetingKey("user-123") + .result( + new Result<>("default").reason(ERROR.name()).errorCode(ErrorCode.TYPE_MISMATCH)), + // Type mismatch: NUMERIC flag evaluated as Integer + new TestCase<>(0) + .flag("double-flag") + .targetingKey("user-123") + .result(new Result<>(0).reason(ERROR.name()).errorCode(ErrorCode.TYPE_MISMATCH)), + // Type mismatch: INTEGER flag evaluated as Double + new TestCase<>(0.0) + .flag("integer-flag") + .targetingKey("user-123") + .result(new Result<>(0.0).reason(ERROR.name()).errorCode(ErrorCode.TYPE_MISMATCH)), + // Variant value type mismatch: INTEGER flag with string variant value + new TestCase<>(0) + .flag("integer-string-variant-flag") + .targetingKey("user-123") + .result(new Result<>(0).reason(ERROR.name()).errorCode(ErrorCode.PARSE_ERROR)), new TestCase<>("default") .flag("broken-flag") .targetingKey("user-123") @@ -551,6 +578,9 @@ private ServerConfiguration createTestConfiguration() { flags.put("not-one-of-false-flag", createNotOneOfFalseFlag()); flags.put("null-context-values-flag", createNullContextValuesFlag()); flags.put("country-rule-flag", createCountryRuleFlag()); + flags.put( + "integer-string-variant-flag", + createSimpleFlag("integer-string-variant-flag", ValueType.INTEGER, "not-a-number", "bad")); return new ServerConfiguration(null, null, null, flags); }