Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #82 from launchdarkly/eb/ch19787/explanations-in-e…
Browse files Browse the repository at this point in the history
…vents

include explanations, if requested, in full feature request events
  • Loading branch information
eli-darkly authored Jul 20, 2018
2 parents 742514e + ebfb18a commit fd1b8d9
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 57 deletions.
8 changes: 8 additions & 0 deletions src/main/java/com/launchdarkly/client/Components.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public abstract class Components {

/**
* Returns a factory for the default in-memory implementation of {@link FeatureStore}.
* @return a factory object
*/
public static FeatureStoreFactory inMemoryFeatureStore() {
return inMemoryFeatureStoreFactory;
Expand All @@ -26,6 +27,7 @@ public static FeatureStoreFactory inMemoryFeatureStore() {
/**
* Returns a factory with builder methods for creating a Redis-backed implementation of {@link FeatureStore},
* using {@link RedisFeatureStoreBuilder#DEFAULT_URI}.
* @return a factory/builder object
*/
public static RedisFeatureStoreBuilder redisFeatureStore() {
return new RedisFeatureStoreBuilder();
Expand All @@ -34,6 +36,8 @@ public static RedisFeatureStoreBuilder redisFeatureStore() {
/**
* Returns a factory with builder methods for creating a Redis-backed implementation of {@link FeatureStore},
* specifying the Redis URI.
* @param redisUri the URI of the Redis host
* @return a factory/builder object
*/
public static RedisFeatureStoreBuilder redisFeatureStore(URI redisUri) {
return new RedisFeatureStoreBuilder(redisUri);
Expand All @@ -43,6 +47,7 @@ public static RedisFeatureStoreBuilder redisFeatureStore(URI redisUri) {
* Returns a factory for the default implementation of {@link EventProcessor}, which
* forwards all analytics events to LaunchDarkly (unless the client is offline or you have
* set {@link LDConfig.Builder#sendEvents(boolean)} to {@code false}).
* @return a factory object
*/
public static EventProcessorFactory defaultEventProcessor() {
return defaultEventProcessorFactory;
Expand All @@ -51,6 +56,7 @@ public static EventProcessorFactory defaultEventProcessor() {
/**
* Returns a factory for a null implementation of {@link EventProcessor}, which will discard
* all analytics events and not send them to LaunchDarkly, regardless of any other configuration.
* @return a factory object
*/
public static EventProcessorFactory nullEventProcessor() {
return nullEventProcessorFactory;
Expand All @@ -60,6 +66,7 @@ public static EventProcessorFactory nullEventProcessor() {
* Returns a factory for the default implementation of {@link UpdateProcessor}, which receives
* feature flag data from LaunchDarkly using either streaming or polling as configured (or does
* nothing if the client is offline, or in LDD mode).
* @return a factory object
*/
public static UpdateProcessorFactory defaultUpdateProcessor() {
return defaultUpdateProcessorFactory;
Expand All @@ -68,6 +75,7 @@ public static UpdateProcessorFactory defaultUpdateProcessor() {
/**
* Returns a factory for a null implementation of {@link UpdateProcessor}, which does not
* connect to LaunchDarkly, regardless of any other configuration.
* @return a factory object
*/
public static UpdateProcessorFactory nullUpdateProcessor() {
return nullUpdateProcessorFactory;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/launchdarkly/client/EvaluationReason.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public static TargetMatch targetMatch() {

/**
* Returns an instance of {@link RuleMatch}.
* @param ruleIndex the rule index
* @param ruleId the rule identifier
* @return a reason object
*/
public static RuleMatch ruleMatch(int ruleIndex, String ruleId) {
Expand All @@ -118,6 +120,7 @@ public static RuleMatch ruleMatch(int ruleIndex, String ruleId) {

/**
* Returns an instance of {@link PrerequisitesFailed}.
* @param prerequisiteKeys the list of flag keys of prerequisites that failed
* @return a reason object
*/
public static PrerequisitesFailed prerequisitesFailed(Iterable<String> prerequisiteKeys) {
Expand All @@ -134,6 +137,7 @@ public static Fallthrough fallthrough() {

/**
* Returns an instance of {@link Error}.
* @param errorKind describes the type of error
* @return a reason object
*/
public static Error error(ErrorKind errorKind) {
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/launchdarkly/client/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,17 @@ public static final class FeatureRequest extends Event {
final String prereqOf;
final boolean trackEvents;
final Long debugEventsUntilDate;
final EvaluationReason reason;
final boolean debug;


@Deprecated
public FeatureRequest(long timestamp, String key, LDUser user, Integer version, Integer variation, JsonElement value,
JsonElement defaultVal, String prereqOf, boolean trackEvents, Long debugEventsUntilDate, boolean debug) {
this(timestamp, key, user, version, variation, value, defaultVal, prereqOf, trackEvents, debugEventsUntilDate, null, debug);
}

public FeatureRequest(long timestamp, String key, LDUser user, Integer version, Integer variation, JsonElement value,
JsonElement defaultVal, String prereqOf, boolean trackEvents, Long debugEventsUntilDate, EvaluationReason reason, boolean debug) {
super(timestamp, user);
this.key = key;
this.version = version;
Expand All @@ -59,6 +66,7 @@ public FeatureRequest(long timestamp, String key, LDUser user, Integer version,
this.prereqOf = prereqOf;
this.trackEvents = trackEvents;
this.debugEventsUntilDate = debugEventsUntilDate;
this.reason = reason;
this.debug = debug;
}
}
Expand Down
35 changes: 27 additions & 8 deletions src/main/java/com/launchdarkly/client/EventFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,43 @@
import com.google.gson.JsonElement;

abstract class EventFactory {
public static final EventFactory DEFAULT = new DefaultEventFactory();
public static final EventFactory DEFAULT = new DefaultEventFactory(false);
public static final EventFactory DEFAULT_WITH_REASONS = new DefaultEventFactory(true);

protected abstract long getTimestamp();
protected abstract boolean isIncludeReasons();

public Event.FeatureRequest newFeatureRequestEvent(FeatureFlag flag, LDUser user, EvaluationDetail<JsonElement> result, JsonElement defaultVal) {
return new Event.FeatureRequest(getTimestamp(), flag.getKey(), user, flag.getVersion(),
result == null ? null : result.getVariationIndex(), result == null ? null : result.getValue(),
defaultVal, null, flag.isTrackEvents(), flag.getDebugEventsUntilDate(), false);
defaultVal, null, flag.isTrackEvents(), flag.getDebugEventsUntilDate(),
isIncludeReasons() ? result.getReason() : null, false);
}

public Event.FeatureRequest newDefaultFeatureRequestEvent(FeatureFlag flag, LDUser user, JsonElement defaultValue) {
public Event.FeatureRequest newDefaultFeatureRequestEvent(FeatureFlag flag, LDUser user, JsonElement defaultValue,
EvaluationReason.ErrorKind errorKind) {
return new Event.FeatureRequest(getTimestamp(), flag.getKey(), user, flag.getVersion(),
null, defaultValue, defaultValue, null, flag.isTrackEvents(), flag.getDebugEventsUntilDate(), false);
null, defaultValue, defaultValue, null, flag.isTrackEvents(), flag.getDebugEventsUntilDate(),
isIncludeReasons() ? EvaluationReason.error(errorKind) : null, false);
}

public Event.FeatureRequest newUnknownFeatureRequestEvent(String key, LDUser user, JsonElement defaultValue) {
return new Event.FeatureRequest(getTimestamp(), key, user, null, null, defaultValue, defaultValue, null, false, null, false);
public Event.FeatureRequest newUnknownFeatureRequestEvent(String key, LDUser user, JsonElement defaultValue,
EvaluationReason.ErrorKind errorKind) {
return new Event.FeatureRequest(getTimestamp(), key, user, null, null, defaultValue, defaultValue, null, false, null,
isIncludeReasons() ? EvaluationReason.error(errorKind) : null, false);
}

public Event.FeatureRequest newPrerequisiteFeatureRequestEvent(FeatureFlag prereqFlag, LDUser user, EvaluationDetail<JsonElement> result,
FeatureFlag prereqOf) {
return new Event.FeatureRequest(getTimestamp(), prereqFlag.getKey(), user, prereqFlag.getVersion(),
result == null ? null : result.getVariationIndex(), result == null ? null : result.getValue(),
null, prereqOf.getKey(), prereqFlag.isTrackEvents(), prereqFlag.getDebugEventsUntilDate(), false);
null, prereqOf.getKey(), prereqFlag.isTrackEvents(), prereqFlag.getDebugEventsUntilDate(),
isIncludeReasons() ? result.getReason() : null, false);
}

public Event.FeatureRequest newDebugEvent(Event.FeatureRequest from) {
return new Event.FeatureRequest(from.creationDate, from.key, from.user, from.version, from.variation, from.value,
from.defaultVal, from.prereqOf, from.trackEvents, from.debugEventsUntilDate, true);
from.defaultVal, from.prereqOf, from.trackEvents, from.debugEventsUntilDate, from.reason, true);
}

public Event.Custom newCustomEvent(String key, LDUser user, JsonElement data) {
Expand All @@ -43,9 +51,20 @@ public Event.Identify newIdentifyEvent(LDUser user) {
}

public static class DefaultEventFactory extends EventFactory {
private final boolean includeReasons;

public DefaultEventFactory(boolean includeReasons) {
this.includeReasons = includeReasons;
}

@Override
protected long getTimestamp() {
return System.currentTimeMillis();
}

@Override
protected boolean isIncludeReasons() {
return includeReasons;
}
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/launchdarkly/client/EventOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ static final class FeatureRequest extends EventOutputWithTimestamp {
private final JsonElement value;
@SerializedName("default") private final JsonElement defaultVal;
private final String prereqOf;
private final EvaluationReason reason;

FeatureRequest(long creationDate, String key, String userKey, LDUser user,
Integer version, Integer variation, JsonElement value, JsonElement defaultVal, String prereqOf, boolean debug) {
Integer version, Integer variation, JsonElement value, JsonElement defaultVal, String prereqOf,
EvaluationReason reason, boolean debug) {
super(debug ? "debug" : "feature", creationDate);
this.key = key;
this.userKey = userKey;
Expand All @@ -56,6 +58,7 @@ static final class FeatureRequest extends EventOutputWithTimestamp {
this.value = value;
this.defaultVal = defaultVal;
this.prereqOf = prereqOf;
this.reason = reason;
}
}

Expand Down Expand Up @@ -163,7 +166,7 @@ private EventOutput createOutputEvent(Event e) {
return new EventOutput.FeatureRequest(fe.creationDate, fe.key,
inlineThisUser ? null : userKey,
inlineThisUser ? e.user : null,
fe.version, fe.variation, fe.value, fe.defaultVal, fe.prereqOf, fe.debug);
fe.version, fe.variation, fe.value, fe.defaultVal, fe.prereqOf, fe.reason, fe.debug);
} else if (e instanceof Event.Identify) {
return new EventOutput.Identify(e.creationDate, e.user);
} else if (e instanceof Event.Custom) {
Expand Down
58 changes: 37 additions & 21 deletions src/main/java/com/launchdarkly/client/LDClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public final class LDClient implements LDClientInterface {
final UpdateProcessor updateProcessor;
final FeatureStore featureStore;
final boolean shouldCloseFeatureStore;
private final EventFactory eventFactory = EventFactory.DEFAULT;


/**
* Creates a new client instance that connects to LaunchDarkly with the default configuration. In most
* cases, you should use this constructor.
Expand Down Expand Up @@ -116,7 +115,7 @@ public void track(String eventName, LDUser user, JsonElement data) {
if (user == null || user.getKey() == null) {
logger.warn("Track called with null user or null user key!");
}
eventProcessor.sendEvent(eventFactory.newCustomEvent(eventName, user, data));
eventProcessor.sendEvent(EventFactory.DEFAULT.newCustomEvent(eventName, user, data));
}

@Override
Expand All @@ -132,7 +131,7 @@ public void identify(LDUser user) {
if (user == null || user.getKey() == null) {
logger.warn("Identify called with null user or null user key!");
}
eventProcessor.sendEvent(eventFactory.newIdentifyEvent(user));
eventProcessor.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user));
}

private void sendFlagRequestEvent(Event.FeatureRequest event) {
Expand Down Expand Up @@ -165,9 +164,8 @@ public Map<String, JsonElement> allFlags(LDUser user) {

for (Map.Entry<String, FeatureFlag> entry : flags.entrySet()) {
try {
JsonElement evalResult = entry.getValue().evaluate(user, featureStore, eventFactory).getDetails().getValue();
result.put(entry.getKey(), evalResult);

JsonElement evalResult = entry.getValue().evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue();
result.put(entry.getKey(), evalResult);
} catch (EvaluationException e) {
logger.error("Exception caught when evaluating all flags:", e);
}
Expand Down Expand Up @@ -202,27 +200,32 @@ public JsonElement jsonVariation(String featureKey, LDUser user, JsonElement def

@Override
public EvaluationDetail<Boolean> boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) {
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Boolean);
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Boolean,
EventFactory.DEFAULT_WITH_REASONS);
}

@Override
public EvaluationDetail<Integer> intVariationDetail(String featureKey, LDUser user, int defaultValue) {
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Integer);
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Integer,
EventFactory.DEFAULT_WITH_REASONS);
}

@Override
public EvaluationDetail<Double> doubleVariationDetail(String featureKey, LDUser user, double defaultValue) {
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Double);
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Double,
EventFactory.DEFAULT_WITH_REASONS);
}

@Override
public EvaluationDetail<String> stringVariationDetail(String featureKey, LDUser user, String defaultValue) {
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.String);
return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.String,
EventFactory.DEFAULT_WITH_REASONS);
}

@Override
public EvaluationDetail<JsonElement> jsonVariationDetail(String featureKey, LDUser user, JsonElement defaultValue) {
return evaluateDetail(featureKey, user, defaultValue, defaultValue, VariationType.Json);
return evaluateDetail(featureKey, user, defaultValue, defaultValue, VariationType.Json,
EventFactory.DEFAULT_WITH_REASONS);
}

@Override
Expand All @@ -248,12 +251,12 @@ public boolean isFlagKnown(String featureKey) {
}

private <T> T evaluate(String featureKey, LDUser user, T defaultValue, JsonElement defaultJson, VariationType<T> expectedType) {
return evaluateDetail(featureKey, user, defaultValue, defaultJson, expectedType).getValue();
return evaluateDetail(featureKey, user, defaultValue, defaultJson, expectedType, EventFactory.DEFAULT).getValue();
}

private <T> EvaluationDetail<T> evaluateDetail(String featureKey, LDUser user, T defaultValue,
JsonElement defaultJson, VariationType<T> expectedType) {
EvaluationDetail<JsonElement> details = evaluateInternal(featureKey, user, defaultJson);
JsonElement defaultJson, VariationType<T> expectedType, EventFactory eventFactory) {
EvaluationDetail<JsonElement> details = evaluateInternal(featureKey, user, defaultJson, eventFactory);
T resultValue;
if (details.getReason().getKind() == EvaluationReason.Kind.ERROR) {
resultValue = defaultValue;
Expand All @@ -268,13 +271,14 @@ private <T> EvaluationDetail<T> evaluateDetail(String featureKey, LDUser user, T
return new EvaluationDetail<T>(details.getReason(), details.getVariationIndex(), resultValue);
}

private EvaluationDetail<JsonElement> evaluateInternal(String featureKey, LDUser user, JsonElement defaultValue) {
private EvaluationDetail<JsonElement> evaluateInternal(String featureKey, LDUser user, JsonElement defaultValue, EventFactory eventFactory) {
if (!initialized()) {
if (featureStore.initialized()) {
logger.warn("Evaluation called before client initialized for feature flag " + featureKey + "; using last known values from feature store");
} else {
logger.warn("Evaluation called before client initialized for feature flag " + featureKey + "; feature store unavailable, returning default value");
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.CLIENT_NOT_READY));
return EvaluationDetail.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY, defaultValue);
}
}
Expand All @@ -283,18 +287,29 @@ private EvaluationDetail<JsonElement> evaluateInternal(String featureKey, LDUser
FeatureFlag featureFlag = featureStore.get(FEATURES, featureKey);
if (featureFlag == null) {
logger.info("Unknown feature flag " + featureKey + "; returning default value");
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.FLAG_NOT_FOUND));
return EvaluationDetail.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND, defaultValue);
}
if (user == null || user.getKey() == null) {
logger.warn("Null user or null user key when evaluating flag: " + featureKey + "; returning default value");
sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue));
sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue,
EvaluationReason.ErrorKind.USER_NOT_SPECIFIED));
return EvaluationDetail.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, defaultValue);
}
if (user.getKeyAsString().isEmpty()) {
logger.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly");
}
FeatureFlag.EvalResult evalResult = featureFlag.evaluate(user, featureStore, eventFactory);
FeatureFlag.EvalResult evalResult;
try {
evalResult = featureFlag.evaluate(user, featureStore, eventFactory);
} catch (Exception e) {
logger.error("Encountered exception in LaunchDarkly client: " + e);
logger.debug(e.getMessage(), e);;
sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue,
EvaluationReason.ErrorKind.EXCEPTION));
return EvaluationDetail.error(EvaluationReason.ErrorKind.EXCEPTION, defaultValue);
}
for (Event.FeatureRequest event : evalResult.getPrerequisiteEvents()) {
eventProcessor.sendEvent(event);
}
Expand All @@ -303,7 +318,8 @@ private EvaluationDetail<JsonElement> evaluateInternal(String featureKey, LDUser
} catch (Exception e) {
logger.error("Encountered exception in LaunchDarkly client: " + e);
logger.debug(e.getMessage(), e);
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.EXCEPTION));
return EvaluationDetail.error(EvaluationReason.ErrorKind.EXCEPTION, defaultValue);
}
}
Expand Down
Loading

0 comments on commit fd1b8d9

Please sign in to comment.