diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 6a206c0..d465c43 100644 --- a/src/main/java/cloud/eppo/BaseEppoClient.java +++ b/src/main/java/cloud/eppo/BaseEppoClient.java @@ -223,11 +223,20 @@ protected EppoValue getTypedAssignment( return defaultValue; } - FlagEvaluationResult evaluationResult = - FlagEvaluator.evaluateFlag( - flag, flagKey, subjectKey, subjectAttributes, config.isConfigObfuscated()); + // Evaluate flag with details + DetailedFlagEvaluationResult detailedResult = + FlagEvaluator.evaluateFlagWithDetails( + flag, + flagKey, + subjectKey, + subjectAttributes, + config.isConfigObfuscated(), + config.getEnvironmentName(), + config.getConfigFetchedAt(), + config.getConfigPublishedAt()); + EppoValue assignedValue = - evaluationResult.getVariation() != null ? evaluationResult.getVariation().getValue() : null; + detailedResult.getVariation() != null ? detailedResult.getVariation().getValue() : null; if (assignedValue != null && !valueTypeMatchesExpected(expectedType, assignedValue)) { log.warn( @@ -238,17 +247,17 @@ protected EppoValue getTypedAssignment( return defaultValue; } - if (assignedValue != null && assignmentLogger != null && evaluationResult.doLog()) { + if (assignedValue != null && assignmentLogger != null && detailedResult.doLog()) { try { - String allocationKey = evaluationResult.getAllocationKey(); + String allocationKey = detailedResult.getAllocationKey(); String experimentKey = flagKey + '-' + allocationKey; // Our experiment key is derived by hyphenating the flag key and // allocation key - String variationKey = evaluationResult.getVariation().getKey(); - Map extraLogging = evaluationResult.getExtraLogging(); + String variationKey = detailedResult.getVariation().getKey(); + Map extraLogging = detailedResult.getExtraLogging(); Map metaData = buildLogMetaData(config.isConfigObfuscated()); Assignment assignment = diff --git a/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java b/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java new file mode 100644 index 0000000..0cfb9c8 --- /dev/null +++ b/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java @@ -0,0 +1,153 @@ +package cloud.eppo; + +import cloud.eppo.api.*; +import cloud.eppo.ufc.dto.Variation; +import java.util.Date; +import java.util.Map; + +/** + * Extended flag evaluation result that includes detailed evaluation information for debugging and + * understanding flag assignments. + */ +public class DetailedFlagEvaluationResult extends FlagEvaluationResult { + private final EvaluationDetails evaluationDetails; + + public DetailedFlagEvaluationResult( + String flagKey, + String subjectKey, + Attributes subjectAttributes, + String allocationKey, + Variation variation, + Map extraLogging, + boolean doLog, + EvaluationDetails evaluationDetails) { + super(flagKey, subjectKey, subjectAttributes, allocationKey, variation, extraLogging, doLog); + this.evaluationDetails = evaluationDetails; + } + + public EvaluationDetails getEvaluationDetails() { + return evaluationDetails; + } + + /** Builder to construct detailed evaluation results during flag evaluation. */ + public static class Builder { + private String flagKey; + private String subjectKey; + private Attributes subjectAttributes; + private String allocationKey; + private Variation variation; + private Map extraLogging; + private boolean doLog; + + // Delegate to EvaluationDetails.Builder for evaluation details + private final EvaluationDetails.Builder detailsBuilder = EvaluationDetails.builder(); + + public Builder flagKey(String flagKey) { + this.flagKey = flagKey; + return this; + } + + public Builder subjectKey(String subjectKey) { + this.subjectKey = subjectKey; + return this; + } + + public Builder subjectAttributes(Attributes subjectAttributes) { + this.subjectAttributes = subjectAttributes; + return this; + } + + public Builder allocationKey(String allocationKey) { + this.allocationKey = allocationKey; + return this; + } + + public Builder variation(Variation variation) { + this.variation = variation; + return this; + } + + public Builder extraLogging(Map extraLogging) { + this.extraLogging = extraLogging; + return this; + } + + public Builder doLog(boolean doLog) { + this.doLog = doLog; + return this; + } + + public Builder environmentName(String environmentName) { + detailsBuilder.environmentName(environmentName); + return this; + } + + public Builder flagEvaluationCode(FlagEvaluationCode code) { + detailsBuilder.flagEvaluationCode(code); + return this; + } + + public Builder flagEvaluationDescription(String description) { + detailsBuilder.flagEvaluationDescription(description); + return this; + } + + public Builder banditKey(String banditKey) { + detailsBuilder.banditKey(banditKey); + return this; + } + + public Builder banditAction(String banditAction) { + detailsBuilder.banditAction(banditAction); + return this; + } + + public Builder matchedRule(MatchedRule matchedRule) { + detailsBuilder.matchedRule(matchedRule); + return this; + } + + public Builder matchedAllocation(AllocationDetails matchedAllocation) { + detailsBuilder.matchedAllocation(matchedAllocation); + return this; + } + + public Builder addUnmatchedAllocation(AllocationDetails allocation) { + detailsBuilder.addUnmatchedAllocation(allocation); + return this; + } + + public Builder addUnevaluatedAllocation(AllocationDetails allocation) { + detailsBuilder.addUnevaluatedAllocation(allocation); + return this; + } + + public Builder configFetchedAt(Date configFetchedAt) { + detailsBuilder.configFetchedAt(configFetchedAt); + return this; + } + + public Builder configPublishedAt(Date configPublishedAt) { + detailsBuilder.configPublishedAt(configPublishedAt); + return this; + } + + public DetailedFlagEvaluationResult build() { + // Set variation details before building + if (variation != null) { + detailsBuilder.variationKey(variation.getKey()); + detailsBuilder.variationValue(variation.getValue()); + } + + return new DetailedFlagEvaluationResult( + flagKey, + subjectKey, + subjectAttributes, + allocationKey, + variation, + extraLogging, + doLog, + detailsBuilder.build()); + } + } +} diff --git a/src/main/java/cloud/eppo/FlagEvaluator.java b/src/main/java/cloud/eppo/FlagEvaluator.java index 0a2e78f..015c05b 100644 --- a/src/main/java/cloud/eppo/FlagEvaluator.java +++ b/src/main/java/cloud/eppo/FlagEvaluator.java @@ -1,98 +1,168 @@ package cloud.eppo; import static cloud.eppo.Utils.base64Decode; +import static cloud.eppo.Utils.getMD5Hex; import static cloud.eppo.Utils.getShard; +import cloud.eppo.api.AllocationDetails; +import cloud.eppo.api.AllocationEvaluationCode; import cloud.eppo.api.Attributes; import cloud.eppo.api.EppoValue; +import cloud.eppo.api.FlagEvaluationCode; +import cloud.eppo.api.MatchedRule; +import cloud.eppo.api.RuleCondition; import cloud.eppo.model.ShardRange; import cloud.eppo.ufc.dto.Allocation; import cloud.eppo.ufc.dto.FlagConfig; import cloud.eppo.ufc.dto.Shard; import cloud.eppo.ufc.dto.Split; +import cloud.eppo.ufc.dto.TargetingRule; import cloud.eppo.ufc.dto.Variation; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class FlagEvaluator { - public static FlagEvaluationResult evaluateFlag( + /** + * Evaluates a flag and returns detailed evaluation information including allocation statuses, + * matched rules, and evaluation codes. This is useful for debugging and understanding why a + * particular variation was assigned. + */ + public static DetailedFlagEvaluationResult evaluateFlagWithDetails( FlagConfig flag, String flagKey, String subjectKey, Attributes subjectAttributes, - boolean isConfigObfuscated) { + boolean isConfigObfuscated, + String environmentName, + Date configFetchedAt, + Date configPublishedAt) { Date now = new Date(); - Variation variation = null; - String allocationKey = null; - Map extraLogging = new HashMap<>(); - boolean doLog = false; + DetailedFlagEvaluationResult.Builder builder = + new DetailedFlagEvaluationResult.Builder() + .flagKey(flagKey) + .subjectKey(subjectKey) + .subjectAttributes(subjectAttributes) + .extraLogging(new HashMap<>()) + .environmentName(environmentName != null ? environmentName : "Unknown") + .configFetchedAt(configFetchedAt) + .configPublishedAt(configPublishedAt); + + // Handle disabled flag + if (!flag.isEnabled()) { + builder + .doLog(false) + .flagEvaluationCode(FlagEvaluationCode.FLAG_UNRECOGNIZED_OR_DISABLED) + .flagEvaluationDescription("Unrecognized or disabled flag: " + flagKey); + + // All allocations are unevaluated for disabled flags + if (flag.getAllocations() != null) { + for (int i = 0; i < flag.getAllocations().size(); i++) { + Allocation allocation = flag.getAllocations().get(i); + String allocationKey = + isConfigObfuscated ? base64Decode(allocation.getKey()) : allocation.getKey(); + builder.addUnevaluatedAllocation( + new AllocationDetails(allocationKey, AllocationEvaluationCode.UNEVALUATED, i + 1)); + } + } + + return builder.build(); + } - // If flag is disabled; use an empty list of allocations so that the empty result is returned - // Note: this is a safety check; disabled flags should be filtered upstream List allocationsToConsider = - flag.isEnabled() && flag.getAllocations() != null - ? flag.getAllocations() - : new LinkedList<>(); + flag.getAllocations() != null ? flag.getAllocations() : new LinkedList<>(); + + int allocationPosition = 0; + boolean foundMatch = false; for (Allocation allocation : allocationsToConsider) { + allocationPosition++; + String allocationKey = allocation.getKey(); + String deobfuscatedAllocationKey = + isConfigObfuscated ? base64Decode(allocationKey) : allocationKey; + + // Check if allocation is time-bound and not yet active if (allocation.getStartAt() != null && allocation.getStartAt().after(now)) { - // Allocation not yet active + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.BEFORE_START_TIME, + allocationPosition)); continue; } + + // Check if allocation is time-bound and no longer active if (allocation.getEndAt() != null && allocation.getEndAt().before(now)) { - // Allocation no longer active + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.AFTER_END_TIME, + allocationPosition)); continue; } - // For convenience, we will automatically include the subject key as the "id" attribute if - // none is provided + // For convenience, automatically include subject key as "id" attribute if not provided Attributes subjectAttributesToEvaluate = new Attributes(subjectAttributes); if (!subjectAttributesToEvaluate.containsKey("id")) { subjectAttributesToEvaluate.put("id", subjectKey); } - if (allocation.getRules() != null - && !allocation.getRules().isEmpty() - && RuleEvaluator.findMatchingRule( - subjectAttributesToEvaluate, allocation.getRules(), isConfigObfuscated) - == null) { - // Rules are defined, but none match - continue; + // Check rules + TargetingRule matchedTargetingRule = null; + if (allocation.getRules() != null && !allocation.getRules().isEmpty()) { + matchedTargetingRule = + RuleEvaluator.findMatchingRule( + subjectAttributesToEvaluate, allocation.getRules(), isConfigObfuscated); + + if (matchedTargetingRule == null) { + // Rules are defined but none match + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.FAILING_RULE, + allocationPosition)); + continue; + } } - // This allocation has matched; find variation + // This allocation has matched rules; find variation in splits + Variation variation = null; + Map extraLogging = new HashMap<>(); + Split matchedSplit = null; + for (Split split : allocation.getSplits()) { if (allShardsMatch(split, subjectKey, flag.getTotalShards(), isConfigObfuscated)) { - // Variation and extra logging is determined by the relevant split variation = flag.getVariations().get(split.getVariationKey()); if (variation == null) { throw new RuntimeException("Unknown split variation key: " + split.getVariationKey()); } extraLogging = split.getExtraLogging(); + matchedSplit = split; break; } } - if (variation != null) { - // We only evaluate the first relevant allocation - allocationKey = allocation.getKey(); - // doLog is determined by the allocation - doLog = allocation.doLog(); - break; + if (variation == null) { + // Rules matched but subject doesn't fall in traffic split + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS, + allocationPosition)); + continue; } - } - if (isConfigObfuscated) { - // Need to unobfuscate for the returned evaluation result - if (allocationKey != null) { - allocationKey = base64Decode(allocationKey); - } - if (variation != null) { + foundMatch = true; + + // Deobfuscate if needed + if (isConfigObfuscated) { + allocationKey = deobfuscatedAllocationKey; String key = base64Decode(variation.getKey()); EppoValue decodedValue = EppoValue.nullValue(); if (!variation.getValue().isNull()) { @@ -116,27 +186,116 @@ public static FlagEvaluationResult evaluateFlag( } } variation = new Variation(key, decodedValue); - } - // Deobfuscate extraLogging if present - if (extraLogging != null && !extraLogging.isEmpty()) { - Map deobfuscatedExtraLogging = new HashMap<>(); - for (Map.Entry entry : extraLogging.entrySet()) { - try { - String deobfuscatedKey = base64Decode(entry.getKey()); - String deobfuscatedValue = base64Decode(entry.getValue()); - deobfuscatedExtraLogging.put(deobfuscatedKey, deobfuscatedValue); - } catch (Exception e) { - // If deobfuscation fails, keep the original key-value pair - deobfuscatedExtraLogging.put(entry.getKey(), entry.getValue()); + // Deobfuscate extraLogging if present + if (extraLogging != null && !extraLogging.isEmpty()) { + Map deobfuscatedExtraLogging = new HashMap<>(); + for (Map.Entry entry : extraLogging.entrySet()) { + try { + String deobfuscatedKey = base64Decode(entry.getKey()); + String deobfuscatedValue = base64Decode(entry.getValue()); + deobfuscatedExtraLogging.put(deobfuscatedKey, deobfuscatedValue); + } catch (Exception e) { + // If deobfuscation fails, keep the original key-value pair + deobfuscatedExtraLogging.put(entry.getKey(), entry.getValue()); + } } + extraLogging = deobfuscatedExtraLogging; + } + } + + // Build matched rule details if applicable + MatchedRule matchedRule = null; + if (matchedTargetingRule != null) { + Set conditions = + matchedTargetingRule.getConditions().stream() + .map( + tc -> { + // Deobfuscate attribute name if config is obfuscated + String attribute = tc.getAttribute(); + if (isConfigObfuscated) { + // Find the original attribute name by matching the MD5 hash + for (Map.Entry entry : + subjectAttributesToEvaluate.entrySet()) { + if (getMD5Hex(entry.getKey()).equals(attribute)) { + attribute = entry.getKey(); + break; + } + } + } + + // Condition values are already handled by RuleEvaluator during evaluation + // For display purposes, we keep the raw value + return new RuleCondition(attribute, tc.getOperator().value, tc.getValue()); + }) + .collect(Collectors.toSet()); + matchedRule = new MatchedRule(conditions); + } + + // Determine evaluation description + String description; + if (matchedRule != null) { + // Check if we need to include traffic assignment details + // Include traffic details if there are multiple splits OR multiple shards + boolean hasMultipleSplits = allocation.getSplits().size() > 1; + boolean hasMultipleShards = + matchedSplit.getShards() != null && matchedSplit.getShards().size() > 1; + + if (hasMultipleSplits || hasMultipleShards) { + description = + String.format( + "Supplied attributes match rules defined in allocation \"%s\" and %s belongs to the range of traffic assigned to \"%s\".", + allocationKey, subjectKey, variation.getKey()); + } else { + description = + String.format( + "Supplied attributes match rules defined in allocation \"%s\".", allocationKey); } - extraLogging = deobfuscatedExtraLogging; + } else { + description = + String.format( + "%s belongs to the range of traffic assigned to \"%s\" defined in allocation \"%s\".", + subjectKey, variation.getKey(), allocationKey); + } + + // TODO check type + + builder + .allocationKey(allocationKey) + .variation(variation) + .extraLogging(extraLogging) + .doLog(allocation.doLog()) + .flagEvaluationCode(FlagEvaluationCode.MATCH) + .flagEvaluationDescription(description) + .matchedRule(matchedRule) + .matchedAllocation( + new AllocationDetails( + allocationKey, AllocationEvaluationCode.MATCH, allocationPosition)); + + // Mark remaining allocations as unevaluated + for (int i = allocationPosition; i < allocationsToConsider.size(); i++) { + Allocation unevaluatedAllocation = allocationsToConsider.get(i); + String unevaluatedKey = + isConfigObfuscated + ? base64Decode(unevaluatedAllocation.getKey()) + : unevaluatedAllocation.getKey(); + builder.addUnevaluatedAllocation( + new AllocationDetails(unevaluatedKey, AllocationEvaluationCode.UNEVALUATED, i + 1)); } + + break; + } + + // If no match was found, return default with appropriate code + if (!foundMatch) { + builder + .doLog(false) + .flagEvaluationCode(FlagEvaluationCode.DEFAULT_ALLOCATION_NULL) + .flagEvaluationDescription( + "No allocations matched. Falling back to \"Default Allocation\", serving NULL"); } - return new FlagEvaluationResult( - flagKey, subjectKey, subjectAttributes, allocationKey, variation, extraLogging, doLog); + return builder.build(); } private static boolean allShardsMatch( diff --git a/src/test/java/cloud/eppo/FlagEvaluatorTest.java b/src/test/java/cloud/eppo/FlagEvaluatorTest.java index c4c7c21..3f89541 100644 --- a/src/test/java/cloud/eppo/FlagEvaluatorTest.java +++ b/src/test/java/cloud/eppo/FlagEvaluatorTest.java @@ -6,6 +6,7 @@ import cloud.eppo.api.Attributes; import cloud.eppo.api.EppoValue; +import cloud.eppo.api.EvaluationDetails; import cloud.eppo.model.ShardRange; import cloud.eppo.ufc.dto.Allocation; import cloud.eppo.ufc.dto.FlagConfig; @@ -37,8 +38,22 @@ public void testDisabledFlag() { List splits = createSplits("a", shards); List allocations = createAllocations("allocation", splits); FlagConfig flag = createFlag("flag", false, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + + // Create test metadata values + String testEnvironmentName = "Production"; + Date testConfigFetchedAt = new Date(1672531200000L); // Jan 1, 2023 + Date testConfigPublishedAt = new Date(1672444800000L); // Dec 31, 2022 + + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, + "flag", + "subjectKey", + new Attributes(), + false, + testEnvironmentName, + testConfigFetchedAt, + testConfigPublishedAt); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -46,14 +61,22 @@ public void testDisabledFlag() { assertNull(result.getAllocationKey()); assertNull(result.getVariation()); assertFalse(result.doLog()); + + // Verify configuration metadata flows through to evaluation details + EvaluationDetails details = result.getEvaluationDetails(); + assertNotNull(details); + assertEquals("Production", details.getEnvironmentName()); + assertEquals(testConfigFetchedAt, details.getConfigFetchedAt()); + assertEquals(testConfigPublishedAt, details.getConfigPublishedAt()); } @Test public void testNoAllocations() { Map variations = createVariations("a"); FlagConfig flag = createFlag("flag", true, variations, null); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -70,8 +93,22 @@ public void testSimpleFlag() { List splits = createSplits("a", shards); List allocations = createAllocations("allocation", splits); FlagConfig flag = createFlag("flag", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + + // Create test metadata values + String testEnvironmentName = "Staging"; + Date testConfigFetchedAt = new Date(1672617600000L); // Jan 2, 2023 + Date testConfigPublishedAt = new Date(1672531200000L); // Jan 1, 2023 + + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, + "flag", + "subjectKey", + new Attributes(), + false, + testEnvironmentName, + testConfigFetchedAt, + testConfigPublishedAt); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -79,6 +116,13 @@ public void testSimpleFlag() { assertEquals("allocation", result.getAllocationKey()); assertEquals("A", result.getVariation().getValue().stringValue()); assertTrue(result.doLog()); + + // Verify configuration metadata flows through to evaluation details + EvaluationDetails details = result.getEvaluationDetails(); + assertNotNull(details); + assertEquals("Staging", details.getEnvironmentName()); + assertEquals(testConfigFetchedAt, details.getConfigFetchedAt()); + assertEquals(testConfigPublishedAt, details.getConfigPublishedAt()); } @Test @@ -97,16 +141,21 @@ public void testIDTargetingCondition() { // Check that subjectKey is evaluated as the "id" attribute - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "alice", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "alice", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "bob", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "bob", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "charlie", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "charlie", new Attributes(), false, null, null, null); assertNull(result.getVariation()); @@ -114,14 +163,18 @@ public void testIDTargetingCondition() { Attributes aliceAttributes = new Attributes(); aliceAttributes.put("id", "charlie"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "alice", aliceAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "alice", aliceAttributes, false, null, null, null); assertNull(result.getVariation()); Attributes charlieAttributes = new Attributes(); charlieAttributes.put("id", "alice"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "charlie", charlieAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "charlie", charlieAttributes, false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -133,8 +186,9 @@ public void testCatchAllAllocation() { List allocations = createAllocations("default", splits); FlagConfig flag = createFlag("key", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals("default", result.getAllocationKey()); assertEquals("A", result.getVariation().getValue().stringValue()); @@ -155,16 +209,21 @@ public void testMultipleAllocations() { Attributes matchingEmailAttributes = new Attributes(); matchingEmailAttributes.put("email", "eppo@example.com"); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", matchingEmailAttributes, false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", matchingEmailAttributes, false, null, null, null); assertEquals("B", result.getVariation().getValue().stringValue()); Attributes unknownEmailAttributes = new Attributes(); unknownEmailAttributes.put("email", "eppo@test.com"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", unknownEmailAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", unknownEmailAttributes, false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -188,16 +247,21 @@ public void testVariationShardRanges() { FlagConfig flag = createFlag("key", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subject4", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject4", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject13", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject13", new Attributes(), false, null, null, null); assertEquals("B", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject14", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject14", new Attributes(), false, null, null, null); assertEquals("C", result.getVariation().getValue().stringValue()); } @@ -219,8 +283,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(startAt); allocation.setEndAt(endAt); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); assertTrue(result.doLog()); @@ -229,7 +294,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(new Date(now.getTime() + oneDayInMilliseconds)); allocation.setEndAt(new Date(now.getTime() + 2 * oneDayInMilliseconds)); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertNull(result.getVariation()); assertFalse(result.doLog()); @@ -238,7 +305,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(new Date(now.getTime() - 2 * oneDayInMilliseconds)); allocation.setEndAt(new Date(now.getTime() - oneDayInMilliseconds)); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertNull(result.getVariation()); assertFalse(result.doLog()); @@ -332,9 +401,9 @@ public void testObfuscated() { flag.getVariationType(), encodedVariations, encodedAllocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag( - obfuscatedFlag, "flag", "subjectKey", matchingEmailAttributes, true); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", matchingEmailAttributes, true, null, null, null); // Expect an unobfuscated evaluation result assertEquals("flag", result.getFlagKey()); @@ -347,12 +416,13 @@ public void testObfuscated() { Attributes unknownEmailAttributes = new Attributes(); unknownEmailAttributes.put("email", "eppo@test.com"); result = - FlagEvaluator.evaluateFlag( - obfuscatedFlag, "flag", "subjectKey", unknownEmailAttributes, true); + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", unknownEmailAttributes, true, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); result = - FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subjectKey", new Attributes(), true); + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", new Attributes(), true, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -423,8 +493,9 @@ public void testObfuscatedExtraLogging() { encodedAllocations); // Test with obfuscated config - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subject", new Attributes(), true); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subject", new Attributes(), true, null, null, null); // Verify that extraLogging is deobfuscated Map extraLogging = result.getExtraLogging(); @@ -434,7 +505,9 @@ public void testObfuscatedExtraLogging() { assertEquals(2, extraLogging.size()); // Test with non-obfuscated config to ensure no deobfuscation happens - result = FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subject", new Attributes(), false, null, null, null); // Verify that extraLogging remains obfuscated extraLogging = result.getExtraLogging(); diff --git a/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java b/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java index 3dd2480..3787a7d 100644 --- a/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java +++ b/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java @@ -94,9 +94,9 @@ public void testGetStringAssignmentPerformance() { assertEquals(0.12, variationCounts.get("yellow").doubleValue() / numIterations, 0.02); // Seeing ~48,000,000 - ~54,000,000 for 10k iterations on a M2 Macbook Pro; let's fail if more - // than 150,000,000; giving a generous allowance for slower systems (like GitHub) but will still - // catch if things slow down considerably - long maxAllowedTime = 15000 * numIterations; + // than 200,000,000; giving a generous allowance for slower systems (like GitHub) but will still + // catch if things slow down considerably (>4x) + long maxAllowedTime = 20000 * numIterations; assertTrue( elapsedTime < maxAllowedTime, "Cpu time of " + elapsedTime + " is more than the " + maxAllowedTime + " allowed");