From cd472e2d81b47b927c922bfbec019db25e2ad6a8 Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 23 Apr 2026 16:03:25 -0600 Subject: [PATCH] Fix evaluation reason logic: SPLIT precedence (ADR-004), DEFAULT for catch-all allocs, date window precludes STATIC (ADR-003) --- .../trace/api/openfeature/DDEvaluator.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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 a0a85f6a9ab..a75fbf5788b 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 @@ -386,10 +386,7 @@ private static ProviderEvaluation resolveVariant( final ProviderEvaluation result = ProviderEvaluation.builder() .value(mappedValue) - .reason( - !isEmpty(allocation.rules) - ? Reason.TARGETING_MATCH.name() - : !isEmpty(split.shards) ? Reason.SPLIT.name() : Reason.STATIC.name()) + .reason(resolveReason(allocation, split, flag)) .variant(variant.key) .flagMetadata(metadataBuilder.build()) .build(); @@ -400,6 +397,25 @@ private static ProviderEvaluation resolveVariant( return result; } + private static String resolveReason( + final Allocation allocation, final Split split, final Flag flag) { + // ADR-004: SPLIT overrides TARGETING_MATCH when both rules and shard contributed + if (!isEmpty(allocation.rules) && !isEmpty(split.shards)) { + return Reason.SPLIT.name(); + } + if (!isEmpty(allocation.rules)) { + return Reason.TARGETING_MATCH.name(); + } + if (!isEmpty(split.shards)) { + return Reason.SPLIT.name(); + } + // No rules, no shards (vacuous split). STATIC only when this is the sole allocation + // with no date-window constraints (ADR-003: time-gated result is not permanently stable). + final boolean hasDateWindow = allocation.startAt != null || allocation.endAt != null; + final boolean isSoleStaticAlloc = flag.allocations.size() == 1 && !hasDateWindow; + return isSoleStaticAlloc ? Reason.STATIC.name() : Reason.DEFAULT.name(); + } + private static Object resolveAttribute(final String name, final EvaluationContext context) { // Special handling for "id" attribute: if not explicitly provided, use targeting key if ("id".equals(name) && !context.keySet().contains(name)) {