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 8b04534b7d1..44cd1fc9efe 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 @@ -87,10 +87,6 @@ public ProviderEvaluation evaluate( return error(defaultValue, ErrorCode.INVALID_CONTEXT); } - if (context.getTargetingKey() == null) { - return error(defaultValue, ErrorCode.TARGETING_KEY_MISSING); - } - final Flag flag = config.flags.get(key); if (flag == null) { return error(defaultValue, ErrorCode.FLAG_NOT_FOUND); @@ -127,6 +123,9 @@ public ProviderEvaluation evaluate( return resolveVariant( target, key, defaultValue, flag, split.variationKey, allocation, context); } else { + if (targetingKey == null) { + return error(defaultValue, ErrorCode.TARGETING_KEY_MISSING); + } // To match a split, subject must match ALL underlying shards boolean allShardsMatch = true; for (final Shard shard : split.shards) { 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 4b723fac7fb..3df51d1bde3 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 @@ -210,9 +210,23 @@ public void testFlattening( private static List> evaluateTestCases() { return Arrays.asList( + // OF spec 3.1.1: targeting key is optional; static flags must evaluate successfully without + // it new TestCase<>("default") .flag("simple-string") + // no .targetingKey() -- null by default + .result(new Result<>("test-value").reason(TARGETING_MATCH.name()).variant("on")), + // Null targeting key on sharded flag must return TARGETING_KEY_MISSING + new TestCase<>("default") + .flag("shard-flag") + // no .targetingKey() -- null .result(new Result<>("default").reason(ERROR.name()).errorCode(TARGETING_KEY_MISSING)), + // Null targeting key on rule-only flag (matching on non-id attribute) must succeed + new TestCase<>("default") + .flag("country-rule-flag") + // no .targetingKey() -- null + .context("country", "US") + .result(new Result<>("us-value").reason(TARGETING_MATCH.name()).variant("us")), // OF.7: Empty string is a valid targeting key - evaluation should proceed as normal new TestCase<>("default") .flag("simple-string") @@ -536,6 +550,7 @@ private ServerConfiguration createTestConfiguration() { flags.put("not-matches-false-flag", createNotMatchesFalseFlag()); flags.put("not-one-of-false-flag", createNotOneOfFalseFlag()); flags.put("null-context-values-flag", createNullContextValuesFlag()); + flags.put("country-rule-flag", createCountryRuleFlag()); return new ServerConfiguration(null, null, null, flags); } @@ -1194,6 +1209,33 @@ private Flag createNullContextValuesFlag() { "null-context-values-flag", true, ValueType.STRING, variants, singletonList(allocation)); } + private Flag createCountryRuleFlag() { + final Map variants = new HashMap<>(); + variants.put("us", new Variant("us", "us-value")); + variants.put("global", new Variant("global", "global-value")); + + // Rule: country ONE_OF ["US"] -> us (no shards, so null targeting key is fine) + final List usCountries = singletonList("US"); + final List usConditions = + singletonList(new ConditionConfiguration(ConditionOperator.ONE_OF, "country", usCountries)); + final List usRules = singletonList(new Rule(usConditions)); + final List usSplits = singletonList(new Split(emptyList(), "us", null)); + final Allocation usAllocation = + new Allocation("us-alloc", usRules, null, null, usSplits, false); + + // Fallback allocation (no rules, no shards) + final List globalSplits = singletonList(new Split(emptyList(), "global", null)); + final Allocation globalAllocation = + new Allocation("global-alloc", null, null, null, globalSplits, false); + + return new Flag( + "country-rule-flag", + true, + ValueType.STRING, + variants, + asList(usAllocation, globalAllocation)); + } + private static Map mapOf(final Object... props) { final Map result = new HashMap<>(props.length << 1); int index = 0;