Skip to content

Commit e9ea4df

Browse files
authored
feat: Adds support for client side prerequisite events. (#39)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SDK-687
1 parent 1af87f0 commit e9ea4df

File tree

10 files changed

+251
-100
lines changed

10 files changed

+251
-100
lines changed

lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public class TestService {
3737
"event-sampling",
3838
"inline-context",
3939
"anonymous-redaction",
40-
"evaluation-hooks"
40+
"evaluation-hooks",
41+
"client-prereq-events"
4142
};
4243

4344
static final Gson gson = new GsonBuilder().serializeNulls().create();

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import com.launchdarkly.sdk.LDValue;
77
import com.launchdarkly.sdk.LDValueType;
88

9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.List;
12+
913
import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
1014

1115
/**
@@ -30,6 +34,9 @@ final class EvalResult {
3034
private final EvaluationDetail<String> asString;
3135
private final boolean forceReasonTracking;
3236

37+
// A list of prerequisites evaluation records evaluated as part of obtaining this result.
38+
private List<PrerequisiteEvalRecord> prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance
39+
3340
/**
3441
* Constructs an instance that wraps the specified EvaluationDetail and also precomputes
3542
* any appropriate type-specific variants (asBoolean, etc.).
@@ -100,6 +107,7 @@ private EvalResult(EvalResult from, EvaluationReason newReason) {
100107
this.asDouble = transformReason(from.asDouble, newReason);
101108
this.asString = transformReason(from.asString, newReason);
102109
this.forceReasonTracking = from.forceReasonTracking;
110+
this.prerequisiteEvalRecords = from.prerequisiteEvalRecords;
103111
}
104112

105113
private EvalResult(EvalResult from, boolean newForceTracking) {
@@ -109,6 +117,17 @@ private EvalResult(EvalResult from, boolean newForceTracking) {
109117
this.asDouble = from.asDouble;
110118
this.asString = from.asString;
111119
this.forceReasonTracking = newForceTracking;
120+
this.prerequisiteEvalRecords = from.prerequisiteEvalRecords;
121+
}
122+
123+
private EvalResult(EvalResult from, List<PrerequisiteEvalRecord> prerequisiteEvalRecords) {
124+
this.anyType = from.anyType;
125+
this.asBoolean = from.asBoolean;
126+
this.asInteger = from.asInteger;
127+
this.asDouble = from.asDouble;
128+
this.asString = from.asString;
129+
this.forceReasonTracking = from.forceReasonTracking;
130+
this.prerequisiteEvalRecords = prerequisiteEvalRecords;
112131
}
113132

114133
/**
@@ -208,6 +227,8 @@ public EvaluationDetail<String> getAsString() {
208227
* @return true if reason tracking is required for this result
209228
*/
210229
public boolean isForceReasonTracking() { return forceReasonTracking; }
230+
231+
public List<PrerequisiteEvalRecord> getPrerequisiteEvalRecords() { return prerequisiteEvalRecords; }
211232

212233
/**
213234
* Returns a transformed copy of this EvalResult with a different evaluation reason.
@@ -226,6 +247,10 @@ public EvalResult withReason(EvaluationReason newReason) {
226247
public EvalResult withForceReasonTracking(boolean newValue) {
227248
return this.forceReasonTracking == newValue ? this : new EvalResult(this, newValue);
228249
}
250+
251+
public EvalResult withPrerequisiteEvalRecords(List<PrerequisiteEvalRecord> newValue) {
252+
return this.prerequisiteEvalRecords == newValue ? this : new EvalResult(this, newValue);
253+
}
229254

230255
@Override
231256
public boolean equals(Object other) {

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ private static class EvaluatorState {
122122
private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null;
123123
private FeatureFlag originalFlag = null;
124124
private List<String> prerequisiteStack = null;
125+
private List<PrerequisiteEvalRecord> prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance
125126
private List<String> segmentStack = null;
126127
}
127128

@@ -150,17 +151,31 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco
150151
EvalResult result = evaluateInternal(flag, context, recorder, state);
151152

152153
if (state.bigSegmentsStatus != null) {
153-
return result.withReason(
154+
result = result.withReason(
154155
result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus)
155156
);
156157
}
158+
159+
if (state.prerequisiteEvalRecords != null && !state.prerequisiteEvalRecords.isEmpty()) {
160+
result = result.withPrerequisiteEvalRecords(state.prerequisiteEvalRecords);
161+
}
162+
157163
return result;
158164
} catch (EvaluationException e) {
159165
logger.error("Could not evaluate flag \"{}\": {}", flag.getKey(), e.getMessage());
160166
return EvalResult.error(e.errorKind);
161167
}
162168
}
163169

170+
/**
171+
* Internal evaluation function that may be called multiple times during a flag evaluation.
172+
*
173+
* @param flag that to evaluate
174+
* @param context to use for evaluation
175+
* @param recorder that will be used to record evaluation events
176+
* @param state for mutable values needed during evaluation
177+
* @return the evaluation result
178+
*/
164179
private EvalResult evaluateInternal(FeatureFlag flag, LDContext context, @Nonnull EvaluationRecorder recorder, EvaluatorState state) {
165180
if (!flag.isOn()) {
166181
return EvaluatorHelpers.offResult(flag);
@@ -237,6 +252,7 @@ private EvalResult checkPrerequisites(FeatureFlag flag, LDContext context, @Nonn
237252
if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) {
238253
prereqOk = false;
239254
}
255+
state.prerequisiteEvalRecords.add(new PrerequisiteEvalRecord(prereqFeatureFlag, flag, prereqEvalResult));
240256
recorder.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, context, prereqEvalResult);
241257
}
242258
if (!prereqOk) {

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
1313

1414
import java.io.IOException;
15+
import java.util.ArrayList;
1516
import java.util.HashMap;
17+
import java.util.List;
1618
import java.util.Map;
1719
import java.util.Objects;
20+
import java.util.stream.Collectors;
1821

1922
import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstanceWithNullsAllowed;
2023

@@ -48,18 +51,20 @@ static class FlagMetadata {
4851
final boolean trackEvents;
4952
final boolean trackReason;
5053
final Long debugEventsUntilDate;
54+
final List<String> prerequisites;
5155

5256
FlagMetadata(LDValue value, Integer variation, EvaluationReason reason, Integer version,
53-
boolean trackEvents, boolean trackReason, Long debugEventsUntilDate) {
57+
boolean trackEvents, boolean trackReason, Long debugEventsUntilDate, List<String> prerequisites) {
5458
this.value = LDValue.normalize(value);
5559
this.variation = variation;
5660
this.reason = reason;
5761
this.version = version;
5862
this.trackEvents = trackEvents;
5963
this.trackReason = trackReason;
6064
this.debugEventsUntilDate = debugEventsUntilDate;
65+
this.prerequisites = prerequisites;
6166
}
62-
67+
6368
@Override
6469
public boolean equals(Object other) {
6570
if (other instanceof FlagMetadata) {
@@ -70,14 +75,15 @@ public boolean equals(Object other) {
7075
Objects.equals(version, o.version) &&
7176
trackEvents == o.trackEvents &&
7277
trackReason == o.trackReason &&
73-
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate);
78+
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate) &&
79+
Objects.equals(prerequisites, o.prerequisites);
7480
}
7581
return false;
7682
}
77-
83+
7884
@Override
7985
public int hashCode() {
80-
return Objects.hash(variation, version, trackEvents, trackReason, debugEventsUntilDate);
86+
return Objects.hash(value, variation, reason, version, trackEvents, trackReason, debugEventsUntilDate, prerequisites);
8187
}
8288
}
8389

@@ -206,7 +212,8 @@ public Builder valid(boolean valid) {
206212
* @param reason the evaluation reason
207213
* @param flagVersion the current flag version
208214
* @param trackEvents true if full event tracking is turned on for this flag
209-
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
215+
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
216+
* @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation
210217
* @return the builder
211218
*/
212219
public Builder add(
@@ -216,9 +223,10 @@ public Builder add(
216223
EvaluationReason reason,
217224
int flagVersion,
218225
boolean trackEvents,
219-
Long debugEventsUntilDate
226+
Long debugEventsUntilDate,
227+
List<String> prerequisites
220228
) {
221-
return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate);
229+
return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate, prerequisites);
222230
}
223231

224232
/**
@@ -236,7 +244,8 @@ public Builder add(
236244
* @param flagVersion the current flag version
237245
* @param trackEvents true if full event tracking is turned on for this flag
238246
* @param trackReason true if evaluation reasons must be included due to experimentation
239-
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
247+
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
248+
* @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation
240249
* @return the builder
241250
*/
242251
public Builder add(
@@ -247,7 +256,8 @@ public Builder add(
247256
int flagVersion,
248257
boolean trackEvents,
249258
boolean trackReason,
250-
Long debugEventsUntilDate
259+
Long debugEventsUntilDate,
260+
List<String> prerequisites
251261
) {
252262
final boolean flagIsTracked = trackEvents ||
253263
(debugEventsUntilDate != null && debugEventsUntilDate > System.currentTimeMillis());
@@ -259,8 +269,8 @@ public Builder add(
259269
wantDetails ? Integer.valueOf(flagVersion) : null,
260270
trackEvents,
261271
trackReason,
262-
debugEventsUntilDate
263-
);
272+
debugEventsUntilDate,
273+
prerequisites);
264274
flagMetadata.put(flagKey, data);
265275
return this;
266276
}
@@ -274,7 +284,11 @@ Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) {
274284
flag.getVersion(),
275285
flag.isTrackEvents() || eval.isForceReasonTracking(),
276286
eval.isForceReasonTracking(),
277-
flag.getDebugEventsUntilDate()
287+
flag.getDebugEventsUntilDate(),
288+
eval.getPrerequisiteEvalRecords().stream()
289+
.filter(record -> record.prereqOfFlag.getKey() == flag.getKey()) // only include top level prereqs
290+
.map(record -> record.flag.getKey()) // map from prereq record to prereq key
291+
.collect(Collectors.toList())
278292
);
279293
}
280294

@@ -331,6 +345,14 @@ public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
331345
out.name("debugEventsUntilDate");
332346
out.value(meta.debugEventsUntilDate.longValue());
333347
}
348+
if (meta.prerequisites != null && !meta.prerequisites.isEmpty()) {
349+
out.name("prerequisites");
350+
out.beginArray();
351+
for (String s: meta.prerequisites) {
352+
out.value(s);
353+
}
354+
out.endArray();
355+
}
334356
out.endObject();
335357
}
336358
out.endObject();
@@ -377,8 +399,8 @@ public FeatureFlagsState read(JsonReader in) throws IOException {
377399
m0.version,
378400
m0.trackEvents,
379401
m0.trackReason,
380-
m0.debugEventsUntilDate
381-
);
402+
m0.debugEventsUntilDate,
403+
m0.prerequisites != null ? m0.prerequisites : new ArrayList<>(0));
382404
allFlagMetadata.put(e.getKey(), m1);
383405
}
384406
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
public class PrerequisiteEvalRecord {
4+
public final DataModel.FeatureFlag flag;
5+
public final DataModel.FeatureFlag prereqOfFlag;
6+
public final EvalResult result;
7+
8+
public PrerequisiteEvalRecord(DataModel.FeatureFlag flag, DataModel.FeatureFlag prereqOfFlag, EvalResult result) {
9+
this.flag = flag;
10+
this.prereqOfFlag = prereqOfFlag;
11+
this.result = result;
12+
}
13+
}

0 commit comments

Comments
 (0)