diff --git a/src/main/java/com/launchdarkly/sdk/EvaluationReason.java b/src/main/java/com/launchdarkly/sdk/EvaluationReason.java index 17e0084..47bfbac 100644 --- a/src/main/java/com/launchdarkly/sdk/EvaluationReason.java +++ b/src/main/java/com/launchdarkly/sdk/EvaluationReason.java @@ -27,6 +27,9 @@ */ @JsonAdapter(EvaluationReasonTypeAdapter.class) public final class EvaluationReason implements JsonSerializable { + private static boolean IN_EXPERIMENT = true; + private static boolean NOT_IN_EXPERIMENT = false; + /** * Enumerated type defining the possible values of {@link EvaluationReason#getKind()}. */ @@ -96,6 +99,7 @@ public static enum ErrorKind { // static instances to avoid repeatedly allocating reasons for the same parameters private static final EvaluationReason OFF_INSTANCE = new EvaluationReason(Kind.OFF); private static final EvaluationReason FALLTHROUGH_INSTANCE = new EvaluationReason(Kind.FALLTHROUGH); + private static final EvaluationReason FALLTHROUGH_INSTANCE_IN_EXPERIMENT = new EvaluationReason(Kind.FALLTHROUGH, IN_EXPERIMENT); private static final EvaluationReason TARGET_MATCH_INSTANCE = new EvaluationReason(Kind.TARGET_MATCH); private static final EvaluationReason ERROR_CLIENT_NOT_READY = new EvaluationReason(ErrorKind.CLIENT_NOT_READY, null); private static final EvaluationReason ERROR_FLAG_NOT_FOUND = new EvaluationReason(ErrorKind.FLAG_NOT_FOUND, null); @@ -108,25 +112,31 @@ public static enum ErrorKind { private final int ruleIndex; private final String ruleId; private final String prerequisiteKey; + private final boolean inExperiment; private final ErrorKind errorKind; private final Exception exception; - private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequisiteKey, + private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequisiteKey, boolean inExperiment, ErrorKind errorKind, Exception exception) { this.kind = kind; this.ruleIndex = ruleIndex; this.ruleId = ruleId; this.prerequisiteKey = prerequisiteKey; + this.inExperiment = inExperiment; this.errorKind = errorKind; this.exception = exception; } private EvaluationReason(Kind kind) { - this(kind, -1, null, null, null, null); + this(kind, -1, null, null, NOT_IN_EXPERIMENT, null, null); + } + + private EvaluationReason(Kind kind, boolean inExperiment) { + this(kind, -1, null, null, inExperiment, null, null); } private EvaluationReason(ErrorKind errorKind, Exception exception) { - this(Kind.ERROR, -1, null, null, errorKind, exception); + this(Kind.ERROR, -1, null, null, NOT_IN_EXPERIMENT, errorKind, exception); } /** @@ -171,6 +181,17 @@ public String getPrerequisiteKey() { return prerequisiteKey; } + /** + * Whether the evaluation was part of an experiment. Returns true if the evaluation + * resulted in an experiment rollout *and* served one of the variations in the + * experiment. Otherwise it returns false. + * + * @return whether the evaluation was part of an experiment + */ + public boolean isInExperiment() { + return inExperiment; + } + /** * An enumeration value indicating the general category of error, if the * {@code kind} is {@link Kind#PREREQUISITE_FAILED}. Otherwise {@code null}. @@ -223,16 +244,20 @@ public boolean equals(Object other) { } if (other instanceof EvaluationReason) { EvaluationReason o = (EvaluationReason)other; - return kind == o.kind && ruleIndex == o.ruleIndex && Objects.equals(ruleId, o.ruleId)&& - Objects.equals(prerequisiteKey, o.prerequisiteKey) && Objects.equals(errorKind, o.errorKind) && - Objects.equals(exception, o.exception); + return kind == o.kind && + ruleIndex == o.ruleIndex && + Objects.equals(ruleId, o.ruleId) && + Objects.equals(prerequisiteKey, o.prerequisiteKey) && + inExperiment == o.inExperiment && + Objects.equals(errorKind, o.errorKind) && + Objects.equals(exception, o.exception); } return false; } @Override public int hashCode() { - return Objects.hash(kind, ruleIndex, ruleId, prerequisiteKey, errorKind, exception); + return Objects.hash(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind, exception); } /** @@ -253,6 +278,18 @@ public static EvaluationReason fallthrough() { return FALLTHROUGH_INSTANCE; } + /** + * Returns an instance whose {@code kind} is {@link Kind#FALLTHROUGH} and + * where the inExperiment parameter represents whether the evaluation was + * part of an experiment. + * + * @param inExperiment whether the evaluation was part of an experiment + * @return a reason object + */ + public static EvaluationReason fallthrough(boolean inExperiment) { + return inExperiment ? FALLTHROUGH_INSTANCE_IN_EXPERIMENT : FALLTHROUGH_INSTANCE; + } + /** * Returns an instance whose {@code kind} is {@link Kind#TARGET_MATCH}. * @@ -270,7 +307,21 @@ public static EvaluationReason targetMatch() { * @return a reason object */ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId) { - return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, null, null); + return ruleMatch(ruleIndex, ruleId, NOT_IN_EXPERIMENT); + } + + /** + * Returns an instance whose {@code kind} is {@link Kind#RULE_MATCH} and + * where the inExperiment parameter represents whether the evaluation was + * part of an experiment. + * + * @param ruleIndex the rule index + * @param ruleId the rule identifier + * @param inExperiment whether the evaluation was part of an experiment + * @return a reason object + */ + public static EvaluationReason ruleMatch(int ruleIndex, String ruleId, boolean inExperiment) { + return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, inExperiment, null, null); } /** @@ -280,7 +331,7 @@ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId) { * @return a reason object */ public static EvaluationReason prerequisiteFailed(String prerequisiteKey) { - return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, null, null); + return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, NOT_IN_EXPERIMENT, null, null); } /** diff --git a/src/main/java/com/launchdarkly/sdk/EvaluationReasonTypeAdapter.java b/src/main/java/com/launchdarkly/sdk/EvaluationReasonTypeAdapter.java index 389f0d8..122f72f 100644 --- a/src/main/java/com/launchdarkly/sdk/EvaluationReasonTypeAdapter.java +++ b/src/main/java/com/launchdarkly/sdk/EvaluationReasonTypeAdapter.java @@ -22,6 +22,7 @@ static EvaluationReason parse(JsonReader reader) throws IOException { int ruleIndex = -1; String ruleId = null; String prereqKey = null; + boolean inExperiment = false; EvaluationReason.ErrorKind errorKind = null; reader.beginObject(); @@ -40,6 +41,9 @@ static EvaluationReason parse(JsonReader reader) throws IOException { case "prerequisiteKey": prereqKey = reader.nextString(); break; + case "inExperiment": + inExperiment = reader.nextBoolean(); + break; case "errorKind": errorKind = readEnum(EvaluationReason.ErrorKind.class, reader); break; @@ -56,11 +60,11 @@ static EvaluationReason parse(JsonReader reader) throws IOException { case OFF: return EvaluationReason.off(); case FALLTHROUGH: - return EvaluationReason.fallthrough(); + return EvaluationReason.fallthrough(inExperiment); case TARGET_MATCH: return EvaluationReason.targetMatch(); case RULE_MATCH: - return EvaluationReason.ruleMatch(ruleIndex, ruleId); + return EvaluationReason.ruleMatch(ruleIndex, ruleId, inExperiment); case PREREQUISITE_FAILED: return EvaluationReason.prerequisiteFailed(prereqKey); case ERROR: @@ -85,6 +89,16 @@ public void write(JsonWriter writer, EvaluationReason reason) throws IOException writer.name("ruleId"); writer.value(reason.getRuleId()); } + if (reason.isInExperiment()) { + writer.name("inExperiment"); + writer.value(reason.isInExperiment()); + } + break; + case FALLTHROUGH: + if (reason.isInExperiment()) { + writer.name("inExperiment"); + writer.value(reason.isInExperiment()); + } break; case PREREQUISITE_FAILED: writer.name("prerequisiteKey"); diff --git a/src/test/java/com/launchdarkly/sdk/EvaluationReasonTest.java b/src/test/java/com/launchdarkly/sdk/EvaluationReasonTest.java index 54625f8..5311f9f 100644 --- a/src/test/java/com/launchdarkly/sdk/EvaluationReasonTest.java +++ b/src/test/java/com/launchdarkly/sdk/EvaluationReasonTest.java @@ -70,10 +70,11 @@ public void basicProperties() { @Test public void simpleStringRepresentations() { assertEquals("OFF", EvaluationReason.off().toString()); - assertEquals("FALLTHROUGH", EvaluationReason.fallthrough().toString()); assertEquals("TARGET_MATCH", EvaluationReason.targetMatch().toString()); assertEquals("RULE_MATCH(1)", EvaluationReason.ruleMatch(1, null).toString()); assertEquals("RULE_MATCH(1,id)", EvaluationReason.ruleMatch(1, "id").toString()); + assertEquals("RULE_MATCH(1,id)", EvaluationReason.ruleMatch(1, "id", false).toString()); + assertEquals("RULE_MATCH(1,id)", EvaluationReason.ruleMatch(1, "id", true).toString()); assertEquals("PREREQUISITE_FAILED(key)", EvaluationReason.prerequisiteFailed("key").toString()); assertEquals("ERROR(FLAG_NOT_FOUND)", EvaluationReason.error(FLAG_NOT_FOUND).toString()); assertEquals("ERROR(EXCEPTION)", EvaluationReason.exception(null).toString()); diff --git a/src/test/java/com/launchdarkly/sdk/json/EvaluationReasonJsonSerializationTest.java b/src/test/java/com/launchdarkly/sdk/json/EvaluationReasonJsonSerializationTest.java index 1ae82e0..0af6d32 100644 --- a/src/test/java/com/launchdarkly/sdk/json/EvaluationReasonJsonSerializationTest.java +++ b/src/test/java/com/launchdarkly/sdk/json/EvaluationReasonJsonSerializationTest.java @@ -6,6 +6,7 @@ import org.junit.Test; import static com.launchdarkly.sdk.json.JsonTestHelpers.verifyDeserializeInvalidJson; +import static com.launchdarkly.sdk.json.JsonTestHelpers.verifyDeserialize; import static com.launchdarkly.sdk.json.JsonTestHelpers.verifySerialize; import static com.launchdarkly.sdk.json.JsonTestHelpers.verifySerializeAndDeserialize; @@ -15,16 +16,31 @@ public class EvaluationReasonJsonSerializationTest extends BaseTest { public void reasonJsonSerializations() throws Exception { verifySerializeAndDeserialize(EvaluationReason.off(), "{\"kind\":\"OFF\"}"); verifySerializeAndDeserialize(EvaluationReason.fallthrough(), "{\"kind\":\"FALLTHROUGH\"}"); + verifySerializeAndDeserialize(EvaluationReason.fallthrough(false), "{\"kind\":\"FALLTHROUGH\"}"); + verifySerializeAndDeserialize(EvaluationReason.fallthrough(true), "{\"kind\":\"FALLTHROUGH\",\"inExperiment\":true}"); verifySerializeAndDeserialize(EvaluationReason.targetMatch(), "{\"kind\":\"TARGET_MATCH\"}"); verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, "id"), "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\"}"); + verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, "id", false), + "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\"}"); + verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, "id", true), + "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\",\"inExperiment\":true}"); verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, null), "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1}"); + verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, null, false), + "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1}"); + verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, null, true), + "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"inExperiment\":true}"); verifySerializeAndDeserialize(EvaluationReason.prerequisiteFailed("key"), "{\"kind\":\"PREREQUISITE_FAILED\",\"prerequisiteKey\":\"key\"}"); verifySerializeAndDeserialize(EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND), "{\"kind\":\"ERROR\",\"errorKind\":\"FLAG_NOT_FOUND\"}"); + // properties with defaults can be included + verifyDeserialize(EvaluationReason.fallthrough(false), "{\"kind\":\"FALLTHROUGH\",\"inExperiment\":false}"); + verifyDeserialize(EvaluationReason.ruleMatch(1, "id", false), + "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\",\"inExperiment\":false}"); + // unknown properties are ignored JsonTestHelpers.verifyDeserialize(EvaluationReason.off(), "{\"kind\":\"OFF\",\"other\":true}");