From 93c0b1047594e304612ba74a39543c47ac1e3ea3 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 10:13:50 -0500 Subject: [PATCH 1/3] AssignmentDetails return object and supporting classes --- .../cloud/eppo/api/AllocationDetails.java | 30 ++ .../eppo/api/AllocationEvaluationCode.java | 61 ++++ .../cloud/eppo/api/AssignmentDetails.java | 31 ++ .../cloud/eppo/api/EvaluationDetails.java | 271 ++++++++++++++++++ .../cloud/eppo/api/FlagEvaluationCode.java | 74 +++++ src/main/java/cloud/eppo/api/MatchedRule.java | 16 ++ .../java/cloud/eppo/api/RuleCondition.java | 57 ++++ 7 files changed, 540 insertions(+) create mode 100644 src/main/java/cloud/eppo/api/AllocationDetails.java create mode 100644 src/main/java/cloud/eppo/api/AllocationEvaluationCode.java create mode 100644 src/main/java/cloud/eppo/api/AssignmentDetails.java create mode 100644 src/main/java/cloud/eppo/api/EvaluationDetails.java create mode 100644 src/main/java/cloud/eppo/api/FlagEvaluationCode.java create mode 100644 src/main/java/cloud/eppo/api/MatchedRule.java create mode 100644 src/main/java/cloud/eppo/api/RuleCondition.java diff --git a/src/main/java/cloud/eppo/api/AllocationDetails.java b/src/main/java/cloud/eppo/api/AllocationDetails.java new file mode 100644 index 0000000..b7bb8b9 --- /dev/null +++ b/src/main/java/cloud/eppo/api/AllocationDetails.java @@ -0,0 +1,30 @@ +package cloud.eppo.api; + +/** + * Details about an allocation evaluation, including its key, evaluation status, and position in the + * allocation list. + */ +public class AllocationDetails { + private final String key; + private final AllocationEvaluationCode allocationEvaluationCode; + private final int orderPosition; + + public AllocationDetails( + String key, AllocationEvaluationCode allocationEvaluationCode, int orderPosition) { + this.key = key; + this.allocationEvaluationCode = allocationEvaluationCode; + this.orderPosition = orderPosition; + } + + public String getKey() { + return key; + } + + public AllocationEvaluationCode getAllocationEvaluationCode() { + return allocationEvaluationCode; + } + + public int getOrderPosition() { + return orderPosition; + } +} diff --git a/src/main/java/cloud/eppo/api/AllocationEvaluationCode.java b/src/main/java/cloud/eppo/api/AllocationEvaluationCode.java new file mode 100644 index 0000000..1888019 --- /dev/null +++ b/src/main/java/cloud/eppo/api/AllocationEvaluationCode.java @@ -0,0 +1,61 @@ +package cloud.eppo.api; + +/** + * Enum representing the result code of an allocation evaluation within a flag. + * + *

Allocations are evaluated in order, and this code indicates why an allocation was or was not + * selected. + */ +public enum AllocationEvaluationCode { + /** Allocation rules matched and the allocation was selected. */ + MATCH("MATCH"), + + /** Allocation rules did not match the subject attributes. */ + FAILING_RULE("FAILING_RULE"), + + /** Current time is before the allocation's start time. */ + BEFORE_START_TIME("BEFORE_START_TIME"), + + /** Current time is after the allocation's end time. */ + AFTER_END_TIME("AFTER_END_TIME"), + + /** Subject was not selected due to traffic exposure percentage. */ + TRAFFIC_EXPOSURE_MISS("TRAFFIC_EXPOSURE_MISS"), + + /** Allocation was not evaluated (e.g., a previous allocation matched). */ + UNEVALUATED("UNEVALUATED"); + + private final String code; + + AllocationEvaluationCode(String code) { + this.code = code; + } + + /** Returns the string representation of this allocation evaluation code. */ + public String getCode() { + return code; + } + + /** + * Parses a string code into an AllocationEvaluationCode enum. + * + * @param code the string code to parse + * @return the corresponding AllocationEvaluationCode, or null if not recognized + */ + public static AllocationEvaluationCode fromString(String code) { + if (code == null) { + return null; + } + for (AllocationEvaluationCode evaluationCode : values()) { + if (evaluationCode.code.equals(code)) { + return evaluationCode; + } + } + return null; + } + + @Override + public String toString() { + return code; + } +} diff --git a/src/main/java/cloud/eppo/api/AssignmentDetails.java b/src/main/java/cloud/eppo/api/AssignmentDetails.java new file mode 100644 index 0000000..5ad296b --- /dev/null +++ b/src/main/java/cloud/eppo/api/AssignmentDetails.java @@ -0,0 +1,31 @@ +package cloud.eppo.api; + +/** + * Contains both the assigned variation value and comprehensive evaluation details explaining why + * that variation was assigned. + * + * @param The type of the variation value (Boolean, Integer, Double, String, etc.) + */ +public class AssignmentDetails { + private final T variation; + private final String action; + private final EvaluationDetails evaluationDetails; + + public AssignmentDetails(T variation, String action, EvaluationDetails evaluationDetails) { + this.variation = variation; + this.action = action; + this.evaluationDetails = evaluationDetails; + } + + public T getVariation() { + return variation; + } + + public String getAction() { + return action; + } + + public EvaluationDetails getEvaluationDetails() { + return evaluationDetails; + } +} diff --git a/src/main/java/cloud/eppo/api/EvaluationDetails.java b/src/main/java/cloud/eppo/api/EvaluationDetails.java new file mode 100644 index 0000000..0d500de --- /dev/null +++ b/src/main/java/cloud/eppo/api/EvaluationDetails.java @@ -0,0 +1,271 @@ +package cloud.eppo.api; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Contains comprehensive debugging information about a flag evaluation. This includes why a + * particular variation was assigned, which allocations matched or didn't match, and other metadata + * useful for understanding flag behavior. + */ +public class EvaluationDetails { + private final String environmentName; + private final FlagEvaluationCode flagEvaluationCode; + private final String flagEvaluationDescription; + private final String banditKey; + private final String banditAction; + private final String variationKey; + private final EppoValue variationValue; + private final MatchedRule matchedRule; + private final AllocationDetails matchedAllocation; + private final List unmatchedAllocations; + private final List unevaluatedAllocations; + private final Date configFetchedAt; + private final Date configPublishedAt; + + public EvaluationDetails( + String environmentName, + Date configFetchedAt, + Date configPublishedAt, + FlagEvaluationCode flagEvaluationCode, + String flagEvaluationDescription, + String banditKey, + String banditAction, + String variationKey, + EppoValue variationValue, + MatchedRule matchedRule, + AllocationDetails matchedAllocation, + List unmatchedAllocations, + List unevaluatedAllocations) { + this.environmentName = environmentName; + this.configFetchedAt = configFetchedAt; + this.configPublishedAt = configPublishedAt; + this.flagEvaluationCode = flagEvaluationCode; + this.flagEvaluationDescription = flagEvaluationDescription; + this.banditKey = banditKey; + this.banditAction = banditAction; + this.variationKey = variationKey; + this.variationValue = variationValue; + this.matchedRule = matchedRule; + this.matchedAllocation = matchedAllocation; + this.unmatchedAllocations = unmatchedAllocations; + this.unevaluatedAllocations = unevaluatedAllocations; + } + + public String getEnvironmentName() { + return environmentName; + } + + public Date getConfigFetchedAt() { + return configFetchedAt; + } + + public Date getConfigPublishedAt() { + return configPublishedAt; + } + + public FlagEvaluationCode getFlagEvaluationCode() { + return flagEvaluationCode; + } + + public String getFlagEvaluationDescription() { + return flagEvaluationDescription; + } + + public String getBanditKey() { + return banditKey; + } + + public String getBanditAction() { + return banditAction; + } + + public String getVariationKey() { + return variationKey; + } + + public EppoValue getVariationValue() { + return variationValue; + } + + public MatchedRule getMatchedRule() { + return matchedRule; + } + + public AllocationDetails getMatchedAllocation() { + return matchedAllocation; + } + + public List getUnmatchedAllocations() { + return unmatchedAllocations; + } + + public List getUnevaluatedAllocations() { + return unevaluatedAllocations; + } + + public boolean evaluationSuccessful() { + return !flagEvaluationCode.isError(); + } + + /** Creates a new Builder for constructing EvaluationDetails. */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a default EvaluationDetails for error conditions or when no flag was matched. This is a + * convenience factory method for common error scenarios. + */ + public static EvaluationDetails buildDefault( + String environmentName, + Date configFetchedAt, + Date configPublishedAt, + FlagEvaluationCode flagEvaluationCode, + String flagEvaluationDescription, + EppoValue variationValue) { + return builder() + .environmentName(environmentName) + .configFetchedAt(configFetchedAt) + .configPublishedAt(configPublishedAt) + .flagEvaluationCode(flagEvaluationCode) + .flagEvaluationDescription(flagEvaluationDescription) + .variationValue(variationValue) + .build(); + } + + /** + * Creates a new Builder initialized with values from an existing EvaluationDetails. Useful for + * creating a modified copy. + */ + public static Builder builder(EvaluationDetails copyFrom) { + return new Builder() + .environmentName(copyFrom.environmentName) + .configFetchedAt(copyFrom.configFetchedAt) + .configPublishedAt(copyFrom.configPublishedAt) + .flagEvaluationCode(copyFrom.flagEvaluationCode) + .flagEvaluationDescription(copyFrom.flagEvaluationDescription) + .banditKey(copyFrom.banditKey) + .banditAction(copyFrom.banditAction) + .variationKey(copyFrom.variationKey) + .variationValue(copyFrom.variationValue) + .matchedRule(copyFrom.matchedRule) + .matchedAllocation(copyFrom.matchedAllocation) + .unmatchedAllocations(copyFrom.unmatchedAllocations) + .unevaluatedAllocations(copyFrom.unevaluatedAllocations); + } + + /** Builder for constructing EvaluationDetails instances. */ + public static class Builder { + private String environmentName = "Unknown"; + private Date configFetchedAt; + private Date configPublishedAt; + private FlagEvaluationCode flagEvaluationCode; + private String flagEvaluationDescription; + private String banditKey; + private String banditAction; + private String variationKey; + private EppoValue variationValue; + private MatchedRule matchedRule; + private AllocationDetails matchedAllocation; + private List unmatchedAllocations = new ArrayList<>(); + private List unevaluatedAllocations = new ArrayList<>(); + + public Builder environmentName(String environmentName) { + this.environmentName = environmentName != null ? environmentName : "Unknown"; + return this; + } + + public Builder configFetchedAt(Date configFetchedAt) { + this.configFetchedAt = configFetchedAt; + return this; + } + + public Builder configPublishedAt(Date configPublishedAt) { + this.configPublishedAt = configPublishedAt; + return this; + } + + public Builder flagEvaluationCode(FlagEvaluationCode flagEvaluationCode) { + this.flagEvaluationCode = flagEvaluationCode; + return this; + } + + public Builder flagEvaluationDescription(String flagEvaluationDescription) { + this.flagEvaluationDescription = flagEvaluationDescription; + return this; + } + + public Builder banditKey(String banditKey) { + this.banditKey = banditKey; + return this; + } + + public Builder banditAction(String banditAction) { + this.banditAction = banditAction; + return this; + } + + public Builder variationKey(String variationKey) { + this.variationKey = variationKey; + return this; + } + + public Builder variationValue(EppoValue variationValue) { + this.variationValue = variationValue; + return this; + } + + public Builder matchedRule(MatchedRule matchedRule) { + this.matchedRule = matchedRule; + return this; + } + + public Builder matchedAllocation(AllocationDetails matchedAllocation) { + this.matchedAllocation = matchedAllocation; + return this; + } + + public Builder unmatchedAllocations(List unmatchedAllocations) { + this.unmatchedAllocations = + unmatchedAllocations != null ? new ArrayList<>(unmatchedAllocations) : new ArrayList<>(); + return this; + } + + public Builder addUnmatchedAllocation(AllocationDetails allocation) { + this.unmatchedAllocations.add(allocation); + return this; + } + + public Builder unevaluatedAllocations(List unevaluatedAllocations) { + this.unevaluatedAllocations = + unevaluatedAllocations != null + ? new ArrayList<>(unevaluatedAllocations) + : new ArrayList<>(); + return this; + } + + public Builder addUnevaluatedAllocation(AllocationDetails allocation) { + this.unevaluatedAllocations.add(allocation); + return this; + } + + public EvaluationDetails build() { + return new EvaluationDetails( + environmentName, + configFetchedAt, + configPublishedAt, + flagEvaluationCode, + flagEvaluationDescription, + banditKey, + banditAction, + variationKey, + variationValue, + matchedRule, + matchedAllocation, + unmatchedAllocations, + unevaluatedAllocations); + } + } +} diff --git a/src/main/java/cloud/eppo/api/FlagEvaluationCode.java b/src/main/java/cloud/eppo/api/FlagEvaluationCode.java new file mode 100644 index 0000000..1509e80 --- /dev/null +++ b/src/main/java/cloud/eppo/api/FlagEvaluationCode.java @@ -0,0 +1,74 @@ +package cloud.eppo.api; + +/** + * Enum representing the result code of a flag evaluation. + * + *

Use {@link #isError()} to determine if the evaluation resulted in an error state. + */ +public enum FlagEvaluationCode { + /** Flag was successfully evaluated and a variation was assigned. */ + MATCH("MATCH", false), + + /** Flag was not found or is disabled. */ + FLAG_UNRECOGNIZED_OR_DISABLED("FLAG_UNRECOGNIZED_OR_DISABLED", true), + + /** The flag's type doesn't match the requested type. */ + TYPE_MISMATCH("TYPE_MISMATCH", true), + + /** The variation value is incompatible with the flag's declared type. */ + ASSIGNMENT_ERROR("ASSIGNMENT_ERROR", true), + + /** No allocations were configured for the flag. */ + DEFAULT_ALLOCATION_NULL("DEFAULT_ALLOCATION_NULL", true), + + /** Flag evaluation succeeded but bandit evaluation failed. */ + BANDIT_ERROR("BANDIT_ERROR", true), + + /** No actions were supplied for bandit evaluation. */ + NO_ACTIONS_SUPPLIED_FOR_BANDIT("NO_ACTIONS_SUPPLIED_FOR_BANDIT", true); + + private final String code; + private final boolean isError; + + FlagEvaluationCode(String code, boolean isError) { + this.code = code; + this.isError = isError; + } + + /** Returns the string representation of this evaluation code. */ + public String getCode() { + return code; + } + + /** + * Returns true if this evaluation code represents an error state. + * + * @return true if the evaluation failed and the default value should be used + */ + public boolean isError() { + return isError; + } + + /** + * Parses a string code into a FlagEvaluationCode enum. + * + * @param code the string code to parse + * @return the corresponding FlagEvaluationCode, or null if not recognized + */ + public static FlagEvaluationCode fromString(String code) { + if (code == null) { + return null; + } + for (FlagEvaluationCode evaluationCode : values()) { + if (evaluationCode.code.equals(code)) { + return evaluationCode; + } + } + return null; + } + + @Override + public String toString() { + return code; + } +} diff --git a/src/main/java/cloud/eppo/api/MatchedRule.java b/src/main/java/cloud/eppo/api/MatchedRule.java new file mode 100644 index 0000000..04230c3 --- /dev/null +++ b/src/main/java/cloud/eppo/api/MatchedRule.java @@ -0,0 +1,16 @@ +package cloud.eppo.api; + +import java.util.Set; + +/** Details about a rule that matched during allocation evaluation. */ +public class MatchedRule { + private final Set conditions; + + public MatchedRule(Set conditions) { + this.conditions = conditions; + } + + public Set getConditions() { + return conditions; + } +} diff --git a/src/main/java/cloud/eppo/api/RuleCondition.java b/src/main/java/cloud/eppo/api/RuleCondition.java new file mode 100644 index 0000000..f5adf7b --- /dev/null +++ b/src/main/java/cloud/eppo/api/RuleCondition.java @@ -0,0 +1,57 @@ +package cloud.eppo.api; + +import java.util.Objects; + +/** Represents a single condition within a targeting rule. */ +public class RuleCondition { + private final String attribute; + private final String operator; + private final EppoValue value; + + public RuleCondition(String attribute, String operator, EppoValue value) { + this.attribute = attribute; + this.operator = operator; + this.value = value; + } + + public String getAttribute() { + return attribute; + } + + public String getOperator() { + return operator; + } + + public EppoValue getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RuleCondition that = (RuleCondition) o; + return Objects.equals(attribute, that.attribute) + && Objects.equals(operator, that.operator) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(attribute, operator, value); + } + + @Override + public String toString() { + return "RuleCondition{" + + "attribute='" + + attribute + + '\'' + + ", operator='" + + operator + + '\'' + + ", value=" + + value + + '}'; + } +} From 72aba1b5cab335e4e9f74b9feebcde85fa18f524 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 10:19:37 -0500 Subject: [PATCH 2/3] spatially locate flag-independent fields --- src/main/java/cloud/eppo/api/EvaluationDetails.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/cloud/eppo/api/EvaluationDetails.java b/src/main/java/cloud/eppo/api/EvaluationDetails.java index 0d500de..ab211d7 100644 --- a/src/main/java/cloud/eppo/api/EvaluationDetails.java +++ b/src/main/java/cloud/eppo/api/EvaluationDetails.java @@ -11,6 +11,8 @@ */ public class EvaluationDetails { private final String environmentName; + private final Date configFetchedAt; + private final Date configPublishedAt; private final FlagEvaluationCode flagEvaluationCode; private final String flagEvaluationDescription; private final String banditKey; @@ -21,8 +23,7 @@ public class EvaluationDetails { private final AllocationDetails matchedAllocation; private final List unmatchedAllocations; private final List unevaluatedAllocations; - private final Date configFetchedAt; - private final Date configPublishedAt; + public EvaluationDetails( String environmentName, From f18140af20c98c9fc153683de608995213ddc9b8 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 19:57:23 -0500 Subject: [PATCH 3/3] appease linter --- src/main/java/cloud/eppo/api/EvaluationDetails.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/cloud/eppo/api/EvaluationDetails.java b/src/main/java/cloud/eppo/api/EvaluationDetails.java index ab211d7..152ed2d 100644 --- a/src/main/java/cloud/eppo/api/EvaluationDetails.java +++ b/src/main/java/cloud/eppo/api/EvaluationDetails.java @@ -24,7 +24,6 @@ public class EvaluationDetails { private final List unmatchedAllocations; private final List unevaluatedAllocations; - public EvaluationDetails( String environmentName, Date configFetchedAt,