variations = new ArrayList<>();
+ private boolean clientSide;
private boolean trackEvents;
private Long debugEventsUntilDate;
private boolean deleted;
@@ -37,13 +38,13 @@ class FeatureFlagBuilder {
this.fallthrough = f.getFallthrough();
this.offVariation = f.getOffVariation();
this.variations = f.getVariations();
+ this.clientSide = f.isClientSide();
this.trackEvents = f.isTrackEvents();
this.debugEventsUntilDate = f.getDebugEventsUntilDate();
this.deleted = f.isDeleted();
}
}
-
FeatureFlagBuilder version(int version) {
this.version = version;
return this;
@@ -93,6 +94,11 @@ FeatureFlagBuilder variations(JsonElement... variations) {
return variations(Arrays.asList(variations));
}
+ FeatureFlagBuilder clientSide(boolean clientSide) {
+ this.clientSide = clientSide;
+ return this;
+ }
+
FeatureFlagBuilder trackEvents(boolean trackEvents) {
this.trackEvents = trackEvents;
return this;
@@ -110,6 +116,6 @@ FeatureFlagBuilder deleted(boolean deleted) {
FeatureFlag build() {
return new FeatureFlag(key, version, on, prerequisites, salt, targets, rules, fallthrough, offVariation, variations,
- trackEvents, debugEventsUntilDate, deleted);
+ clientSide, trackEvents, debugEventsUntilDate, deleted);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/launchdarkly/client/FeatureFlagsState.java b/src/main/java/com/launchdarkly/client/FeatureFlagsState.java
new file mode 100644
index 000000000..97df3c5fc
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/FeatureFlagsState.java
@@ -0,0 +1,202 @@
+package com.launchdarkly.client;
+
+import com.google.common.base.Objects;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A snapshot of the state of all feature flags with regard to a specific user, generated by
+ * calling {@link LDClientInterface#allFlagsState(LDUser)}.
+ *
+ * Serializing this object to JSON using Gson will produce the appropriate data structure for
+ * bootstrapping the LaunchDarkly JavaScript client.
+ *
+ * @since 4.3.0
+ */
+@JsonAdapter(FeatureFlagsState.JsonSerialization.class)
+public class FeatureFlagsState {
+ private static final Gson gson = new Gson();
+
+ private final Map flagValues;
+ private final Map flagMetadata;
+ private final boolean valid;
+
+ static class FlagMetadata {
+ final Integer variation;
+ final EvaluationReason reason;
+ final int version;
+ final boolean trackEvents;
+ final Long debugEventsUntilDate;
+
+ FlagMetadata(Integer variation, EvaluationReason reason, int version, boolean trackEvents,
+ Long debugEventsUntilDate) {
+ this.variation = variation;
+ this.reason = reason;
+ this.version = version;
+ this.trackEvents = trackEvents;
+ this.debugEventsUntilDate = debugEventsUntilDate;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof FlagMetadata) {
+ FlagMetadata o = (FlagMetadata)other;
+ return Objects.equal(variation, o.variation) &&
+ version == o.version &&
+ trackEvents == o.trackEvents &&
+ Objects.equal(debugEventsUntilDate, o.debugEventsUntilDate);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(variation, version, trackEvents, debugEventsUntilDate);
+ }
+ }
+
+ private FeatureFlagsState(Map flagValues,
+ Map flagMetadata, boolean valid) {
+ this.flagValues = Collections.unmodifiableMap(flagValues);
+ this.flagMetadata = Collections.unmodifiableMap(flagMetadata);
+ this.valid = valid;
+ }
+
+ /**
+ * Returns true if this object contains a valid snapshot of feature flag state, or false if the
+ * state could not be computed (for instance, because the client was offline or there was no user).
+ * @return true if the state is valid
+ */
+ public boolean isValid() {
+ return valid;
+ }
+
+ /**
+ * Returns the value of an individual feature flag at the time the state was recorded.
+ * @param key the feature flag key
+ * @return the flag's JSON value; null if the flag returned the default value, or if there was no such flag
+ */
+ public JsonElement getFlagValue(String key) {
+ return flagValues.get(key);
+ }
+
+ /**
+ * Returns the evaluation reason for an individual feature flag at the time the state was recorded.
+ * @param key the feature flag key
+ * @return an {@link EvaluationReason}; null if reasons were not recorded, or if there was no such flag
+ */
+ public EvaluationReason getFlagReason(String key) {
+ FlagMetadata data = flagMetadata.get(key);
+ return data == null ? null : data.reason;
+ }
+
+ /**
+ * Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
+ * its value will be null.
+ *
+ * Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
+ * Instead, serialize the FeatureFlagsState object to JSON using {@code Gson.toJson()} or {@code Gson.toJsonTree()}.
+ * @return an immutable map of flag keys to JSON values
+ */
+ public Map toValuesMap() {
+ return flagValues;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof FeatureFlagsState) {
+ FeatureFlagsState o = (FeatureFlagsState)other;
+ return flagValues.equals(o.flagValues) &&
+ flagMetadata.equals(o.flagMetadata) &&
+ valid == o.valid;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(flagValues, flagMetadata, valid);
+ }
+
+ static class Builder {
+ private Map flagValues = new HashMap<>();
+ private Map flagMetadata = new HashMap<>();
+ private final boolean saveReasons;
+ private boolean valid = true;
+
+ Builder(FlagsStateOption... options) {
+ saveReasons = FlagsStateOption.hasOption(options, FlagsStateOption.WITH_REASONS);
+ }
+
+ Builder valid(boolean valid) {
+ this.valid = valid;
+ return this;
+ }
+
+ Builder addFlag(FeatureFlag flag, EvaluationDetail eval) {
+ flagValues.put(flag.getKey(), eval.getValue());
+ FlagMetadata data = new FlagMetadata(eval.getVariationIndex(),
+ saveReasons ? eval.getReason() : null,
+ flag.getVersion(), flag.isTrackEvents(), flag.getDebugEventsUntilDate());
+ flagMetadata.put(flag.getKey(), data);
+ return this;
+ }
+
+ FeatureFlagsState build() {
+ return new FeatureFlagsState(flagValues, flagMetadata, valid);
+ }
+ }
+
+ static class JsonSerialization extends TypeAdapter {
+ @Override
+ public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
+ out.beginObject();
+ for (Map.Entry entry: state.flagValues.entrySet()) {
+ out.name(entry.getKey());
+ gson.toJson(entry.getValue(), out);
+ }
+ out.name("$flagsState");
+ gson.toJson(state.flagMetadata, Map.class, out);
+ out.name("$valid");
+ out.value(state.valid);
+ out.endObject();
+ }
+
+ // There isn't really a use case for deserializing this, but we have to implement it
+ @Override
+ public FeatureFlagsState read(JsonReader in) throws IOException {
+ Map flagValues = new HashMap<>();
+ Map flagMetadata = new HashMap<>();
+ boolean valid = true;
+ in.beginObject();
+ while (in.hasNext()) {
+ String name = in.nextName();
+ if (name.equals("$flagsState")) {
+ in.beginObject();
+ while (in.hasNext()) {
+ String metaName = in.nextName();
+ FlagMetadata meta = gson.fromJson(in, FlagMetadata.class);
+ flagMetadata.put(metaName, meta);
+ }
+ in.endObject();
+ } else if (name.equals("$valid")) {
+ valid = in.nextBoolean();
+ } else {
+ JsonElement value = gson.fromJson(in, JsonElement.class);
+ flagValues.put(name, value);
+ }
+ }
+ in.endObject();
+ return new FeatureFlagsState(flagValues, flagMetadata, valid);
+ }
+ }
+}
diff --git a/src/main/java/com/launchdarkly/client/FlagsStateOption.java b/src/main/java/com/launchdarkly/client/FlagsStateOption.java
new file mode 100644
index 000000000..1cda057be
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/FlagsStateOption.java
@@ -0,0 +1,38 @@
+package com.launchdarkly.client;
+
+/**
+ * Optional parameters that can be passed to {@link LDClientInterface#allFlagsState(LDUser)}.
+ * @since 4.3.0
+ */
+public final class FlagsStateOption {
+ private final String description;
+
+ private FlagsStateOption(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+
+ /**
+ * Specifies that only flags marked for use with the client-side SDK should be included in the state object.
+ * By default, all flags are included.
+ */
+ public static final FlagsStateOption CLIENT_SIDE_ONLY = new FlagsStateOption("CLIENT_SIDE_ONLY");
+
+ /**
+ * Specifies that {@link EvaluationReason} data should be captured in the state object. By default, it is not.
+ */
+ public static final FlagsStateOption WITH_REASONS = new FlagsStateOption("WITH_REASONS");
+
+ static boolean hasOption(FlagsStateOption[] options, FlagsStateOption option) {
+ for (FlagsStateOption o: options) {
+ if (o == option) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java
index 9dba6eca0..67da07f3d 100644
--- a/src/main/java/com/launchdarkly/client/LDClient.java
+++ b/src/main/java/com/launchdarkly/client/LDClient.java
@@ -12,7 +12,6 @@
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -40,8 +39,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.
@@ -117,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
@@ -133,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) {
@@ -143,70 +141,109 @@ private void sendFlagRequestEvent(Event.FeatureRequest event) {
@Override
public Map allFlags(LDUser user) {
- if (isOffline()) {
- logger.debug("allFlags() was called when client is in offline mode.");
+ FeatureFlagsState state = allFlagsState(user);
+ if (!state.isValid()) {
+ return null;
}
+ return state.toValuesMap();
+ }
+ @Override
+ public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) {
+ FeatureFlagsState.Builder builder = new FeatureFlagsState.Builder(options);
+
+ if (isOffline()) {
+ logger.debug("allFlagsState() was called when client is in offline mode.");
+ }
+
if (!initialized()) {
if (featureStore.initialized()) {
- logger.warn("allFlags() was called before client initialized; using last known values from feature store");
+ logger.warn("allFlagsState() was called before client initialized; using last known values from feature store");
} else {
- logger.warn("allFlags() was called before client initialized; feature store unavailable, returning null");
- return null;
+ logger.warn("allFlagsState() was called before client initialized; feature store unavailable, returning no data");
+ return builder.valid(false).build();
}
}
if (user == null || user.getKey() == null) {
- logger.warn("allFlags() was called with null user or null user key! returning null");
- return null;
+ logger.warn("allFlagsState() was called with null user or null user key! returning no data");
+ return builder.valid(false).build();
}
+ boolean clientSideOnly = FlagsStateOption.hasOption(options, FlagsStateOption.CLIENT_SIDE_ONLY);
Map flags = featureStore.all(FEATURES);
- Map result = new HashMap<>();
-
for (Map.Entry entry : flags.entrySet()) {
+ FeatureFlag flag = entry.getValue();
+ if (clientSideOnly && !flag.isClientSide()) {
+ continue;
+ }
try {
- JsonElement evalResult = entry.getValue().evaluate(user, featureStore, eventFactory).getResult().getValue();
- result.put(entry.getKey(), evalResult);
-
- } catch (EvaluationException e) {
+ EvaluationDetail result = flag.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails();
+ builder.addFlag(flag, result);
+ } catch (Exception e) {
logger.error("Exception caught for feature flag \"{}\" when evaluating all flags: {}", entry.getKey(), e.toString());
logger.debug(e.toString(), e);
+ builder.addFlag(entry.getValue(), EvaluationDetail.error(EvaluationReason.ErrorKind.EXCEPTION, null));
}
}
- return result;
+ return builder.build();
}
-
+
@Override
public boolean boolVariation(String featureKey, LDUser user, boolean defaultValue) {
- JsonElement value = evaluate(featureKey, user, new JsonPrimitive(defaultValue), VariationType.Boolean);
- return value.getAsJsonPrimitive().getAsBoolean();
+ return evaluate(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Boolean);
}
@Override
public Integer intVariation(String featureKey, LDUser user, int defaultValue) {
- JsonElement value = evaluate(featureKey, user, new JsonPrimitive(defaultValue), VariationType.Integer);
- return value.getAsJsonPrimitive().getAsInt();
+ return evaluate(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Integer);
}
@Override
public Double doubleVariation(String featureKey, LDUser user, Double defaultValue) {
- JsonElement value = evaluate(featureKey, user, new JsonPrimitive(defaultValue), VariationType.Double);
- return value.getAsJsonPrimitive().getAsDouble();
+ return evaluate(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Double);
}
@Override
public String stringVariation(String featureKey, LDUser user, String defaultValue) {
- JsonElement value = evaluate(featureKey, user, new JsonPrimitive(defaultValue), VariationType.String);
- return value.getAsJsonPrimitive().getAsString();
+ return evaluate(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.String);
}
@Override
public JsonElement jsonVariation(String featureKey, LDUser user, JsonElement defaultValue) {
- JsonElement value = evaluate(featureKey, user, defaultValue, VariationType.Json);
- return value;
+ return evaluate(featureKey, user, defaultValue, defaultValue, VariationType.Json);
+ }
+
+ @Override
+ public EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) {
+ return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Boolean,
+ EventFactory.DEFAULT_WITH_REASONS);
}
+ @Override
+ public EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue) {
+ return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Integer,
+ EventFactory.DEFAULT_WITH_REASONS);
+ }
+
+ @Override
+ public EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue) {
+ return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.Double,
+ EventFactory.DEFAULT_WITH_REASONS);
+ }
+
+ @Override
+ public EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue) {
+ return evaluateDetail(featureKey, user, defaultValue, new JsonPrimitive(defaultValue), VariationType.String,
+ EventFactory.DEFAULT_WITH_REASONS);
+ }
+
+ @Override
+ public EvaluationDetail jsonVariationDetail(String featureKey, LDUser user, JsonElement defaultValue) {
+ return evaluateDetail(featureKey, user, defaultValue, defaultValue, VariationType.Json,
+ EventFactory.DEFAULT_WITH_REASONS);
+ }
+
@Override
public boolean isFlagKnown(String featureKey) {
if (!initialized()) {
@@ -230,14 +267,36 @@ public boolean isFlagKnown(String featureKey) {
return false;
}
- private JsonElement evaluate(String featureKey, LDUser user, JsonElement defaultValue, VariationType expectedType) {
+ private T evaluate(String featureKey, LDUser user, T defaultValue, JsonElement defaultJson, VariationType expectedType) {
+ return evaluateDetail(featureKey, user, defaultValue, defaultJson, expectedType, EventFactory.DEFAULT).getValue();
+ }
+
+ private EvaluationDetail evaluateDetail(String featureKey, LDUser user, T defaultValue,
+ JsonElement defaultJson, VariationType expectedType, EventFactory eventFactory) {
+ EvaluationDetail details = evaluateInternal(featureKey, user, defaultJson, eventFactory);
+ T resultValue = null;
+ if (details.getReason().getKind() == EvaluationReason.Kind.ERROR) {
+ resultValue = defaultValue;
+ } else if (details.getValue() != null) {
+ try {
+ resultValue = expectedType.coerceValue(details.getValue());
+ } catch (EvaluationException e) {
+ logger.error("Encountered exception in LaunchDarkly client: " + e);
+ return EvaluationDetail.error(EvaluationReason.ErrorKind.WRONG_TYPE, defaultValue);
+ }
+ }
+ return new EvaluationDetail(details.getReason(), details.getVariationIndex(), resultValue);
+ }
+
+ private EvaluationDetail evaluateInternal(String featureKey, LDUser user, JsonElement defaultValue, EventFactory eventFactory) {
if (!initialized()) {
if (featureStore.initialized()) {
logger.warn("Evaluation called before client initialized for feature flag \"{}\"; using last known values from feature store", featureKey);
} else {
logger.warn("Evaluation called before client initialized for feature flag \"{}\"; feature store unavailable, returning default value", featureKey);
- sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
- return defaultValue;
+ sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
+ EvaluationReason.ErrorKind.CLIENT_NOT_READY));
+ return EvaluationDetail.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY, defaultValue);
}
}
@@ -246,13 +305,15 @@ private JsonElement evaluate(String featureKey, LDUser user, JsonElement default
featureFlag = featureStore.get(FEATURES, featureKey);
if (featureFlag == null) {
logger.info("Unknown feature flag \"{}\"; returning default value", featureKey);
- sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
- return 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 \"{}\"; returning default value", featureKey);
- sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue));
- return 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");
@@ -261,23 +322,23 @@ private JsonElement evaluate(String featureKey, LDUser user, JsonElement default
for (Event.FeatureRequest event : evalResult.getPrerequisiteEvents()) {
eventProcessor.sendEvent(event);
}
- if (evalResult.getResult() != null && evalResult.getResult().getValue() != null) {
- expectedType.assertResultType(evalResult.getResult().getValue());
- sendFlagRequestEvent(eventFactory.newFeatureRequestEvent(featureFlag, user, evalResult.getResult(), defaultValue));
- return evalResult.getResult().getValue();
- } else {
- sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue));
- return defaultValue;
+ EvaluationDetail details = evalResult.getDetails();
+ if (details.isDefaultValue()) {
+ details = new EvaluationDetail(details.getReason(), null, defaultValue);
}
+ sendFlagRequestEvent(eventFactory.newFeatureRequestEvent(featureFlag, user, details, defaultValue));
+ return details;
} catch (Exception e) {
logger.error("Encountered exception while evaluating feature flag \"{}\": {}", featureKey, e.toString());
logger.debug(e.toString(), e);
if (featureFlag == null) {
- sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue));
+ sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
+ EvaluationReason.ErrorKind.EXCEPTION));
} else {
- sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue));
+ sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue,
+ EvaluationReason.ErrorKind.EXCEPTION));
}
- return defaultValue;
+ return EvaluationDetail.error(EvaluationReason.ErrorKind.EXCEPTION, defaultValue);
}
}
diff --git a/src/main/java/com/launchdarkly/client/LDClientInterface.java b/src/main/java/com/launchdarkly/client/LDClientInterface.java
index 94ee3f060..6385f474f 100644
--- a/src/main/java/com/launchdarkly/client/LDClientInterface.java
+++ b/src/main/java/com/launchdarkly/client/LDClientInterface.java
@@ -46,9 +46,28 @@ public interface LDClientInterface extends Closeable {
*
* @param user the end user requesting the feature flags
* @return a map from feature flag keys to {@code JsonElement} for the specified user
+ *
+ * @deprecated Use {@link #allFlagsState} instead. Current versions of the client-side SDK will not
+ * generate analytics events correctly if you pass the result of {@code allFlags()}.
*/
+ @Deprecated
Map allFlags(LDUser user);
+ /**
+ * Returns an object that encapsulates the state of all feature flags for a given user, including the flag
+ * values and also metadata that can be used on the front end. This method does not send analytics events
+ * back to LaunchDarkly.
+ *
+ * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
+ *
+ * @param user the end user requesting the feature flags
+ * @param options optional {@link FlagsStateOption} values affecting how the state is computed - for
+ * instance, to filter the set of flags to only include the client-side-enabled ones
+ * @return a {@link FeatureFlagsState} object (will never be null; see {@link FeatureFlagsState#isValid()}
+ * @since 4.3.0
+ */
+ FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options);
+
/**
* Calculates the value of a feature flag for a given user.
*
@@ -58,7 +77,7 @@ public interface LDClientInterface extends Closeable {
* @return whether or not the flag should be enabled, or {@code defaultValue} if the flag is disabled in the LaunchDarkly control panel
*/
boolean boolVariation(String featureKey, LDUser user, boolean defaultValue);
-
+
/**
* Calculates the integer value of a feature flag for a given user.
*
@@ -99,6 +118,66 @@ public interface LDClientInterface extends Closeable {
*/
JsonElement jsonVariation(String featureKey, LDUser user, JsonElement defaultValue);
+ /**
+ * Calculates the value of a feature flag for a given user, and returns an object that describes the
+ * way the value was determined. The {@code reason} property in the result will also be included in
+ * analytics events, if you are capturing detailed event data for this flag.
+ * @param featureKey the unique key for the feature flag
+ * @param user the end user requesting the flag
+ * @param defaultValue the default value of the flag
+ * @return an {@link EvaluationDetail} object
+ * @since 2.3.0
+ */
+ EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue);
+
+ /**
+ * Calculates the value of a feature flag for a given user, and returns an object that describes the
+ * way the value was determined. The {@code reason} property in the result will also be included in
+ * analytics events, if you are capturing detailed event data for this flag.
+ * @param featureKey the unique key for the feature flag
+ * @param user the end user requesting the flag
+ * @param defaultValue the default value of the flag
+ * @return an {@link EvaluationDetail} object
+ * @since 2.3.0
+ */
+ EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue);
+
+ /**
+ * Calculates the value of a feature flag for a given user, and returns an object that describes the
+ * way the value was determined. The {@code reason} property in the result will also be included in
+ * analytics events, if you are capturing detailed event data for this flag.
+ * @param featureKey the unique key for the feature flag
+ * @param user the end user requesting the flag
+ * @param defaultValue the default value of the flag
+ * @return an {@link EvaluationDetail} object
+ * @since 2.3.0
+ */
+ EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue);
+
+ /**
+ * Calculates the value of a feature flag for a given user, and returns an object that describes the
+ * way the value was determined. The {@code reason} property in the result will also be included in
+ * analytics events, if you are capturing detailed event data for this flag.
+ * @param featureKey the unique key for the feature flag
+ * @param user the end user requesting the flag
+ * @param defaultValue the default value of the flag
+ * @return an {@link EvaluationDetail} object
+ * @since 2.3.0
+ */
+ EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue);
+
+ /**
+ * Calculates the value of a feature flag for a given user, and returns an object that describes the
+ * way the value was determined. The {@code reason} property in the result will also be included in
+ * analytics events, if you are capturing detailed event data for this flag.
+ * @param featureKey the unique key for the feature flag
+ * @param user the end user requesting the flag
+ * @param defaultValue the default value of the flag
+ * @return an {@link EvaluationDetail} object
+ * @since 2.3.0
+ */
+ EvaluationDetail jsonVariationDetail(String featureKey, LDUser user, JsonElement defaultValue);
+
/**
* Returns true if the specified feature flag currently exists.
* @param featureKey the unique key for the feature flag
diff --git a/src/main/java/com/launchdarkly/client/LDUser.java b/src/main/java/com/launchdarkly/client/LDUser.java
index adde857bc..6dc26e836 100644
--- a/src/main/java/com/launchdarkly/client/LDUser.java
+++ b/src/main/java/com/launchdarkly/client/LDUser.java
@@ -1,16 +1,24 @@
package com.launchdarkly.client;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import com.google.gson.*;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.TypeAdapter;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
-import com.google.common.collect.ImmutableMap;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.util.*;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.regex.Pattern;
/**
@@ -55,8 +63,8 @@ protected LDUser(Builder builder) {
this.name = builder.name == null ? null : new JsonPrimitive(builder.name);
this.avatar = builder.avatar == null ? null : new JsonPrimitive(builder.avatar);
this.anonymous = builder.anonymous == null ? null : new JsonPrimitive(builder.anonymous);
- this.custom = new HashMap<>(builder.custom);
- this.privateAttributeNames = new HashSet<>(builder.privateAttrNames);
+ this.custom = ImmutableMap.copyOf(builder.custom);
+ this.privateAttributeNames = ImmutableSet.copyOf(builder.privateAttrNames);
}
/**
@@ -350,8 +358,8 @@ public Builder(LDUser user) {
this.avatar = user.getAvatar() != null ? user.getAvatar().getAsString() : null;
this.anonymous = user.getAnonymous() != null ? user.getAnonymous().getAsBoolean() : null;
this.country = user.getCountry() != null ? LDCountryCode.valueOf(user.getCountry().getAsString()) : null;
- this.custom = ImmutableMap.copyOf(user.custom);
- this.privateAttrNames = ImmutableSet.copyOf(user.privateAttributeNames);
+ this.custom = new HashMap<>(user.custom);
+ this.privateAttrNames = new HashSet<>(user.privateAttributeNames);
}
/**
diff --git a/src/main/java/com/launchdarkly/client/Rule.java b/src/main/java/com/launchdarkly/client/Rule.java
index 6009abc74..cee3d7ae0 100644
--- a/src/main/java/com/launchdarkly/client/Rule.java
+++ b/src/main/java/com/launchdarkly/client/Rule.java
@@ -8,6 +8,7 @@
* Invariant: one of the variation or rollout must be non-nil.
*/
class Rule extends VariationOrRollout {
+ private String id;
private List clauses;
// We need this so Gson doesn't complain in certain java environments that restrict unsafe allocation
@@ -15,11 +16,16 @@ class Rule extends VariationOrRollout {
super();
}
- Rule(List clauses, Integer variation, Rollout rollout) {
+ Rule(String id, List clauses, Integer variation, Rollout rollout) {
super(variation, rollout);
+ this.id = id;
this.clauses = clauses;
}
+ String getId() {
+ return id;
+ }
+
boolean matchesUser(FeatureStore store, LDUser user) {
for (Clause clause : clauses) {
if (!clause.matchesUser(store, user)) {
diff --git a/src/main/java/com/launchdarkly/client/TestFeatureStore.java b/src/main/java/com/launchdarkly/client/TestFeatureStore.java
index afebbad2f..39d20e7cd 100644
--- a/src/main/java/com/launchdarkly/client/TestFeatureStore.java
+++ b/src/main/java/com/launchdarkly/client/TestFeatureStore.java
@@ -32,6 +32,7 @@ public class TestFeatureStore extends InMemoryFeatureStore {
*
* @param key the key of the feature flag
* @param value the new value of the feature flag
+ * @return the feature flag
*/
public FeatureFlag setBooleanValue(String key, Boolean value) {
FeatureFlag newFeature = new FeatureFlagBuilder(key)
@@ -49,6 +50,7 @@ public FeatureFlag setBooleanValue(String key, Boolean value) {
* If the feature rule is not currently in the store, it will create one that is true for every {@link LDUser}.
*
* @param key the key of the feature flag to evaluate to true
+ * @return the feature flag
*/
public FeatureFlag setFeatureTrue(String key) {
return setBooleanValue(key, true);
@@ -59,6 +61,7 @@ public FeatureFlag setFeatureTrue(String key) {
* If the feature rule is not currently in the store, it will create one that is false for every {@link LDUser}.
*
* @param key the key of the feature flag to evaluate to false
+ * @return the feature flag
*/
public FeatureFlag setFeatureFalse(String key) {
return setBooleanValue(key, false);
@@ -68,6 +71,7 @@ public FeatureFlag setFeatureFalse(String key) {
* Sets the value of an integer multivariate feature flag, for all users.
* @param key the key of the flag
* @param value the new value of the flag
+ * @return the feature flag
*/
public FeatureFlag setIntegerValue(String key, Integer value) {
return setJsonValue(key, new JsonPrimitive(value));
@@ -77,6 +81,7 @@ public FeatureFlag setIntegerValue(String key, Integer value) {
* Sets the value of a double multivariate feature flag, for all users.
* @param key the key of the flag
* @param value the new value of the flag
+ * @return the feature flag
*/
public FeatureFlag setDoubleValue(String key, Double value) {
return setJsonValue(key, new JsonPrimitive(value));
@@ -86,6 +91,7 @@ public FeatureFlag setDoubleValue(String key, Double value) {
* Sets the value of a string multivariate feature flag, for all users.
* @param key the key of the flag
* @param value the new value of the flag
+ * @return the feature flag
*/
public FeatureFlag setStringValue(String key, String value) {
return setJsonValue(key, new JsonPrimitive(value));
@@ -95,6 +101,7 @@ public FeatureFlag setStringValue(String key, String value) {
* Sets the value of a JsonElement multivariate feature flag, for all users.
* @param key the key of the flag
* @param value the new value of the flag
+ * @return the feature flag
*/
public FeatureFlag setJsonValue(String key, JsonElement value) {
FeatureFlag newFeature = new FeatureFlagBuilder(key)
diff --git a/src/main/java/com/launchdarkly/client/VariationOrRollout.java b/src/main/java/com/launchdarkly/client/VariationOrRollout.java
index 56537e498..41f58a676 100644
--- a/src/main/java/com/launchdarkly/client/VariationOrRollout.java
+++ b/src/main/java/com/launchdarkly/client/VariationOrRollout.java
@@ -24,6 +24,8 @@ class VariationOrRollout {
this.rollout = rollout;
}
+ // Attempt to determine the variation index for a given user. Returns null if no index can be computed
+ // due to internal inconsistency of the data (i.e. a malformed flag).
Integer variationIndexForUser(LDUser user, String key, String salt) {
if (variation != null) {
return variation;
diff --git a/src/main/java/com/launchdarkly/client/VariationType.java b/src/main/java/com/launchdarkly/client/VariationType.java
index 8d8228f1b..c222d57a4 100644
--- a/src/main/java/com/launchdarkly/client/VariationType.java
+++ b/src/main/java/com/launchdarkly/client/VariationType.java
@@ -3,48 +3,51 @@
import com.google.gson.JsonElement;
-enum VariationType {
- Boolean {
- @Override
- void assertResultType(JsonElement result) throws EvaluationException {
+abstract class VariationType {
+ abstract T coerceValue(JsonElement result) throws EvaluationException;
+
+ private VariationType() {
+ }
+
+ static VariationType Boolean = new VariationType() {
+ Boolean coerceValue(JsonElement result) throws EvaluationException {
if (result.isJsonPrimitive() && result.getAsJsonPrimitive().isBoolean()) {
- return;
+ return result.getAsBoolean();
}
throw new EvaluationException("Feature flag evaluation expected result as boolean type, but got non-boolean type.");
}
- },
- Integer {
- @Override
- void assertResultType(JsonElement result) throws EvaluationException {
+ };
+
+ static VariationType Integer = new VariationType() {
+ Integer coerceValue(JsonElement result) throws EvaluationException {
if (result.isJsonPrimitive() && result.getAsJsonPrimitive().isNumber()) {
- return;
+ return result.getAsInt();
}
throw new EvaluationException("Feature flag evaluation expected result as number type, but got non-number type.");
}
- },
- Double {
- @Override
- void assertResultType(JsonElement result) throws EvaluationException {
+ };
+
+ static VariationType Double = new VariationType() {
+ Double coerceValue(JsonElement result) throws EvaluationException {
if (result.isJsonPrimitive() && result.getAsJsonPrimitive().isNumber()) {
- return;
+ return result.getAsDouble();
}
throw new EvaluationException("Feature flag evaluation expected result as number type, but got non-number type.");
}
- },
- String {
- @Override
- void assertResultType(JsonElement result) throws EvaluationException {
+ };
+
+ static VariationType String = new VariationType() {
+ String coerceValue(JsonElement result) throws EvaluationException {
if (result.isJsonPrimitive() && result.getAsJsonPrimitive().isString()) {
- return;
+ return result.getAsString();
}
throw new EvaluationException("Feature flag evaluation expected result as string type, but got non-string type.");
}
- },
- Json {
- @Override
- void assertResultType(JsonElement result) {
+ };
+
+ static VariationType Json = new VariationType() {
+ JsonElement coerceValue(JsonElement result) throws EvaluationException {
+ return result;
}
};
-
- abstract void assertResultType(JsonElement result) throws EvaluationException;
}
diff --git a/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java b/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
index 36bc2097e..c8184c38c 100644
--- a/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
+++ b/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
@@ -17,6 +17,7 @@
import static com.launchdarkly.client.TestUtil.hasJsonProperty;
import static com.launchdarkly.client.TestUtil.isJsonArray;
+import static com.launchdarkly.client.TestUtil.simpleEvaluation;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
@@ -87,7 +88,7 @@ public void individualFeatureEventIsQueuedWithIndexEvent() throws Exception {
ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -105,7 +106,7 @@ public void userIsFilteredInIndexEvent() throws Exception {
ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -123,7 +124,7 @@ public void featureEventCanContainInlineUser() throws Exception {
ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -140,7 +141,7 @@ public void userIsFilteredInFeatureEvent() throws Exception {
ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -150,6 +151,24 @@ public void userIsFilteredInFeatureEvent() throws Exception {
));
}
+ @SuppressWarnings("unchecked")
+ @Test
+ public void featureEventCanContainReason() throws Exception {
+ ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
+ FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true).build();
+ EvaluationReason reason = EvaluationReason.ruleMatch(1, null);
+ Event.FeatureRequest fe = EventFactory.DEFAULT_WITH_REASONS.newFeatureRequestEvent(flag, user,
+ new EvaluationDetail(reason, 1, new JsonPrimitive("value")), null);
+ ep.sendEvent(fe);
+
+ JsonArray output = flushAndGetEvents(new MockResponse());
+ assertThat(output, hasItems(
+ isIndexEvent(fe, userJson),
+ isFeatureEvent(fe, flag, false, null, reason),
+ isSummaryEvent()
+ ));
+ }
+
@SuppressWarnings("unchecked")
@Test
public void indexEventIsStillGeneratedIfInlineUsersIsTrueButFeatureEventIsNotTracked() throws Exception {
@@ -157,7 +176,7 @@ public void indexEventIsStillGeneratedIfInlineUsersIsTrueButFeatureEventIsNotTra
ep = new DefaultEventProcessor(SDK_KEY, configBuilder.build());
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(false).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -174,7 +193,7 @@ public void eventKindIsDebugIfFlagIsTemporarilyInDebugMode() throws Exception {
long futureTime = System.currentTimeMillis() + 1000000;
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).debugEventsUntilDate(futureTime).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -193,7 +212,7 @@ public void eventCanBeBothTrackedAndDebugged() throws Exception {
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).trackEvents(true)
.debugEventsUntilDate(futureTime).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
JsonArray output = flushAndGetEvents(new MockResponse());
@@ -222,7 +241,7 @@ public void debugModeExpiresBasedOnClientTimeIfClientTimeIsLaterThanServerTime()
long debugUntil = serverTime + 1000;
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).debugEventsUntilDate(debugUntil).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
// Should get a summary event only, not a full feature event
@@ -250,7 +269,7 @@ public void debugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime()
long debugUntil = serverTime - 1000;
FeatureFlag flag = new FeatureFlagBuilder("flagkey").version(11).debugEventsUntilDate(debugUntil).build();
Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user,
- new FeatureFlag.VariationAndValue(new Integer(1), new JsonPrimitive("value")), null);
+ simpleEvaluation(1, new JsonPrimitive("value")), null);
ep.sendEvent(fe);
// Should get a summary event only, not a full feature event
@@ -269,9 +288,9 @@ public void twoFeatureEventsForSameUserGenerateOnlyOneIndexEvent() throws Except
FeatureFlag flag2 = new FeatureFlagBuilder("flagkey2").version(22).trackEvents(true).build();
JsonElement value = new JsonPrimitive("value");
Event.FeatureRequest fe1 = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user,
- new FeatureFlag.VariationAndValue(new Integer(1), value), null);
+ simpleEvaluation(1, value), null);
Event.FeatureRequest fe2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag2, user,
- new FeatureFlag.VariationAndValue(new Integer(1), value), null);
+ simpleEvaluation(1, value), null);
ep.sendEvent(fe1);
ep.sendEvent(fe2);
@@ -294,9 +313,9 @@ public void nonTrackedEventsAreSummarized() throws Exception {
JsonElement default1 = new JsonPrimitive("default1");
JsonElement default2 = new JsonPrimitive("default2");
Event fe1 = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user,
- new FeatureFlag.VariationAndValue(new Integer(2), value), default1);
+ simpleEvaluation(2, value), default1);
Event fe2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag2, user,
- new FeatureFlag.VariationAndValue(new Integer(2), value), default2);
+ simpleEvaluation(2, value), default2);
ep.sendEvent(fe1);
ep.sendEvent(fe2);
@@ -493,8 +512,13 @@ private Matcher isIndexEvent(Event sourceEvent, JsonElement user) {
);
}
- @SuppressWarnings("unchecked")
private Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, FeatureFlag flag, boolean debug, JsonElement inlineUser) {
+ return isFeatureEvent(sourceEvent, flag, debug, inlineUser, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, FeatureFlag flag, boolean debug, JsonElement inlineUser,
+ EvaluationReason reason) {
return allOf(
hasJsonProperty("kind", debug ? "debug" : "feature"),
hasJsonProperty("creationDate", (double)sourceEvent.creationDate),
@@ -505,7 +529,9 @@ private Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, Fe
(inlineUser != null) ? hasJsonProperty("userKey", nullValue(JsonElement.class)) :
hasJsonProperty("userKey", sourceEvent.user.getKeyAsString()),
(inlineUser != null) ? hasJsonProperty("user", inlineUser) :
- hasJsonProperty("user", nullValue(JsonElement.class))
+ hasJsonProperty("user", nullValue(JsonElement.class)),
+ (reason == null) ? hasJsonProperty("reason", nullValue(JsonElement.class)) :
+ hasJsonProperty("reason", gson.toJsonTree(reason))
);
}
diff --git a/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java b/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java
new file mode 100644
index 000000000..41133c46b
--- /dev/null
+++ b/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java
@@ -0,0 +1,66 @@
+package com.launchdarkly.client;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class EvaluationReasonTest {
+ private static final Gson gson = new Gson();
+
+ @Test
+ public void testOffReasonSerialization() {
+ EvaluationReason reason = EvaluationReason.off();
+ String json = "{\"kind\":\"OFF\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("OFF", reason.toString());
+ }
+
+ @Test
+ public void testFallthroughSerialization() {
+ EvaluationReason reason = EvaluationReason.fallthrough();
+ String json = "{\"kind\":\"FALLTHROUGH\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("FALLTHROUGH", reason.toString());
+ }
+
+ @Test
+ public void testTargetMatchSerialization() {
+ EvaluationReason reason = EvaluationReason.targetMatch();
+ String json = "{\"kind\":\"TARGET_MATCH\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("TARGET_MATCH", reason.toString());
+ }
+
+ @Test
+ public void testRuleMatchSerialization() {
+ EvaluationReason reason = EvaluationReason.ruleMatch(1, "id");
+ String json = "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("RULE_MATCH(1,id)", reason.toString());
+ }
+
+ @Test
+ public void testPrerequisiteFailedSerialization() {
+ EvaluationReason reason = EvaluationReason.prerequisiteFailed("key");
+ String json = "{\"kind\":\"PREREQUISITE_FAILED\",\"prerequisiteKey\":\"key\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("PREREQUISITE_FAILED(key)", reason.toString());
+ }
+
+ @Test
+ public void testErrorSerialization() {
+ EvaluationReason reason = EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION);
+ String json = "{\"kind\":\"ERROR\",\"errorKind\":\"EXCEPTION\"}";
+ assertJsonEqual(json, gson.toJson(reason));
+ assertEquals("ERROR(EXCEPTION)", reason.toString());
+ }
+
+ private void assertJsonEqual(String expectedString, String actualString) {
+ JsonElement expected = gson.fromJson(expectedString, JsonElement.class);
+ JsonElement actual = gson.fromJson(actualString, JsonElement.class);
+ assertEquals(expected, actual);
+ }
+}
diff --git a/src/test/java/com/launchdarkly/client/EventSummarizerTest.java b/src/test/java/com/launchdarkly/client/EventSummarizerTest.java
index 6a7a83875..f64ba29bd 100644
--- a/src/test/java/com/launchdarkly/client/EventSummarizerTest.java
+++ b/src/test/java/com/launchdarkly/client/EventSummarizerTest.java
@@ -6,6 +6,7 @@
import java.util.Map;
import static com.launchdarkly.client.TestUtil.js;
+import static com.launchdarkly.client.TestUtil.simpleEvaluation;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
@@ -19,6 +20,11 @@ public class EventSummarizerTest {
protected long getTimestamp() {
return eventTimestamp;
}
+
+ @Override
+ protected boolean isIncludeReasons() {
+ return false;
+ }
};
@Test
@@ -65,14 +71,14 @@ public void summarizeEventIncrementsCounters() {
FeatureFlag flag2 = new FeatureFlagBuilder("key2").version(22).build();
String unknownFlagKey = "badkey";
Event event1 = eventFactory.newFeatureRequestEvent(flag1, user,
- new FeatureFlag.VariationAndValue(1, js("value1")), js("default1"));
+ simpleEvaluation(1, js("value1")), js("default1"));
Event event2 = eventFactory.newFeatureRequestEvent(flag1, user,
- new FeatureFlag.VariationAndValue(2, js("value2")), js("default1"));
+ simpleEvaluation(2, js("value2")), js("default1"));
Event event3 = eventFactory.newFeatureRequestEvent(flag2, user,
- new FeatureFlag.VariationAndValue(1, js("value99")), js("default2"));
+ simpleEvaluation(1, js("value99")), js("default2"));
Event event4 = eventFactory.newFeatureRequestEvent(flag1, user,
- new FeatureFlag.VariationAndValue(1, js("value1")), js("default1"));
- Event event5 = eventFactory.newUnknownFeatureRequestEvent(unknownFlagKey, user, js("default3"));
+ simpleEvaluation(1, js("value1")), js("default1"));
+ Event event5 = eventFactory.newUnknownFeatureRequestEvent(unknownFlagKey, user, js("default3"), EvaluationReason.ErrorKind.FLAG_NOT_FOUND);
es.summarizeEvent(event1);
es.summarizeEvent(event2);
es.summarizeEvent(event3);
diff --git a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
index 70bc94c9f..6ba5317b5 100644
--- a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
+++ b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
@@ -1,5 +1,6 @@
package com.launchdarkly.client;
+import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
@@ -17,7 +18,6 @@
import static com.launchdarkly.client.VersionedDataKind.SEGMENTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
public class FeatureFlagTest {
@@ -40,7 +40,7 @@ public void flagReturnsOffVariationIfFlagIsOff() throws Exception {
.build();
FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertEquals(js("off"), result.getResult().getValue());
+ assertEquals(new EvaluationDetail<>(EvaluationReason.off(), 1, js("off")), result.getDetails());
assertEquals(0, result.getPrerequisiteEvents().size());
}
@@ -53,7 +53,106 @@ public void flagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() throws Exce
.build();
FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertNull(result.getResult().getValue());
+ assertEquals(new EvaluationDetail<>(EvaluationReason.off(), null, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFlagIsOffAndOffVariationIsTooHigh() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(false)
+ .offVariation(999)
+ .fallthrough(fallthroughVariation(0))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFlagIsOffAndOffVariationIsNegative() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(false)
+ .offVariation(-1)
+ .fallthrough(fallthroughVariation(0))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsFallthroughIfFlagIsOnAndThereAreNoRules() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(true)
+ .offVariation(1)
+ .fallthrough(fallthroughVariation(0))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(new EvaluationDetail<>(EvaluationReason.fallthrough(), 0, js("fall")), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFallthroughHasTooHighVariation() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(true)
+ .offVariation(1)
+ .fallthrough(fallthroughVariation(999))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFallthroughHasNegativeVariation() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(true)
+ .offVariation(1)
+ .fallthrough(fallthroughVariation(-1))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFallthroughHasNeitherVariationNorRollout() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(true)
+ .offVariation(1)
+ .fallthrough(new VariationOrRollout(null, null))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagReturnsErrorIfFallthroughHasEmptyRolloutVariationList() throws Exception {
+ FeatureFlag f = new FeatureFlagBuilder("feature")
+ .on(true)
+ .offVariation(1)
+ .fallthrough(new VariationOrRollout(null,
+ new VariationOrRollout.Rollout(ImmutableList.of(), null)))
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ FeatureFlag.EvalResult result = f.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
assertEquals(0, result.getPrerequisiteEvents().size());
}
@@ -68,7 +167,8 @@ public void flagReturnsOffVariationIfPrerequisiteIsNotFound() throws Exception {
.build();
FeatureFlag.EvalResult result = f0.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertEquals(js("off"), result.getResult().getValue());
+ EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1");
+ assertEquals(new EvaluationDetail<>(expectedReason, 1, js("off")), result.getDetails());
assertEquals(0, result.getPrerequisiteEvents().size());
}
@@ -91,7 +191,8 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() throws Excep
featureStore.upsert(FEATURES, f1);
FeatureFlag.EvalResult result = f0.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertEquals(js("off"), result.getResult().getValue());
+ EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1");
+ assertEquals(new EvaluationDetail<>(expectedReason, 1, js("off")), result.getDetails());
assertEquals(1, result.getPrerequisiteEvents().size());
Event.FeatureRequest event = result.getPrerequisiteEvents().get(0);
@@ -120,7 +221,7 @@ public void flagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAr
featureStore.upsert(FEATURES, f1);
FeatureFlag.EvalResult result = f0.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertEquals(js("fall"), result.getResult().getValue());
+ assertEquals(new EvaluationDetail<>(EvaluationReason.fallthrough(), 0, js("fall")), result.getDetails());
assertEquals(1, result.getPrerequisiteEvents().size());
Event.FeatureRequest event = result.getPrerequisiteEvents().get(0);
@@ -157,7 +258,7 @@ public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exceptio
featureStore.upsert(FEATURES, f2);
FeatureFlag.EvalResult result = f0.evaluate(BASE_USER, featureStore, EventFactory.DEFAULT);
- assertEquals(js("fall"), result.getResult().getValue());
+ assertEquals(new EvaluationDetail<>(EvaluationReason.fallthrough(), 0, js("fall")), result.getDetails());
assertEquals(2, result.getPrerequisiteEvents().size());
Event.FeatureRequest event0 = result.getPrerequisiteEvents().get(0);
@@ -185,62 +286,107 @@ public void flagMatchesUserFromTargets() throws Exception {
LDUser user = new LDUser.Builder("userkey").build();
FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
- assertEquals(js("on"), result.getResult().getValue());
+ assertEquals(new EvaluationDetail<>(EvaluationReason.targetMatch(), 2, js("on")), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void flagMatchesUserFromRules() {
+ Clause clause0 = new Clause("key", Operator.in, Arrays.asList(js("wrongkey")), false);
+ Clause clause1 = new Clause("key", Operator.in, Arrays.asList(js("userkey")), false);
+ Rule rule0 = new Rule("ruleid0", Arrays.asList(clause0), 2, null);
+ Rule rule1 = new Rule("ruleid1", Arrays.asList(clause1), 2, null);
+ FeatureFlag f = featureFlagWithRules("feature", rule0, rule1);
+ LDUser user = new LDUser.Builder("userkey").build();
+ FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(new EvaluationDetail<>(EvaluationReason.ruleMatch(1, "ruleid1"), 2, js("on")), result.getDetails());
assertEquals(0, result.getPrerequisiteEvents().size());
}
@Test
- public void flagMatchesUserFromRules() throws Exception {
+ public void ruleWithTooHighVariationReturnsMalformedFlagError() {
Clause clause = new Clause("key", Operator.in, Arrays.asList(js("userkey")), false);
- Rule rule = new Rule(Arrays.asList(clause), 2, null);
- FeatureFlag f = new FeatureFlagBuilder("feature")
- .on(true)
- .rules(Arrays.asList(rule))
- .fallthrough(fallthroughVariation(0))
- .offVariation(1)
- .variations(js("fall"), js("off"), js("on"))
- .build();
+ Rule rule = new Rule("ruleid", Arrays.asList(clause), 999, null);
+ FeatureFlag f = featureFlagWithRules("feature", rule);
LDUser user = new LDUser.Builder("userkey").build();
FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
- assertEquals(js("on"), result.getResult().getValue());
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void ruleWithNegativeVariationReturnsMalformedFlagError() {
+ Clause clause = new Clause("key", Operator.in, Arrays.asList(js("userkey")), false);
+ Rule rule = new Rule("ruleid", Arrays.asList(clause), -1, null);
+ FeatureFlag f = featureFlagWithRules("feature", rule);
+ LDUser user = new LDUser.Builder("userkey").build();
+ FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void ruleWithNoVariationOrRolloutReturnsMalformedFlagError() {
+ Clause clause = new Clause("key", Operator.in, Arrays.asList(js("userkey")), false);
+ Rule rule = new Rule("ruleid", Arrays.asList(clause), null, null);
+ FeatureFlag f = featureFlagWithRules("feature", rule);
+ LDUser user = new LDUser.Builder("userkey").build();
+ FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
+ assertEquals(0, result.getPrerequisiteEvents().size());
+ }
+
+ @Test
+ public void ruleWithRolloutWithEmptyVariationsListReturnsMalformedFlagError() {
+ Clause clause = new Clause("key", Operator.in, Arrays.asList(js("userkey")), false);
+ Rule rule = new Rule("ruleid", Arrays.asList(clause), null,
+ new VariationOrRollout.Rollout(ImmutableList.of(), null));
+ FeatureFlag f = featureFlagWithRules("feature", rule);
+ LDUser user = new LDUser.Builder("userkey").build();
+ FeatureFlag.EvalResult result = f.evaluate(user, featureStore, EventFactory.DEFAULT);
+
+ assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails());
assertEquals(0, result.getPrerequisiteEvents().size());
}
@Test
public void clauseCanMatchBuiltInAttribute() throws Exception {
Clause clause = new Clause("name", Operator.in, Arrays.asList(js("Bob")), false);
- FeatureFlag f = booleanFlagWithClauses(clause);
+ FeatureFlag f = booleanFlagWithClauses("flag", clause);
LDUser user = new LDUser.Builder("key").name("Bob").build();
- assertEquals(jbool(true), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ assertEquals(jbool(true), f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue());
}
@Test
public void clauseCanMatchCustomAttribute() throws Exception {
Clause clause = new Clause("legs", Operator.in, Arrays.asList(jint(4)), false);
- FeatureFlag f = booleanFlagWithClauses(clause);
+ FeatureFlag f = booleanFlagWithClauses("flag", clause);
LDUser user = new LDUser.Builder("key").custom("legs", 4).build();
- assertEquals(jbool(true), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ assertEquals(jbool(true), f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue());
}
@Test
public void clauseReturnsFalseForMissingAttribute() throws Exception {
Clause clause = new Clause("legs", Operator.in, Arrays.asList(jint(4)), false);
- FeatureFlag f = booleanFlagWithClauses(clause);
+ FeatureFlag f = booleanFlagWithClauses("flag", clause);
LDUser user = new LDUser.Builder("key").name("Bob").build();
- assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue());
}
@Test
public void clauseCanBeNegated() throws Exception {
Clause clause = new Clause("name", Operator.in, Arrays.asList(js("Bob")), true);
- FeatureFlag f = booleanFlagWithClauses(clause);
+ FeatureFlag f = booleanFlagWithClauses("flag", clause);
LDUser user = new LDUser.Builder("key").name("Bob").build();
- assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue());
}
@Test
@@ -261,18 +407,18 @@ public void clauseWithUnsupportedOperatorStringIsUnmarshalledWithNullOperator()
@Test
public void clauseWithNullOperatorDoesNotMatch() throws Exception {
Clause badClause = new Clause("name", null, Arrays.asList(js("Bob")), false);
- FeatureFlag f = booleanFlagWithClauses(badClause);
+ FeatureFlag f = booleanFlagWithClauses("flag", badClause);
LDUser user = new LDUser.Builder("key").name("Bob").build();
- assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ assertEquals(jbool(false), f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails().getValue());
}
@Test
public void clauseWithNullOperatorDoesNotStopSubsequentRuleFromMatching() throws Exception {
Clause badClause = new Clause("name", null, Arrays.asList(js("Bob")), false);
- Rule badRule = new Rule(Arrays.asList(badClause), 1, null);
+ Rule badRule = new Rule("rule1", Arrays.asList(badClause), 1, null);
Clause goodClause = new Clause("name", Operator.in, Arrays.asList(js("Bob")), false);
- Rule goodRule = new Rule(Arrays.asList(goodClause), 1, null);
+ Rule goodRule = new Rule("rule2", Arrays.asList(goodClause), 1, null);
FeatureFlag f = new FeatureFlagBuilder("feature")
.on(true)
.rules(Arrays.asList(badRule, goodRule))
@@ -282,7 +428,8 @@ public void clauseWithNullOperatorDoesNotStopSubsequentRuleFromMatching() throws
.build();
LDUser user = new LDUser.Builder("key").name("Bob").build();
- assertEquals(jbool(true), f.evaluate(user, featureStore, EventFactory.DEFAULT).getResult().getValue());
+ EvaluationDetail details = f.evaluate(user, featureStore, EventFactory.DEFAULT).getDetails();
+ assertEquals(new EvaluationDetail<>(EvaluationReason.ruleMatch(1, "rule2"), 1, jbool(true)), details);
}
@Test
@@ -297,7 +444,7 @@ public void testSegmentMatchClauseRetrievesSegmentFromStore() throws Exception {
LDUser user = new LDUser.Builder("foo").build();
FeatureFlag.EvalResult result = flag.evaluate(user, featureStore, EventFactory.DEFAULT);
- assertEquals(jbool(true), result.getResult().getValue());
+ assertEquals(jbool(true), result.getDetails().getValue());
}
@Test
@@ -306,11 +453,21 @@ public void testSegmentMatchClauseFallsThroughIfSegmentNotFound() throws Excepti
LDUser user = new LDUser.Builder("foo").build();
FeatureFlag.EvalResult result = flag.evaluate(user, featureStore, EventFactory.DEFAULT);
- assertEquals(jbool(false), result.getResult().getValue());
+ assertEquals(jbool(false), result.getDetails().getValue());
}
+ private FeatureFlag featureFlagWithRules(String flagKey, Rule... rules) {
+ return new FeatureFlagBuilder(flagKey)
+ .on(true)
+ .rules(Arrays.asList(rules))
+ .fallthrough(fallthroughVariation(0))
+ .offVariation(1)
+ .variations(js("fall"), js("off"), js("on"))
+ .build();
+ }
+
private FeatureFlag segmentMatchBooleanFlag(String segmentKey) {
Clause clause = new Clause("", Operator.segmentMatch, Arrays.asList(js(segmentKey)), false);
- return booleanFlagWithClauses(clause);
+ return booleanFlagWithClauses("flag", clause);
}
}
diff --git a/src/test/java/com/launchdarkly/client/FeatureFlagsStateTest.java b/src/test/java/com/launchdarkly/client/FeatureFlagsStateTest.java
new file mode 100644
index 000000000..86944409c
--- /dev/null
+++ b/src/test/java/com/launchdarkly/client/FeatureFlagsStateTest.java
@@ -0,0 +1,116 @@
+package com.launchdarkly.client;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+
+import org.junit.Test;
+
+import static com.launchdarkly.client.TestUtil.js;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class FeatureFlagsStateTest {
+ private static final Gson gson = new Gson();
+
+ @Test
+ public void canGetFlagValue() {
+ EvaluationDetail eval = new EvaluationDetail(EvaluationReason.off(), 1, js("value"));
+ FeatureFlag flag = new FeatureFlagBuilder("key").build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder().addFlag(flag, eval).build();
+
+ assertEquals(js("value"), state.getFlagValue("key"));
+ }
+
+ @Test
+ public void unknownFlagReturnsNullValue() {
+ FeatureFlagsState state = new FeatureFlagsState.Builder().build();
+
+ assertNull(state.getFlagValue("key"));
+ }
+
+ @Test
+ public void canGetFlagReason() {
+ EvaluationDetail eval = new EvaluationDetail(EvaluationReason.off(), 1, js("value"));
+ FeatureFlag flag = new FeatureFlagBuilder("key").build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder(FlagsStateOption.WITH_REASONS)
+ .addFlag(flag, eval).build();
+
+ assertEquals(EvaluationReason.off(), state.getFlagReason("key"));
+ }
+
+ @Test
+ public void unknownFlagReturnsNullReason() {
+ FeatureFlagsState state = new FeatureFlagsState.Builder().build();
+
+ assertNull(state.getFlagReason("key"));
+ }
+
+ @Test
+ public void reasonIsNullIfReasonsWereNotRecorded() {
+ EvaluationDetail eval = new EvaluationDetail(EvaluationReason.off(), 1, js("value"));
+ FeatureFlag flag = new FeatureFlagBuilder("key").build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder().addFlag(flag, eval).build();
+
+ assertNull(state.getFlagReason("key"));
+ }
+
+ @Test
+ public void flagCanHaveNullValue() {
+ EvaluationDetail eval = new EvaluationDetail(null, 1, null);
+ FeatureFlag flag = new FeatureFlagBuilder("key").build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder().addFlag(flag, eval).build();
+
+ assertNull(state.getFlagValue("key"));
+ }
+
+ @Test
+ public void canConvertToValuesMap() {
+ EvaluationDetail eval1 = new EvaluationDetail(EvaluationReason.off(), 0, js("value1"));
+ FeatureFlag flag1 = new FeatureFlagBuilder("key1").build();
+ EvaluationDetail eval2 = new EvaluationDetail(EvaluationReason.off(), 1, js("value2"));
+ FeatureFlag flag2 = new FeatureFlagBuilder("key2").build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder()
+ .addFlag(flag1, eval1).addFlag(flag2, eval2).build();
+
+ ImmutableMap expected = ImmutableMap.of("key1", js("value1"), "key2", js("value2"));
+ assertEquals(expected, state.toValuesMap());
+ }
+
+ @Test
+ public void canConvertToJson() {
+ EvaluationDetail eval1 = new EvaluationDetail(EvaluationReason.off(), 0, js("value1"));
+ FeatureFlag flag1 = new FeatureFlagBuilder("key1").version(100).trackEvents(false).build();
+ EvaluationDetail eval2 = new EvaluationDetail(EvaluationReason.fallthrough(), 1, js("value2"));
+ FeatureFlag flag2 = new FeatureFlagBuilder("key2").version(200).trackEvents(true).debugEventsUntilDate(1000L).build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder(FlagsStateOption.WITH_REASONS)
+ .addFlag(flag1, eval1).addFlag(flag2, eval2).build();
+
+ String json = "{\"key1\":\"value1\",\"key2\":\"value2\"," +
+ "\"$flagsState\":{" +
+ "\"key1\":{" +
+ "\"variation\":0,\"version\":100,\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":false" +
+ "},\"key2\":{" +
+ "\"variation\":1,\"version\":200,\"reason\":{\"kind\":\"FALLTHROUGH\"},\"trackEvents\":true,\"debugEventsUntilDate\":1000" +
+ "}" +
+ "}," +
+ "\"$valid\":true" +
+ "}";
+ JsonElement expected = gson.fromJson(json, JsonElement.class);
+ assertEquals(expected, gson.toJsonTree(state));
+ }
+
+ @Test
+ public void canConvertFromJson() {
+ EvaluationDetail eval1 = new EvaluationDetail(null, 0, js("value1"));
+ FeatureFlag flag1 = new FeatureFlagBuilder("key1").version(100).trackEvents(false).build();
+ EvaluationDetail eval2 = new EvaluationDetail(null, 1, js("value2"));
+ FeatureFlag flag2 = new FeatureFlagBuilder("key2").version(200).trackEvents(true).debugEventsUntilDate(1000L).build();
+ FeatureFlagsState state = new FeatureFlagsState.Builder()
+ .addFlag(flag1, eval1).addFlag(flag2, eval2).build();
+
+ String json = gson.toJson(state);
+ FeatureFlagsState state1 = gson.fromJson(json, FeatureFlagsState.class);
+ assertEquals(state, state1);
+ }
+}
diff --git a/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java b/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java
index f6d175dc0..201978b9f 100644
--- a/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java
@@ -1,25 +1,40 @@
package com.launchdarkly.client;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.junit.Test;
import java.util.Arrays;
+import java.util.Map;
+import static com.launchdarkly.client.TestUtil.booleanFlagWithClauses;
+import static com.launchdarkly.client.TestUtil.failedUpdateProcessor;
+import static com.launchdarkly.client.TestUtil.fallthroughVariation;
+import static com.launchdarkly.client.TestUtil.featureStoreThatThrowsException;
+import static com.launchdarkly.client.TestUtil.flagWithValue;
+import static com.launchdarkly.client.TestUtil.jbool;
+import static com.launchdarkly.client.TestUtil.jdouble;
import static com.launchdarkly.client.TestUtil.jint;
import static com.launchdarkly.client.TestUtil.js;
import static com.launchdarkly.client.TestUtil.specificFeatureStore;
+import static com.launchdarkly.client.TestUtil.specificUpdateProcessor;
import static com.launchdarkly.client.VersionedDataKind.FEATURES;
import static com.launchdarkly.client.VersionedDataKind.SEGMENTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class LDClientEvaluationTest {
private static final LDUser user = new LDUser("userkey");
+ private static final LDUser userWithNullKey = new LDUser.Builder((String)null).build();
+ private static final Gson gson = new Gson();
- private TestFeatureStore featureStore = new TestFeatureStore();
+ private FeatureStore featureStore = TestUtil.initedFeatureStore();
+
private LDConfig config = new LDConfig.Builder()
.featureStoreFactory(specificFeatureStore(featureStore))
.eventProcessorFactory(Components.nullEventProcessor())
@@ -29,7 +44,7 @@ public class LDClientEvaluationTest {
@Test
public void boolVariationReturnsFlagValue() throws Exception {
- featureStore.setFeatureTrue("key");
+ featureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
assertTrue(client.boolVariation("key", user, false));
}
@@ -41,7 +56,7 @@ public void boolVariationReturnsDefaultValueForUnknownFlag() throws Exception {
@Test
public void intVariationReturnsFlagValue() throws Exception {
- featureStore.setIntegerValue("key", 2);
+ featureStore.upsert(FEATURES, flagWithValue("key", jint(2)));
assertEquals(new Integer(2), client.intVariation("key", user, 1));
}
@@ -53,7 +68,7 @@ public void intVariationReturnsDefaultValueForUnknownFlag() throws Exception {
@Test
public void doubleVariationReturnsFlagValue() throws Exception {
- featureStore.setDoubleValue("key", 2.5d);
+ featureStore.upsert(FEATURES, flagWithValue("key", jdouble(2.5d)));
assertEquals(new Double(2.5d), client.doubleVariation("key", user, 1.0d));
}
@@ -65,7 +80,7 @@ public void doubleVariationReturnsDefaultValueForUnknownFlag() throws Exception
@Test
public void stringVariationReturnsFlagValue() throws Exception {
- featureStore.setStringValue("key", "b");
+ featureStore.upsert(FEATURES, flagWithValue("key", js("b")));
assertEquals("b", client.stringVariation("key", user, "a"));
}
@@ -79,7 +94,7 @@ public void stringVariationReturnsDefaultValueForUnknownFlag() throws Exception
public void jsonVariationReturnsFlagValue() throws Exception {
JsonObject data = new JsonObject();
data.addProperty("thing", "stuff");
- featureStore.setJsonValue("key", data);
+ featureStore.upsert(FEATURES, flagWithValue("key", data));
assertEquals(data, client.jsonVariation("key", user, jint(42)));
}
@@ -100,16 +115,225 @@ public void canMatchUserBySegment() throws Exception {
featureStore.upsert(SEGMENTS, segment);
Clause clause = new Clause("", Operator.segmentMatch, Arrays.asList(js("segment1")), false);
- Rule rule = new Rule(Arrays.asList(clause), 0, null);
- FeatureFlag feature = new FeatureFlagBuilder("test-feature")
- .version(1)
- .rules(Arrays.asList(rule))
- .variations(TestFeatureStore.TRUE_FALSE_VARIATIONS)
+ FeatureFlag feature = booleanFlagWithClauses("feature", clause);
+ featureStore.upsert(FEATURES, feature);
+
+ assertTrue(client.boolVariation("feature", user, false));
+ }
+
+ @Test
+ public void canGetDetailsForSuccessfulEvaluation() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
+
+ EvaluationDetail expectedResult = new EvaluationDetail<>(EvaluationReason.off(), 0, true);
+ assertEquals(expectedResult, client.boolVariationDetail("key", user, false));
+ }
+
+ @Test
+ public void variationReturnsDefaultIfFlagEvaluatesToNull() {
+ FeatureFlag flag = new FeatureFlagBuilder("key").on(false).offVariation(null).build();
+ featureStore.upsert(FEATURES, flag);
+
+ assertEquals("default", client.stringVariation("key", user, "default"));
+ }
+
+ @Test
+ public void variationDetailReturnsDefaultIfFlagEvaluatesToNull() {
+ FeatureFlag flag = new FeatureFlagBuilder("key").on(false).offVariation(null).build();
+ featureStore.upsert(FEATURES, flag);
+
+ EvaluationDetail expected = new EvaluationDetail(EvaluationReason.off(), null, "default");
+ EvaluationDetail actual = client.stringVariationDetail("key", user, "default");
+ assertEquals(expected, actual);
+ assertTrue(actual.isDefaultValue());
+ }
+
+ @Test
+ public void appropriateErrorIfClientNotInitialized() throws Exception {
+ FeatureStore badFeatureStore = new InMemoryFeatureStore();
+ LDConfig badConfig = new LDConfig.Builder()
+ .featureStoreFactory(specificFeatureStore(badFeatureStore))
+ .eventProcessorFactory(Components.nullEventProcessor())
+ .updateProcessorFactory(specificUpdateProcessor(failedUpdateProcessor()))
+ .startWaitMillis(0)
+ .build();
+ try (LDClientInterface badClient = new LDClient("SDK_KEY", badConfig)) {
+ EvaluationDetail expectedResult = EvaluationDetail.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY, false);
+ assertEquals(expectedResult, badClient.boolVariationDetail("key", user, false));
+ }
+ }
+
+ @Test
+ public void appropriateErrorIfFlagDoesNotExist() throws Exception {
+ EvaluationDetail expectedResult = EvaluationDetail.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND, "default");
+ assertEquals(expectedResult, client.stringVariationDetail("key", user, "default"));
+ }
+
+ @Test
+ public void appropriateErrorIfUserNotSpecified() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
+
+ EvaluationDetail expectedResult = EvaluationDetail.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, "default");
+ assertEquals(expectedResult, client.stringVariationDetail("key", null, "default"));
+ }
+
+ @Test
+ public void appropriateErrorIfValueWrongType() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
+
+ EvaluationDetail expectedResult = EvaluationDetail.error(EvaluationReason.ErrorKind.WRONG_TYPE, 3);
+ assertEquals(expectedResult, client.intVariationDetail("key", user, 3));
+ }
+
+ @Test
+ public void appropriateErrorForUnexpectedException() throws Exception {
+ FeatureStore badFeatureStore = featureStoreThatThrowsException(new RuntimeException("sorry"));
+ LDConfig badConfig = new LDConfig.Builder()
+ .featureStoreFactory(specificFeatureStore(badFeatureStore))
+ .eventProcessorFactory(Components.nullEventProcessor())
+ .updateProcessorFactory(Components.nullUpdateProcessor())
+ .build();
+ try (LDClientInterface badClient = new LDClient("SDK_KEY", badConfig)) {
+ EvaluationDetail expectedResult = EvaluationDetail.error(EvaluationReason.ErrorKind.EXCEPTION, false);
+ assertEquals(expectedResult, badClient.boolVariationDetail("key", user, false));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void allFlagsReturnsFlagValues() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key1", js("value1")));
+ featureStore.upsert(FEATURES, flagWithValue("key2", js("value2")));
+
+ Map result = client.allFlags(user);
+ assertEquals(ImmutableMap.of("key1", js("value1"), "key2", js("value2")), result);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void allFlagsReturnsNullForNullUser() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", js("value")));
+
+ assertNull(client.allFlags(null));
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void allFlagsReturnsNullForNullUserKey() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", js("value")));
+
+ assertNull(client.allFlags(userWithNullKey));
+ }
+
+ @Test
+ public void allFlagsStateReturnsState() throws Exception {
+ FeatureFlag flag1 = new FeatureFlagBuilder("key1")
+ .version(100)
+ .trackEvents(false)
+ .on(false)
+ .offVariation(0)
+ .variations(js("value1"))
+ .build();
+ FeatureFlag flag2 = new FeatureFlagBuilder("key2")
+ .version(200)
+ .trackEvents(true)
+ .debugEventsUntilDate(1000L)
.on(true)
- .fallthrough(new VariationOrRollout(1, null))
+ .fallthrough(fallthroughVariation(1))
+ .variations(js("off"), js("value2"))
.build();
- featureStore.upsert(FEATURES, feature);
+ featureStore.upsert(FEATURES, flag1);
+ featureStore.upsert(FEATURES, flag2);
+
+ FeatureFlagsState state = client.allFlagsState(user);
+ assertTrue(state.isValid());
- assertTrue(client.boolVariation("test-feature", user, false));
+ String json = "{\"key1\":\"value1\",\"key2\":\"value2\"," +
+ "\"$flagsState\":{" +
+ "\"key1\":{" +
+ "\"variation\":0,\"version\":100,\"trackEvents\":false" +
+ "},\"key2\":{" +
+ "\"variation\":1,\"version\":200,\"trackEvents\":true,\"debugEventsUntilDate\":1000" +
+ "}" +
+ "}," +
+ "\"$valid\":true" +
+ "}";
+ JsonElement expected = gson.fromJson(json, JsonElement.class);
+ assertEquals(expected, gson.toJsonTree(state));
+ }
+
+ @Test
+ public void allFlagsStateCanFilterForOnlyClientSideFlags() {
+ FeatureFlag flag1 = new FeatureFlagBuilder("server-side-1").build();
+ FeatureFlag flag2 = new FeatureFlagBuilder("server-side-2").build();
+ FeatureFlag flag3 = new FeatureFlagBuilder("client-side-1").clientSide(true)
+ .variations(js("value1")).offVariation(0).build();
+ FeatureFlag flag4 = new FeatureFlagBuilder("client-side-2").clientSide(true)
+ .variations(js("value2")).offVariation(0).build();
+ featureStore.upsert(FEATURES, flag1);
+ featureStore.upsert(FEATURES, flag2);
+ featureStore.upsert(FEATURES, flag3);
+ featureStore.upsert(FEATURES, flag4);
+
+ FeatureFlagsState state = client.allFlagsState(user, FlagsStateOption.CLIENT_SIDE_ONLY);
+ assertTrue(state.isValid());
+
+ Map allValues = state.toValuesMap();
+ assertEquals(ImmutableMap.of("client-side-1", js("value1"), "client-side-2", js("value2")), allValues);
+ }
+
+ @Test
+ public void allFlagsStateReturnsStateWithReasons() {
+ FeatureFlag flag1 = new FeatureFlagBuilder("key1")
+ .version(100)
+ .trackEvents(false)
+ .on(false)
+ .offVariation(0)
+ .variations(js("value1"))
+ .build();
+ FeatureFlag flag2 = new FeatureFlagBuilder("key2")
+ .version(200)
+ .trackEvents(true)
+ .debugEventsUntilDate(1000L)
+ .on(true)
+ .fallthrough(fallthroughVariation(1))
+ .variations(js("off"), js("value2"))
+ .build();
+ featureStore.upsert(FEATURES, flag1);
+ featureStore.upsert(FEATURES, flag2);
+
+ FeatureFlagsState state = client.allFlagsState(user, FlagsStateOption.WITH_REASONS);
+ assertTrue(state.isValid());
+
+ String json = "{\"key1\":\"value1\",\"key2\":\"value2\"," +
+ "\"$flagsState\":{" +
+ "\"key1\":{" +
+ "\"variation\":0,\"version\":100,\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":false" +
+ "},\"key2\":{" +
+ "\"variation\":1,\"version\":200,\"reason\":{\"kind\":\"FALLTHROUGH\"},\"trackEvents\":true,\"debugEventsUntilDate\":1000" +
+ "}" +
+ "}," +
+ "\"$valid\":true" +
+ "}";
+ JsonElement expected = gson.fromJson(json, JsonElement.class);
+ assertEquals(expected, gson.toJsonTree(state));
+ }
+
+ @Test
+ public void allFlagsStateReturnsEmptyStateForNullUser() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", js("value")));
+
+ FeatureFlagsState state = client.allFlagsState(null);
+ assertFalse(state.isValid());
+ assertEquals(0, state.toValuesMap().size());
+ }
+
+ @Test
+ public void allFlagsStateReturnsEmptyStateForNullUserKey() throws Exception {
+ featureStore.upsert(FEATURES, flagWithValue("key", js("value")));
+
+ FeatureFlagsState state = client.allFlagsState(userWithNullKey);
+ assertFalse(state.isValid());
+ assertEquals(0, state.toValuesMap().size());
}
}
diff --git a/src/test/java/com/launchdarkly/client/LDClientEventTest.java b/src/test/java/com/launchdarkly/client/LDClientEventTest.java
index 8123e6e8f..ded8dbb7b 100644
--- a/src/test/java/com/launchdarkly/client/LDClientEventTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientEventTest.java
@@ -3,12 +3,14 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
+import com.launchdarkly.client.EvaluationReason.ErrorKind;
import org.junit.Test;
import java.util.Arrays;
import static com.launchdarkly.client.TestUtil.fallthroughVariation;
+import static com.launchdarkly.client.TestUtil.flagWithValue;
import static com.launchdarkly.client.TestUtil.jbool;
import static com.launchdarkly.client.TestUtil.jdouble;
import static com.launchdarkly.client.TestUtil.jint;
@@ -22,7 +24,7 @@
public class LDClientEventTest {
private static final LDUser user = new LDUser("userkey");
- private TestFeatureStore featureStore = new TestFeatureStore();
+ private FeatureStore featureStore = TestUtil.initedFeatureStore();
private TestUtil.TestEventProcessor eventSink = new TestUtil.TestEventProcessor();
private LDConfig config = new LDConfig.Builder()
.featureStoreFactory(specificFeatureStore(featureStore))
@@ -72,78 +74,155 @@ public void trackSendsEventWithData() throws Exception {
@Test
public void boolVariationSendsEvent() throws Exception {
- FeatureFlag flag = featureStore.setFeatureTrue("key");
+ FeatureFlag flag = flagWithValue("key", jbool(true));
+ featureStore.upsert(FEATURES, flag);
client.boolVariation("key", user, false);
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), flag, jbool(true), jbool(false), null);
+ checkFeatureEvent(eventSink.events.get(0), flag, jbool(true), jbool(false), null, null);
}
@Test
public void boolVariationSendsEventForUnknownFlag() throws Exception {
client.boolVariation("key", user, false);
assertEquals(1, eventSink.events.size());
- checkUnknownFeatureEvent(eventSink.events.get(0), "key", jbool(false), null);
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jbool(false), null, null);
}
+ @Test
+ public void boolVariationDetailSendsEvent() throws Exception {
+ FeatureFlag flag = flagWithValue("key", jbool(true));
+ featureStore.upsert(FEATURES, flag);
+
+ client.boolVariationDetail("key", user, false);
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), flag, jbool(true), jbool(false), null, EvaluationReason.off());
+ }
+
+ @Test
+ public void boolVariationDetailSendsEventForUnknownFlag() throws Exception {
+ client.boolVariationDetail("key", user, false);
+ assertEquals(1, eventSink.events.size());
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jbool(false), null,
+ EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND));
+ }
+
@Test
public void intVariationSendsEvent() throws Exception {
- FeatureFlag flag = featureStore.setIntegerValue("key", 2);
+ FeatureFlag flag = flagWithValue("key", jint(2));
+ featureStore.upsert(FEATURES, flag);
client.intVariation("key", user, 1);
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), flag, jint(2), jint(1), null);
+ checkFeatureEvent(eventSink.events.get(0), flag, jint(2), jint(1), null, null);
}
@Test
public void intVariationSendsEventForUnknownFlag() throws Exception {
client.intVariation("key", user, 1);
assertEquals(1, eventSink.events.size());
- checkUnknownFeatureEvent(eventSink.events.get(0), "key", jint(1), null);
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jint(1), null, null);
+ }
+
+ @Test
+ public void intVariationDetailSendsEvent() throws Exception {
+ FeatureFlag flag = flagWithValue("key", jint(2));
+ featureStore.upsert(FEATURES, flag);
+
+ client.intVariationDetail("key", user, 1);
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), flag, jint(2), jint(1), null, EvaluationReason.off());
+ }
+
+ @Test
+ public void intVariationDetailSendsEventForUnknownFlag() throws Exception {
+ client.intVariationDetail("key", user, 1);
+ assertEquals(1, eventSink.events.size());
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jint(1), null,
+ EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND));
}
@Test
public void doubleVariationSendsEvent() throws Exception {
- FeatureFlag flag = featureStore.setDoubleValue("key", 2.5d);
+ FeatureFlag flag = flagWithValue("key", jdouble(2.5d));
+ featureStore.upsert(FEATURES, flag);
client.doubleVariation("key", user, 1.0d);
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), flag, jdouble(2.5d), jdouble(1.0d), null);
+ checkFeatureEvent(eventSink.events.get(0), flag, jdouble(2.5d), jdouble(1.0d), null, null);
}
@Test
public void doubleVariationSendsEventForUnknownFlag() throws Exception {
client.doubleVariation("key", user, 1.0d);
assertEquals(1, eventSink.events.size());
- checkUnknownFeatureEvent(eventSink.events.get(0), "key", jdouble(1.0), null);
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jdouble(1.0), null, null);
}
-
+
+ @Test
+ public void doubleVariationDetailSendsEvent() throws Exception {
+ FeatureFlag flag = flagWithValue("key", jdouble(2.5d));
+ featureStore.upsert(FEATURES, flag);
+
+ client.doubleVariationDetail("key", user, 1.0d);
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), flag, jdouble(2.5d), jdouble(1.0d), null, EvaluationReason.off());
+ }
+
+ @Test
+ public void doubleVariationDetailSendsEventForUnknownFlag() throws Exception {
+ client.doubleVariationDetail("key", user, 1.0d);
+ assertEquals(1, eventSink.events.size());
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", jdouble(1.0), null,
+ EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND));
+ }
+
@Test
public void stringVariationSendsEvent() throws Exception {
- FeatureFlag flag = featureStore.setStringValue("key", "b");
+ FeatureFlag flag = flagWithValue("key", js("b"));
+ featureStore.upsert(FEATURES, flag);
client.stringVariation("key", user, "a");
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), flag, js("b"), js("a"), null);
+ checkFeatureEvent(eventSink.events.get(0), flag, js("b"), js("a"), null, null);
}
@Test
public void stringVariationSendsEventForUnknownFlag() throws Exception {
client.stringVariation("key", user, "a");
assertEquals(1, eventSink.events.size());
- checkUnknownFeatureEvent(eventSink.events.get(0), "key", js("a"), null);
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", js("a"), null, null);
}
-
+
+ @Test
+ public void stringVariationDetailSendsEvent() throws Exception {
+ FeatureFlag flag = flagWithValue("key", js("b"));
+ featureStore.upsert(FEATURES, flag);
+
+ client.stringVariationDetail("key", user, "a");
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), flag, js("b"), js("a"), null, EvaluationReason.off());
+ }
+
+ @Test
+ public void stringVariationDetailSendsEventForUnknownFlag() throws Exception {
+ client.stringVariationDetail("key", user, "a");
+ assertEquals(1, eventSink.events.size());
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", js("a"), null,
+ EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND));
+ }
+
@Test
public void jsonVariationSendsEvent() throws Exception {
JsonObject data = new JsonObject();
data.addProperty("thing", "stuff");
- FeatureFlag flag = featureStore.setJsonValue("key", data);
+ FeatureFlag flag = flagWithValue("key", data);
+ featureStore.upsert(FEATURES, flag);
JsonElement defaultVal = new JsonPrimitive(42);
client.jsonVariation("key", user, defaultVal);
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), flag, data, defaultVal, null);
+ checkFeatureEvent(eventSink.events.get(0), flag, data, defaultVal, null, null);
}
@Test
@@ -152,7 +231,30 @@ public void jsonVariationSendsEventForUnknownFlag() throws Exception {
client.jsonVariation("key", user, defaultVal);
assertEquals(1, eventSink.events.size());
- checkUnknownFeatureEvent(eventSink.events.get(0), "key", defaultVal, null);
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", defaultVal, null, null);
+ }
+
+ @Test
+ public void jsonVariationDetailSendsEvent() throws Exception {
+ JsonObject data = new JsonObject();
+ data.addProperty("thing", "stuff");
+ FeatureFlag flag = flagWithValue("key", data);
+ featureStore.upsert(FEATURES, flag);
+ JsonElement defaultVal = new JsonPrimitive(42);
+
+ client.jsonVariationDetail("key", user, defaultVal);
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), flag, data, defaultVal, null, EvaluationReason.off());
+ }
+
+ @Test
+ public void jsonVariationDetailSendsEventForUnknownFlag() throws Exception {
+ JsonElement defaultVal = new JsonPrimitive(42);
+
+ client.jsonVariationDetail("key", user, defaultVal);
+ assertEquals(1, eventSink.events.size());
+ checkUnknownFeatureEvent(eventSink.events.get(0), "key", defaultVal, null,
+ EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND));
}
@Test
@@ -177,8 +279,34 @@ public void eventIsSentForExistingPrererequisiteFlag() throws Exception {
client.stringVariation("feature0", user, "default");
assertEquals(2, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), f1, js("go"), null, "feature0");
- checkFeatureEvent(eventSink.events.get(1), f0, js("fall"), js("default"), null);
+ checkFeatureEvent(eventSink.events.get(0), f1, js("go"), null, "feature0", null);
+ checkFeatureEvent(eventSink.events.get(1), f0, js("fall"), js("default"), null, null);
+ }
+
+ @Test
+ public void eventIsSentWithReasonForExistingPrererequisiteFlag() throws Exception {
+ FeatureFlag f0 = new FeatureFlagBuilder("feature0")
+ .on(true)
+ .prerequisites(Arrays.asList(new Prerequisite("feature1", 1)))
+ .fallthrough(fallthroughVariation(0))
+ .offVariation(1)
+ .variations(js("fall"), js("off"), js("on"))
+ .version(1)
+ .build();
+ FeatureFlag f1 = new FeatureFlagBuilder("feature1")
+ .on(true)
+ .fallthrough(fallthroughVariation(1))
+ .variations(js("nogo"), js("go"))
+ .version(2)
+ .build();
+ featureStore.upsert(FEATURES, f0);
+ featureStore.upsert(FEATURES, f1);
+
+ client.stringVariationDetail("feature0", user, "default");
+
+ assertEquals(2, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), f1, js("go"), null, "feature0", EvaluationReason.fallthrough());
+ checkFeatureEvent(eventSink.events.get(1), f0, js("fall"), js("default"), null, EvaluationReason.fallthrough());
}
@Test
@@ -196,11 +324,30 @@ public void eventIsNotSentForUnknownPrererequisiteFlag() throws Exception {
client.stringVariation("feature0", user, "default");
assertEquals(1, eventSink.events.size());
- checkFeatureEvent(eventSink.events.get(0), f0, js("off"), js("default"), null);
+ checkFeatureEvent(eventSink.events.get(0), f0, js("off"), js("default"), null, null);
+ }
+
+ @Test
+ public void failureReasonIsGivenForUnknownPrererequisiteFlagIfDetailsWereRequested() throws Exception {
+ FeatureFlag f0 = new FeatureFlagBuilder("feature0")
+ .on(true)
+ .prerequisites(Arrays.asList(new Prerequisite("feature1", 1)))
+ .fallthrough(fallthroughVariation(0))
+ .offVariation(1)
+ .variations(js("fall"), js("off"), js("on"))
+ .version(1)
+ .build();
+ featureStore.upsert(FEATURES, f0);
+
+ client.stringVariationDetail("feature0", user, "default");
+
+ assertEquals(1, eventSink.events.size());
+ checkFeatureEvent(eventSink.events.get(0), f0, js("off"), js("default"), null,
+ EvaluationReason.prerequisiteFailed("feature1"));
}
private void checkFeatureEvent(Event e, FeatureFlag flag, JsonElement value, JsonElement defaultVal,
- String prereqOf) {
+ String prereqOf, EvaluationReason reason) {
assertEquals(Event.FeatureRequest.class, e.getClass());
Event.FeatureRequest fe = (Event.FeatureRequest)e;
assertEquals(flag.getKey(), fe.key);
@@ -209,9 +356,11 @@ private void checkFeatureEvent(Event e, FeatureFlag flag, JsonElement value, Jso
assertEquals(value, fe.value);
assertEquals(defaultVal, fe.defaultVal);
assertEquals(prereqOf, fe.prereqOf);
+ assertEquals(reason, fe.reason);
}
- private void checkUnknownFeatureEvent(Event e, String key, JsonElement defaultVal, String prereqOf) {
+ private void checkUnknownFeatureEvent(Event e, String key, JsonElement defaultVal, String prereqOf,
+ EvaluationReason reason) {
assertEquals(Event.FeatureRequest.class, e.getClass());
Event.FeatureRequest fe = (Event.FeatureRequest)e;
assertEquals(key, fe.key);
@@ -220,5 +369,6 @@ private void checkUnknownFeatureEvent(Event e, String key, JsonElement defaultVa
assertEquals(defaultVal, fe.value);
assertEquals(defaultVal, fe.defaultVal);
assertEquals(prereqOf, fe.prereqOf);
+ assertEquals(reason, fe.reason);
}
}
diff --git a/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java b/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java
index 62a584c35..f77030875 100644
--- a/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java
@@ -4,7 +4,10 @@
import java.io.IOException;
+import static com.launchdarkly.client.TestUtil.flagWithValue;
+import static com.launchdarkly.client.TestUtil.initedFeatureStore;
import static com.launchdarkly.client.TestUtil.specificFeatureStore;
+import static com.launchdarkly.client.VersionedDataKind.FEATURES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -41,12 +44,13 @@ public void lddModeClientIsInitialized() throws IOException {
@Test
public void lddModeClientGetsFlagFromFeatureStore() throws IOException {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig config = new LDConfig.Builder()
.useLdd(true)
.featureStoreFactory(specificFeatureStore(testFeatureStore))
.build();
- testFeatureStore.setFeatureTrue("key");
+ FeatureFlag flag = flagWithValue("key", TestUtil.jbool(true));
+ testFeatureStore.upsert(FEATURES, flag);
try (LDClient client = new LDClient("SDK_KEY", config)) {
assertTrue(client.boolVariation("key", new LDUser("user"), false));
}
diff --git a/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java b/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java
index 5a8369f49..472e22b30 100644
--- a/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java
@@ -1,19 +1,24 @@
package com.launchdarkly.client;
+import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
-import com.google.gson.JsonPrimitive;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
+import static com.launchdarkly.client.TestUtil.flagWithValue;
+import static com.launchdarkly.client.TestUtil.initedFeatureStore;
+import static com.launchdarkly.client.TestUtil.jbool;
import static com.launchdarkly.client.TestUtil.specificFeatureStore;
+import static com.launchdarkly.client.VersionedDataKind.FEATURES;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class LDClientOfflineTest {
+ private static final LDUser user = new LDUser("user");
+
@Test
public void offlineClientHasNullUpdateProcessor() throws IOException {
LDConfig config = new LDConfig.Builder()
@@ -50,26 +55,39 @@ public void offlineClientReturnsDefaultValue() throws IOException {
.offline(true)
.build();
try (LDClient client = new LDClient("SDK_KEY", config)) {
- assertEquals("x", client.stringVariation("key", new LDUser("user"), "x"));
+ assertEquals("x", client.stringVariation("key", user, "x"));
}
}
@Test
public void offlineClientGetsAllFlagsFromFeatureStore() throws IOException {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig config = new LDConfig.Builder()
.offline(true)
.featureStoreFactory(specificFeatureStore(testFeatureStore))
.build();
- testFeatureStore.setFeatureTrue("key");
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
try (LDClient client = new LDClient("SDK_KEY", config)) {
- Map allFlags = client.allFlags(new LDUser("user"));
- assertNotNull(allFlags);
- assertEquals(1, allFlags.size());
- assertEquals(new JsonPrimitive(true), allFlags.get("key"));
+ Map allFlags = client.allFlags(user);
+ assertEquals(ImmutableMap.of("key", jbool(true)), allFlags);
}
}
+ @Test
+ public void offlineClientGetsFlagsStateFromFeatureStore() throws IOException {
+ FeatureStore testFeatureStore = initedFeatureStore();
+ LDConfig config = new LDConfig.Builder()
+ .offline(true)
+ .featureStoreFactory(specificFeatureStore(testFeatureStore))
+ .build();
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jbool(true)));
+ try (LDClient client = new LDClient("SDK_KEY", config)) {
+ FeatureFlagsState state = client.allFlagsState(user);
+ assertTrue(state.isValid());
+ assertEquals(ImmutableMap.of("key", jbool(true)), state.toValuesMap());
+ }
+ }
+
@Test
public void testSecureModeHash() throws IOException {
LDConfig config = new LDConfig.Builder()
diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java
index cd50a1c57..b95b070e3 100644
--- a/src/test/java/com/launchdarkly/client/LDClientTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientTest.java
@@ -10,7 +10,11 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import static com.launchdarkly.client.TestUtil.flagWithValue;
+import static com.launchdarkly.client.TestUtil.initedFeatureStore;
+import static com.launchdarkly.client.TestUtil.jint;
import static com.launchdarkly.client.TestUtil.specificFeatureStore;
+import static com.launchdarkly.client.VersionedDataKind.FEATURES;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
@@ -152,8 +156,7 @@ public void clientCatchesRuntimeExceptionFromUpdateProcessor() throws Exception
@Test
public void isFlagKnownReturnsTrueForExistingFlag() throws Exception {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
- testFeatureStore.setInitialized(true);
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig.Builder config = new LDConfig.Builder()
.startWaitMillis(0)
.featureStoreFactory(specificFeatureStore(testFeatureStore));
@@ -163,15 +166,14 @@ public void isFlagKnownReturnsTrueForExistingFlag() throws Exception {
client = createMockClient(config);
- testFeatureStore.setIntegerValue("key", 1);
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jint(1)));
assertTrue(client.isFlagKnown("key"));
verifyAll();
}
@Test
public void isFlagKnownReturnsFalseForUnknownFlag() throws Exception {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
- testFeatureStore.setInitialized(true);
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig.Builder config = new LDConfig.Builder()
.startWaitMillis(0)
.featureStoreFactory(specificFeatureStore(testFeatureStore));
@@ -187,8 +189,7 @@ public void isFlagKnownReturnsFalseForUnknownFlag() throws Exception {
@Test
public void isFlagKnownReturnsFalseIfStoreAndClientAreNotInitialized() throws Exception {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
- testFeatureStore.setInitialized(false);
+ FeatureStore testFeatureStore = new InMemoryFeatureStore();
LDConfig.Builder config = new LDConfig.Builder()
.startWaitMillis(0)
.featureStoreFactory(specificFeatureStore(testFeatureStore));
@@ -198,15 +199,14 @@ public void isFlagKnownReturnsFalseIfStoreAndClientAreNotInitialized() throws Ex
client = createMockClient(config);
- testFeatureStore.setIntegerValue("key", 1);
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jint(1)));
assertFalse(client.isFlagKnown("key"));
verifyAll();
}
@Test
public void isFlagKnownUsesStoreIfStoreIsInitializedButClientIsNot() throws Exception {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
- testFeatureStore.setInitialized(true);
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig.Builder config = new LDConfig.Builder()
.startWaitMillis(0)
.featureStoreFactory(specificFeatureStore(testFeatureStore));
@@ -216,15 +216,14 @@ public void isFlagKnownUsesStoreIfStoreIsInitializedButClientIsNot() throws Exce
client = createMockClient(config);
- testFeatureStore.setIntegerValue("key", 1);
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jint(1)));
assertTrue(client.isFlagKnown("key"));
verifyAll();
}
@Test
public void evaluationUsesStoreIfStoreIsInitializedButClientIsNot() throws Exception {
- TestFeatureStore testFeatureStore = new TestFeatureStore();
- testFeatureStore.setInitialized(true);
+ FeatureStore testFeatureStore = initedFeatureStore();
LDConfig.Builder config = new LDConfig.Builder()
.featureStoreFactory(specificFeatureStore(testFeatureStore))
.startWaitMillis(0L);
@@ -235,7 +234,7 @@ public void evaluationUsesStoreIfStoreIsInitializedButClientIsNot() throws Excep
client = createMockClient(config);
- testFeatureStore.setIntegerValue("key", 1);
+ testFeatureStore.upsert(FEATURES, flagWithValue("key", jint(1)));
assertEquals(new Integer(1), client.intVariation("key", new LDUser("user"), 0));
verifyAll();
diff --git a/src/test/java/com/launchdarkly/client/TestUtil.java b/src/test/java/com/launchdarkly/client/TestUtil.java
index 01304c620..a55f81fb9 100644
--- a/src/test/java/com/launchdarkly/client/TestUtil.java
+++ b/src/test/java/com/launchdarkly/client/TestUtil.java
@@ -1,5 +1,6 @@
package com.launchdarkly.client;
+import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
@@ -11,7 +12,10 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
import static org.hamcrest.Matchers.equalTo;
@@ -25,6 +29,12 @@ public FeatureStore createFeatureStore() {
};
}
+ public static FeatureStore initedFeatureStore() {
+ FeatureStore store = new InMemoryFeatureStore();
+ store.init(Collections., Map>emptyMap());
+ return store;
+ }
+
public static EventProcessorFactory specificEventProcessor(final EventProcessor ep) {
return new EventProcessorFactory() {
public EventProcessor createEventProcessor(String sdkKey, LDConfig config) {
@@ -40,7 +50,56 @@ public UpdateProcessor createUpdateProcessor(String sdkKey, LDConfig config, Fea
}
};
}
+
+ public static FeatureStore featureStoreThatThrowsException(final RuntimeException e) {
+ return new FeatureStore() {
+ @Override
+ public void close() throws IOException { }
+
+ @Override
+ public T get(VersionedDataKind kind, String key) {
+ throw e;
+ }
+
+ @Override
+ public Map all(VersionedDataKind kind) {
+ throw e;
+ }
+
+ @Override
+ public void init(Map, Map> allData) { }
+
+ @Override
+ public void delete(VersionedDataKind kind, String key, int version) { }
+
+ @Override
+ public void upsert(VersionedDataKind kind, T item) { }
+
+ @Override
+ public boolean initialized() {
+ return true;
+ }
+ };
+ }
+ public static UpdateProcessor failedUpdateProcessor() {
+ return new UpdateProcessor() {
+ @Override
+ public Future start() {
+ return SettableFuture.create();
+ }
+
+ @Override
+ public boolean initialized() {
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+ };
+ }
+
public static class TestEventProcessor implements EventProcessor {
List events = new ArrayList<>();
@@ -76,9 +135,9 @@ public static VariationOrRollout fallthroughVariation(int variation) {
return new VariationOrRollout(variation, null);
}
- public static FeatureFlag booleanFlagWithClauses(Clause... clauses) {
- Rule rule = new Rule(Arrays.asList(clauses), 1, null);
- return new FeatureFlagBuilder("feature")
+ public static FeatureFlag booleanFlagWithClauses(String key, Clause... clauses) {
+ Rule rule = new Rule(null, Arrays.asList(clauses), 1, null);
+ return new FeatureFlagBuilder(key)
.on(true)
.rules(Arrays.asList(rule))
.fallthrough(fallthroughVariation(0))
@@ -87,6 +146,18 @@ public static FeatureFlag booleanFlagWithClauses(Clause... clauses) {
.build();
}
+ public static FeatureFlag flagWithValue(String key, JsonElement value) {
+ return new FeatureFlagBuilder(key)
+ .on(false)
+ .offVariation(0)
+ .variations(value)
+ .build();
+ }
+
+ public static EvaluationDetail simpleEvaluation(int variation, JsonElement value) {
+ return new EvaluationDetail<>(EvaluationReason.fallthrough(), variation, value);
+ }
+
public static Matcher hasJsonProperty(final String name, JsonElement value) {
return hasJsonProperty(name, equalTo(value));
}