Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds support for client side prerequisite events. #39

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,9 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;

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

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

/**
Expand All @@ -30,6 +33,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;

/**
* Constructs an instance that wraps the specified EvaluationDetail and also precomputes
* any appropriate type-specific variants (asBoolean, etc.).
Expand Down Expand Up @@ -100,6 +106,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 +116,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 +226,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 +246,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 = null;
private List<String> segmentStack = null;
}

Expand All @@ -145,22 +146,40 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco

EvaluatorState state = new EvaluatorState();
state.originalFlag = flag;
// allocate list capacity to avoid size increase during evaluation
state.prerequisiteEvalRecords = new ArrayList<>(); // TODO: optimize when this is used, shouldn't allocate for flag with no prereqs
tanderson-ld marked this conversation as resolved.
Show resolved Hide resolved

try {
EvalResult result = evaluateInternal(flag, context, recorder, state);

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

// TODO: these changes have reduced throughput, can we optimize this a bit. Perhaps by calling constructor
// with all parameters instead of using multiple calls in this immutable style
tanderson-ld marked this conversation as resolved.
Show resolved Hide resolved
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 +256,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 @@ -13,8 +13,10 @@

import java.io.IOException;
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 +50,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 +74,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 +211,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 +222,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 +243,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 +255,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 +268,8 @@ public Builder add(
wantDetails ? Integer.valueOf(flagVersion) : null,
trackEvents,
trackReason,
debugEventsUntilDate
);
debugEventsUntilDate,
prerequisites);
flagMetadata.put(flagKey, data);
return this;
}
Expand All @@ -274,7 +283,11 @@ Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) {
flag.getVersion(),
flag.isTrackEvents() || eval.isForceReasonTracking(),
eval.isForceReasonTracking(),
flag.getDebugEventsUntilDate()
flag.getDebugEventsUntilDate(),
eval.getPrerequisiteEvalRecords() == null ? null : 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 +344,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 +398,8 @@ public FeatureFlagsState read(JsonReader in) throws IOException {
m0.version,
m0.trackEvents,
m0.trackReason,
m0.debugEventsUntilDate
);
m0.debugEventsUntilDate,
m0.prerequisites);
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ abstract class Version {
private Version() {}

// This constant is updated automatically by our Gradle script during a release, if the project version has changed
// x-release-please-start-version
static final String SDK_VERSION = "7.5.0";
// x-release-please-end
}
Loading
Loading