Skip to content

Commit

Permalink
feat: Adds support for client side prerequisite events. (#39)
Browse files Browse the repository at this point in the history
**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
  • Loading branch information
tanderson-ld authored Oct 24, 2024
1 parent 1af87f0 commit e9ea4df
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class TestService {
"event-sampling",
"inline-context",
"anonymous-redaction",
"evaluation-hooks"
"evaluation-hooks",
"client-prereq-events"
};

static final Gson gson = new GsonBuilder().serializeNulls().create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;

/**
Expand All @@ -30,6 +34,9 @@ final class EvalResult {
private final EvaluationDetail<String> asString;
private final boolean forceReasonTracking;

// A list of prerequisites evaluation records evaluated as part of obtaining this result.
private List<PrerequisiteEvalRecord> prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance

/**
* Constructs an instance that wraps the specified EvaluationDetail and also precomputes
* any appropriate type-specific variants (asBoolean, etc.).
Expand Down Expand Up @@ -100,6 +107,7 @@ private EvalResult(EvalResult from, EvaluationReason newReason) {
this.asDouble = transformReason(from.asDouble, newReason);
this.asString = transformReason(from.asString, newReason);
this.forceReasonTracking = from.forceReasonTracking;
this.prerequisiteEvalRecords = from.prerequisiteEvalRecords;
}

private EvalResult(EvalResult from, boolean newForceTracking) {
Expand All @@ -109,6 +117,17 @@ private EvalResult(EvalResult from, boolean newForceTracking) {
this.asDouble = from.asDouble;
this.asString = from.asString;
this.forceReasonTracking = newForceTracking;
this.prerequisiteEvalRecords = from.prerequisiteEvalRecords;
}

private EvalResult(EvalResult from, List<PrerequisiteEvalRecord> prerequisiteEvalRecords) {
this.anyType = from.anyType;
this.asBoolean = from.asBoolean;
this.asInteger = from.asInteger;
this.asDouble = from.asDouble;
this.asString = from.asString;
this.forceReasonTracking = from.forceReasonTracking;
this.prerequisiteEvalRecords = prerequisiteEvalRecords;
}

/**
Expand Down Expand Up @@ -208,6 +227,8 @@ public EvaluationDetail<String> getAsString() {
* @return true if reason tracking is required for this result
*/
public boolean isForceReasonTracking() { return forceReasonTracking; }

public List<PrerequisiteEvalRecord> getPrerequisiteEvalRecords() { return prerequisiteEvalRecords; }

/**
* Returns a transformed copy of this EvalResult with a different evaluation reason.
Expand All @@ -226,6 +247,10 @@ public EvalResult withReason(EvaluationReason newReason) {
public EvalResult withForceReasonTracking(boolean newValue) {
return this.forceReasonTracking == newValue ? this : new EvalResult(this, newValue);
}

public EvalResult withPrerequisiteEvalRecords(List<PrerequisiteEvalRecord> newValue) {
return this.prerequisiteEvalRecords == newValue ? this : new EvalResult(this, newValue);
}

@Override
public boolean equals(Object other) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ private static class EvaluatorState {
private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null;
private FeatureFlag originalFlag = null;
private List<String> prerequisiteStack = null;
private List<PrerequisiteEvalRecord> prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance
private List<String> segmentStack = null;
}

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

if (state.bigSegmentsStatus != null) {
return result.withReason(
result = result.withReason(
result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus)
);
}

if (state.prerequisiteEvalRecords != null && !state.prerequisiteEvalRecords.isEmpty()) {
result = result.withPrerequisiteEvalRecords(state.prerequisiteEvalRecords);
}

return result;
} catch (EvaluationException e) {
logger.error("Could not evaluate flag \"{}\": {}", flag.getKey(), e.getMessage());
return EvalResult.error(e.errorKind);
}
}

/**
* Internal evaluation function that may be called multiple times during a flag evaluation.
*
* @param flag that to evaluate
* @param context to use for evaluation
* @param recorder that will be used to record evaluation events
* @param state for mutable values needed during evaluation
* @return the evaluation result
*/
private EvalResult evaluateInternal(FeatureFlag flag, LDContext context, @Nonnull EvaluationRecorder recorder, EvaluatorState state) {
if (!flag.isOn()) {
return EvaluatorHelpers.offResult(flag);
Expand Down Expand Up @@ -237,6 +252,7 @@ private EvalResult checkPrerequisites(FeatureFlag flag, LDContext context, @Nonn
if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) {
prereqOk = false;
}
state.prerequisiteEvalRecords.add(new PrerequisiteEvalRecord(prereqFeatureFlag, flag, prereqEvalResult));
recorder.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, context, prereqEvalResult);
}
if (!prereqOk) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

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

Expand Down Expand Up @@ -48,18 +51,20 @@ static class FlagMetadata {
final boolean trackEvents;
final boolean trackReason;
final Long debugEventsUntilDate;
final List<String> prerequisites;

FlagMetadata(LDValue value, Integer variation, EvaluationReason reason, Integer version,
boolean trackEvents, boolean trackReason, Long debugEventsUntilDate) {
boolean trackEvents, boolean trackReason, Long debugEventsUntilDate, List<String> prerequisites) {
this.value = LDValue.normalize(value);
this.variation = variation;
this.reason = reason;
this.version = version;
this.trackEvents = trackEvents;
this.trackReason = trackReason;
this.debugEventsUntilDate = debugEventsUntilDate;
this.prerequisites = prerequisites;
}

@Override
public boolean equals(Object other) {
if (other instanceof FlagMetadata) {
Expand All @@ -70,14 +75,15 @@ public boolean equals(Object other) {
Objects.equals(version, o.version) &&
trackEvents == o.trackEvents &&
trackReason == o.trackReason &&
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate);
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate) &&
Objects.equals(prerequisites, o.prerequisites);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(variation, version, trackEvents, trackReason, debugEventsUntilDate);
return Objects.hash(value, variation, reason, version, trackEvents, trackReason, debugEventsUntilDate, prerequisites);
}
}

Expand Down Expand Up @@ -206,7 +212,8 @@ public Builder valid(boolean valid) {
* @param reason the evaluation reason
* @param flagVersion the current flag version
* @param trackEvents true if full event tracking is turned on for this flag
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
* @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation
* @return the builder
*/
public Builder add(
Expand All @@ -216,9 +223,10 @@ public Builder add(
EvaluationReason reason,
int flagVersion,
boolean trackEvents,
Long debugEventsUntilDate
Long debugEventsUntilDate,
List<String> prerequisites
) {
return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate);
return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate, prerequisites);
}

/**
Expand All @@ -236,7 +244,8 @@ public Builder add(
* @param flagVersion the current flag version
* @param trackEvents true if full event tracking is turned on for this flag
* @param trackReason true if evaluation reasons must be included due to experimentation
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
* @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp)
* @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation
* @return the builder
*/
public Builder add(
Expand All @@ -247,7 +256,8 @@ public Builder add(
int flagVersion,
boolean trackEvents,
boolean trackReason,
Long debugEventsUntilDate
Long debugEventsUntilDate,
List<String> prerequisites
) {
final boolean flagIsTracked = trackEvents ||
(debugEventsUntilDate != null && debugEventsUntilDate > System.currentTimeMillis());
Expand All @@ -259,8 +269,8 @@ public Builder add(
wantDetails ? Integer.valueOf(flagVersion) : null,
trackEvents,
trackReason,
debugEventsUntilDate
);
debugEventsUntilDate,
prerequisites);
flagMetadata.put(flagKey, data);
return this;
}
Expand All @@ -274,7 +284,11 @@ Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) {
flag.getVersion(),
flag.isTrackEvents() || eval.isForceReasonTracking(),
eval.isForceReasonTracking(),
flag.getDebugEventsUntilDate()
flag.getDebugEventsUntilDate(),
eval.getPrerequisiteEvalRecords().stream()
.filter(record -> record.prereqOfFlag.getKey() == flag.getKey()) // only include top level prereqs
.map(record -> record.flag.getKey()) // map from prereq record to prereq key
.collect(Collectors.toList())
);
}

Expand Down Expand Up @@ -331,6 +345,14 @@ public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
out.name("debugEventsUntilDate");
out.value(meta.debugEventsUntilDate.longValue());
}
if (meta.prerequisites != null && !meta.prerequisites.isEmpty()) {
out.name("prerequisites");
out.beginArray();
for (String s: meta.prerequisites) {
out.value(s);
}
out.endArray();
}
out.endObject();
}
out.endObject();
Expand Down Expand Up @@ -377,8 +399,8 @@ public FeatureFlagsState read(JsonReader in) throws IOException {
m0.version,
m0.trackEvents,
m0.trackReason,
m0.debugEventsUntilDate
);
m0.debugEventsUntilDate,
m0.prerequisites != null ? m0.prerequisites : new ArrayList<>(0));
allFlagMetadata.put(e.getKey(), m1);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.launchdarkly.sdk.server;

public class PrerequisiteEvalRecord {
public final DataModel.FeatureFlag flag;
public final DataModel.FeatureFlag prereqOfFlag;
public final EvalResult result;

public PrerequisiteEvalRecord(DataModel.FeatureFlag flag, DataModel.FeatureFlag prereqOfFlag, EvalResult result) {
this.flag = flag;
this.prereqOfFlag = prereqOfFlag;
this.result = result;
}
}
Loading

0 comments on commit e9ea4df

Please sign in to comment.