Skip to content

Commit b5deba0

Browse files
committed
feat: Add the overrideAffected indicator to EvaluationReason
The OVERRIDE specification marks an evaluation as override-affected when any definition it read came from the SDK's override store: the flag itself, a prerequisite at any depth, or a segment consulted during matching. The evaluation reason carries that marking as a boolean indicator named overrideAffected, alongside inExperiment, so that callers of the variation detail methods can tell such evaluations apart. EvaluationReason gains isOverrideAffected() and withOverrideAffected(), which returns the same instance when the value is unchanged so that the shared static instances stay shared. The indicator takes part in equality and hashing. The JSON representation writes overrideAffected only when it is true, and parsing accepts the property. Flag overrides are currently experimental and subject to change.
1 parent 8eba048 commit b5deba0

4 files changed

Lines changed: 146 additions & 12 deletions

File tree

‎lib/shared/common/src/main/java/com/launchdarkly/sdk/EvaluationReason.java‎

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,10 @@ public static enum BigSegmentsStatus {
142142
private final ErrorKind errorKind;
143143
private final Exception exception;
144144
private final BigSegmentsStatus bigSegmentsStatus;
145+
private final boolean overrideAffected;
145146

146147
private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequisiteKey, boolean inExperiment,
147-
ErrorKind errorKind, Exception exception, BigSegmentsStatus bigSegmentsStatus) {
148+
ErrorKind errorKind, Exception exception, BigSegmentsStatus bigSegmentsStatus, boolean overrideAffected) {
148149
this.kind = kind;
149150
this.ruleIndex = ruleIndex;
150151
this.ruleId = ruleId;
@@ -153,18 +154,19 @@ private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequ
153154
this.errorKind = errorKind;
154155
this.exception = exception;
155156
this.bigSegmentsStatus = bigSegmentsStatus;
157+
this.overrideAffected = overrideAffected;
156158
}
157159

158160
private EvaluationReason(Kind kind) {
159-
this(kind, -1, null, null, NOT_IN_EXPERIMENT, null, null, null);
161+
this(kind, -1, null, null, NOT_IN_EXPERIMENT, null, null, null, false);
160162
}
161163

162164
private EvaluationReason(Kind kind, boolean inExperiment) {
163-
this(kind, -1, null, null, inExperiment, null, null, null);
165+
this(kind, -1, null, null, inExperiment, null, null, null, false);
164166
}
165167

166168
private EvaluationReason(ErrorKind errorKind, Exception exception) {
167-
this(Kind.ERROR, -1, null, null, NOT_IN_EXPERIMENT, errorKind, exception, null);
169+
this(Kind.ERROR, -1, null, null, NOT_IN_EXPERIMENT, errorKind, exception, null, false);
168170
}
169171

170172
/**
@@ -267,7 +269,41 @@ public BigSegmentsStatus getBigSegmentsStatus() {
267269
*/
268270
public EvaluationReason withBigSegmentsStatus(BigSegmentsStatus bigSegmentsStatus) {
269271
return new EvaluationReason(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
270-
exception, bigSegmentsStatus);
272+
exception, bigSegmentsStatus, overrideAffected);
273+
}
274+
275+
/**
276+
* Whether an override affected this evaluation, directly or transitively. Returns true if the
277+
* evaluated flag came from the SDK's override store. Also returns true if a prerequisite flag at
278+
* any depth, or a segment read during the evaluation, came from that store. Otherwise it returns
279+
* false.
280+
* <p>
281+
* In the JSON representation, the {@code overrideAffected} property appears only when this
282+
* value is true.
283+
* <p>
284+
* Flag overrides are currently experimental and subject to change.
285+
*
286+
* @return whether an override affected the evaluation
287+
*/
288+
public boolean isOverrideAffected() {
289+
return overrideAffected;
290+
}
291+
292+
/**
293+
* Returns a copy of this {@link EvaluationReason} with a specific value for
294+
* {@link #isOverrideAffected()}. The copy keeps all other properties of the original reason.
295+
* <p>
296+
* Flag overrides are currently experimental and subject to change.
297+
*
298+
* @param overrideAffected the new property value
299+
* @return a reason object with the given value, which is this same instance if the value is unchanged
300+
*/
301+
public EvaluationReason withOverrideAffected(boolean overrideAffected) {
302+
if (this.overrideAffected == overrideAffected) {
303+
return this;
304+
}
305+
return new EvaluationReason(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
306+
exception, bigSegmentsStatus, overrideAffected);
271307
}
272308

273309
/**
@@ -305,15 +341,16 @@ public boolean equals(Object other) {
305341
inExperiment == o.inExperiment &&
306342
Objects.equals(errorKind, o.errorKind) &&
307343
Objects.equals(exception, o.exception) &&
308-
Objects.equals(bigSegmentsStatus, o.bigSegmentsStatus);
344+
Objects.equals(bigSegmentsStatus, o.bigSegmentsStatus) &&
345+
overrideAffected == o.overrideAffected;
309346
}
310347
return false;
311348
}
312349

313350
@Override
314351
public int hashCode() {
315352
return Objects.hash(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
316-
exception, bigSegmentsStatus);
353+
exception, bigSegmentsStatus, overrideAffected);
317354
}
318355

319356
/**
@@ -377,7 +414,7 @@ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId) {
377414
* @return a reason object
378415
*/
379416
public static EvaluationReason ruleMatch(int ruleIndex, String ruleId, boolean inExperiment) {
380-
return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, inExperiment, null, null, null);
417+
return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, inExperiment, null, null, null, false);
381418
}
382419

383420
/**
@@ -387,7 +424,7 @@ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId, boolean i
387424
* @return a reason object
388425
*/
389426
public static EvaluationReason prerequisiteFailed(String prerequisiteKey) {
390-
return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, NOT_IN_EXPERIMENT, null, null, null);
427+
return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, NOT_IN_EXPERIMENT, null, null, null, false);
391428
}
392429

393430
/**

‎lib/shared/common/src/main/java/com/launchdarkly/sdk/EvaluationReasonTypeAdapter.java‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
2525
boolean inExperiment = false;
2626
EvaluationReason.ErrorKind errorKind = null;
2727
EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null;
28+
boolean overrideAffected = false;
2829

2930
reader.beginObject();
3031
while (reader.peek() != JsonToken.END_OBJECT) {
@@ -51,6 +52,9 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
5152
case "bigSegmentsStatus":
5253
bigSegmentsStatus = readEnum(EvaluationReason.BigSegmentsStatus.class, reader);
5354
break;
55+
case "overrideAffected":
56+
overrideAffected = reader.nextBoolean();
57+
break;
5458
default:
5559
reader.skipValue(); // ignore any unexpected property
5660
}
@@ -85,9 +89,9 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
8589
return null;
8690
}
8791
if (bigSegmentsStatus != null) {
88-
return reason.withBigSegmentsStatus(bigSegmentsStatus);
92+
reason = reason.withBigSegmentsStatus(bigSegmentsStatus);
8993
}
90-
return reason;
94+
return reason.withOverrideAffected(overrideAffected);
9195
}
9296

9397
@Override
@@ -134,6 +138,12 @@ public void write(JsonWriter writer, EvaluationReason reason) throws IOException
134138
writer.name("bigSegmentsStatus");
135139
writer.value(reason.getBigSegmentsStatus().name());
136140
}
141+
142+
// The override marker is written only when it is true, like inExperiment.
143+
if (reason.isOverrideAffected()) {
144+
writer.name("overrideAffected");
145+
writer.value(true);
146+
}
137147

138148
writer.endObject();
139149
}

‎lib/shared/common/src/test/java/com/launchdarkly/sdk/EvaluationReasonTest.java‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
import static com.launchdarkly.sdk.EvaluationReason.Kind.RULE_MATCH;
1313
import static com.launchdarkly.sdk.EvaluationReason.Kind.TARGET_MATCH;
1414
import static org.junit.Assert.assertEquals;
15+
import static org.junit.Assert.assertFalse;
16+
import static org.junit.Assert.assertNotEquals;
1517
import static org.junit.Assert.assertNull;
1618
import static org.junit.Assert.assertSame;
19+
import static org.junit.Assert.assertTrue;
1720
import static java.util.Arrays.asList;
1821

1922
import org.junit.Test;
@@ -84,6 +87,67 @@ public void bigSegmentsStatus() {
8487
assertNull(reason.getBigSegmentsStatus());
8588
}
8689

90+
@Test
91+
public void overrideAffectedIsFalseByDefault() {
92+
assertFalse(EvaluationReason.off().isOverrideAffected());
93+
assertFalse(EvaluationReason.fallthrough().isOverrideAffected());
94+
assertFalse(EvaluationReason.fallthrough(true).isOverrideAffected());
95+
assertFalse(EvaluationReason.targetMatch().isOverrideAffected());
96+
assertFalse(EvaluationReason.ruleMatch(1, "id").isOverrideAffected());
97+
assertFalse(EvaluationReason.prerequisiteFailed("key").isOverrideAffected());
98+
assertFalse(EvaluationReason.error(FLAG_NOT_FOUND).isOverrideAffected());
99+
assertFalse(EvaluationReason.exception(new Exception("sorry")).isOverrideAffected());
100+
assertFalse(EvaluationReason.fallthrough().withBigSegmentsStatus(HEALTHY).isOverrideAffected());
101+
}
102+
103+
@Test
104+
public void withOverrideAffectedKeepsOtherProperties() {
105+
EvaluationReason reason = EvaluationReason.ruleMatch(2, "id", true).withBigSegmentsStatus(STALE);
106+
EvaluationReason marked = reason.withOverrideAffected(true);
107+
108+
assertTrue(marked.isOverrideAffected());
109+
assertFalse(reason.isOverrideAffected());
110+
assertEquals(RULE_MATCH, marked.getKind());
111+
assertEquals(2, marked.getRuleIndex());
112+
assertEquals("id", marked.getRuleId());
113+
assertTrue(marked.isInExperiment());
114+
assertEquals(STALE, marked.getBigSegmentsStatus());
115+
116+
Exception e = new Exception("sorry");
117+
EvaluationReason markedError = EvaluationReason.exception(e).withOverrideAffected(true);
118+
assertEquals(ERROR, markedError.getKind());
119+
assertEquals(EvaluationReason.ErrorKind.EXCEPTION, markedError.getErrorKind());
120+
assertEquals(e, markedError.getException());
121+
assertTrue(markedError.isOverrideAffected());
122+
123+
// The marking survives a later change of another property.
124+
assertTrue(marked.withBigSegmentsStatus(HEALTHY).isOverrideAffected());
125+
}
126+
127+
@Test
128+
public void withOverrideAffectedReturnsSameInstanceWhenUnchanged() {
129+
EvaluationReason reason = EvaluationReason.off();
130+
assertSame(reason, reason.withOverrideAffected(false));
131+
EvaluationReason marked = reason.withOverrideAffected(true);
132+
assertSame(marked, marked.withOverrideAffected(true));
133+
assertFalse(marked.withOverrideAffected(false).isOverrideAffected());
134+
}
135+
136+
@Test
137+
public void overrideAffectedParticipatesInEquality() {
138+
EvaluationReason plain = EvaluationReason.off();
139+
EvaluationReason marked = plain.withOverrideAffected(true);
140+
assertNotEquals(plain, marked);
141+
assertEquals(marked, plain.withOverrideAffected(true));
142+
assertEquals(marked.hashCode(), plain.withOverrideAffected(true).hashCode());
143+
}
144+
145+
@Test
146+
public void overrideAffectedDoesNotChangeStringRepresentation() {
147+
assertEquals("OFF", EvaluationReason.off().withOverrideAffected(true).toString());
148+
assertEquals("RULE_MATCH(1,id)", EvaluationReason.ruleMatch(1, "id").withOverrideAffected(true).toString());
149+
}
150+
87151
@Test
88152
public void simpleStringRepresentations() {
89153
assertEquals("OFF", EvaluationReason.off().toString());
@@ -129,7 +193,10 @@ public void equalInstancesAreEqual() {
129193
asList(EvaluationReason.ruleMatch(2, "id1"), EvaluationReason.ruleMatch(2, "id1")),
130194
asList(EvaluationReason.prerequisiteFailed("a"), EvaluationReason.prerequisiteFailed("a")),
131195
asList(EvaluationReason.error(CLIENT_NOT_READY), EvaluationReason.error(CLIENT_NOT_READY)),
132-
asList(EvaluationReason.error(WRONG_TYPE), EvaluationReason.error(WRONG_TYPE))
196+
asList(EvaluationReason.error(WRONG_TYPE), EvaluationReason.error(WRONG_TYPE)),
197+
asList(EvaluationReason.off().withOverrideAffected(true), EvaluationReason.off().withOverrideAffected(true)),
198+
asList(EvaluationReason.error(WRONG_TYPE).withOverrideAffected(true),
199+
EvaluationReason.error(WRONG_TYPE).withOverrideAffected(true))
133200
);
134201
TestHelpers.doEqualityTests(testValues);
135202
}

‎lib/shared/common/src/test/java/com/launchdarkly/sdk/json/EvaluationReasonJsonSerializationTest.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ public void reasonJsonSerializations() throws Exception {
5959
verifyDeserializeInvalidJson(EvaluationReason.class, "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":3}");
6060
}
6161

62+
@Test
63+
public void overrideAffectedIsSerializedOnlyWhenTrue() throws Exception {
64+
verifySerializeAndDeserialize(EvaluationReason.off().withOverrideAffected(true),
65+
"{\"kind\":\"OFF\",\"overrideAffected\":true}");
66+
verifySerializeAndDeserialize(EvaluationReason.fallthrough(true).withOverrideAffected(true),
67+
"{\"kind\":\"FALLTHROUGH\",\"inExperiment\":true,\"overrideAffected\":true}");
68+
verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, "id").withOverrideAffected(true),
69+
"{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\",\"overrideAffected\":true}");
70+
verifySerializeAndDeserialize(EvaluationReason.prerequisiteFailed("key").withOverrideAffected(true),
71+
"{\"kind\":\"PREREQUISITE_FAILED\",\"prerequisiteKey\":\"key\",\"overrideAffected\":true}");
72+
verifySerializeAndDeserialize(EvaluationReason.error(EvaluationReason.ErrorKind.MALFORMED_FLAG).withOverrideAffected(true),
73+
"{\"kind\":\"ERROR\",\"errorKind\":\"MALFORMED_FLAG\",\"overrideAffected\":true}");
74+
verifySerializeAndDeserialize(EvaluationReason.fallthrough().withBigSegmentsStatus(HEALTHY).withOverrideAffected(true),
75+
"{\"kind\":\"FALLTHROUGH\",\"bigSegmentsStatus\":\"HEALTHY\",\"overrideAffected\":true}");
76+
77+
// A false value is never written, and an explicit false parses the same as an absent property.
78+
verifySerialize(EvaluationReason.off().withOverrideAffected(false), "{\"kind\":\"OFF\"}");
79+
verifyDeserialize(EvaluationReason.off(), "{\"kind\":\"OFF\",\"overrideAffected\":false}");
80+
}
81+
6282
@Test
6383
public void errorSerializationWithException() throws Exception {
6484
// We do *not* want the JSON representation to include the exception, because that is used in events, and

0 commit comments

Comments
 (0)