-
Notifications
You must be signed in to change notification settings - Fork 55
Even more v2 things. #65
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,13 +4,19 @@ | |
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.JsonPrimitive; | ||
| import org.apache.commons.codec.binary.Hex; | ||
| import org.apache.http.annotation.ThreadSafe; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import javax.crypto.Mac; | ||
| import javax.crypto.spec.SecretKeySpec; | ||
| import java.io.Closeable; | ||
| import java.io.IOException; | ||
| import java.io.UnsupportedEncodingException; | ||
| 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; | ||
|
|
@@ -26,33 +32,37 @@ | |
| @ThreadSafe | ||
| public class LDClient implements Closeable { | ||
| private static final Logger logger = LoggerFactory.getLogger(LDClient.class); | ||
| private static final String HMAC_ALGORITHM = "HmacSHA256"; | ||
| protected static final String CLIENT_VERSION = getClientVersion(); | ||
|
|
||
| private final LDConfig config; | ||
| private final String sdkKey; | ||
| private final FeatureRequestor requestor; | ||
| private final EventProcessor eventProcessor; | ||
| private UpdateProcessor updateProcessor; | ||
| protected static final String CLIENT_VERSION = getClientVersion(); | ||
|
|
||
| /** | ||
| * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most | ||
| * cases, you should use this constructor. | ||
| * | ||
| * @param apiKey the API key for your account | ||
| * @param sdkKey the SDK key for your LaunchDarkly environment | ||
| */ | ||
| public LDClient(String apiKey) { | ||
| this(apiKey, LDConfig.DEFAULT); | ||
| public LDClient(String sdkKey) { | ||
| this(sdkKey, LDConfig.DEFAULT); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new client to connect to LaunchDarkly with a custom configuration. This constructor | ||
| * can be used to configure advanced client features, such as customizing the LaunchDarkly base URL. | ||
| * | ||
| * @param apiKey the API key for your account | ||
| * @param sdkKey the SDK key for your LaunchDarkly environment | ||
| * @param config a client configuration object | ||
| */ | ||
| public LDClient(String apiKey, LDConfig config) { | ||
| public LDClient(String sdkKey, LDConfig config) { | ||
| this.config = config; | ||
| this.requestor = createFeatureRequestor(apiKey, config); | ||
| this.eventProcessor = createEventProcessor(apiKey, config); | ||
| this.sdkKey = sdkKey; | ||
| this.requestor = createFeatureRequestor(sdkKey, config); | ||
| this.eventProcessor = createEventProcessor(sdkKey, config); | ||
|
|
||
| if (config.offline) { | ||
| logger.info("Starting LaunchDarkly client in offline mode"); | ||
|
|
@@ -66,7 +76,7 @@ public LDClient(String apiKey, LDConfig config) { | |
|
|
||
| if (config.stream) { | ||
| logger.info("Enabling streaming API"); | ||
| this.updateProcessor = createStreamProcessor(apiKey, config, requestor); | ||
| this.updateProcessor = createStreamProcessor(sdkKey, config, requestor); | ||
| } else { | ||
| logger.info("Disabling streaming API"); | ||
| this.updateProcessor = createPollingProcessor(config); | ||
|
|
@@ -91,18 +101,18 @@ public boolean initialized() { | |
| } | ||
|
|
||
| @VisibleForTesting | ||
| protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { | ||
| return new FeatureRequestor(apiKey, config); | ||
| protected FeatureRequestor createFeatureRequestor(String sdkKey, LDConfig config) { | ||
| return new FeatureRequestor(sdkKey, config); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { | ||
| return new EventProcessor(apiKey, config); | ||
| protected EventProcessor createEventProcessor(String sdkKey, LDConfig config) { | ||
| return new EventProcessor(sdkKey, config); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { | ||
| return new StreamProcessor(apiKey, config, requestor); | ||
| protected StreamProcessor createStreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor) { | ||
| return new StreamProcessor(sdkKey, config, requestor); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
|
|
@@ -174,33 +184,42 @@ private void sendFlagRequestEvent(String featureKey, LDUser user, JsonElement va | |
| } | ||
|
|
||
| /** | ||
| * Returns a map from feature flag keys to Boolean feature flag values for a given user. The map will contain {@code null} | ||
| * entries for any flags that are off or for any feature flags with non-boolean variations. If the client is offline or | ||
| * has not been initialized, a {@code null} map will be returned. | ||
| * Returns a map from feature flag keys to {@code JsonElement} feature flag values for a given user. | ||
| * If the result of a flag's evaluation would have returned the default variation, it will have a null entry | ||
| * in the map. If the client is offline, has not been initialized, or a null user or user with null/empty user key a {@code null} map will be returned. | ||
| * This method will not send analytics events back to LaunchDarkly. | ||
| * <p> | ||
| * 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 | ||
| * @return a map from feature flag keys to JsonElement values for the specified user | ||
| * @return a map from feature flag keys to {@code JsonElement} for the specified user | ||
| */ | ||
| public Map<String, Boolean> allFlags(LDUser user) { | ||
| public Map<String, JsonElement> allFlags(LDUser user) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One step we need to take with the new SDKs is provide a change log / migration guide. This is one of the changes that will be API breaking for customers. Perhaps now is a good time to start creating that doc? It should be part of the release notes, but I suggest tabulating them it on quip for now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the changes should be in the Github release- I'll start a draft for 2.0.0 now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if (isOffline()) { | ||
| logger.warn("allFlags() was called when client is in offline mode! Returning null."); | ||
| return null; | ||
| } | ||
|
|
||
| if (!initialized()) { | ||
| logger.warn("allFlags() was called before Client has been initialized! Returning null."); | ||
| return null; | ||
| } | ||
|
|
||
| if (user == null || user.getKeyAsString().isEmpty()) { | ||
| logger.warn("allFlags() was called with null user or null/empty user key! returning null"); | ||
| return null; | ||
| } | ||
|
|
||
| Map<String, FeatureFlag> flags = this.config.featureStore.all(); | ||
| Map<String, Boolean> result = new HashMap<>(); | ||
| Map<String, JsonElement> result = new HashMap<>(); | ||
|
|
||
| for (String key : flags.keySet()) { | ||
| JsonElement evalResult = evaluate(key, user, null); | ||
| if (evalResult.isJsonPrimitive() && evalResult.getAsJsonPrimitive().isBoolean()) { | ||
| result.put(key, evalResult.getAsBoolean()); | ||
| for (Map.Entry<String, FeatureFlag> entry : flags.entrySet()) { | ||
| try { | ||
| JsonElement evalResult = entry.getValue().evaluate(user, config.featureStore).getValue(); | ||
| result.put(entry.getKey(), evalResult); | ||
|
|
||
| } catch (EvaluationException e) { | ||
| logger.error("Exception caught when evaluating all flags:", e); | ||
| } | ||
| } | ||
| return result; | ||
|
|
@@ -315,22 +334,15 @@ private JsonElement evaluate(String featureKey, LDUser user, JsonElement default | |
| sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue, null); | ||
| return defaultValue; | ||
| } | ||
| if (featureFlag.isOn()) { | ||
| FeatureFlag.EvalResult evalResult = featureFlag.evaluate(user, config.featureStore); | ||
| if (!isOffline()) { | ||
| for (FeatureRequestEvent event : evalResult.getPrerequisiteEvents()) { | ||
| eventProcessor.sendEvent(event); | ||
| } | ||
| } | ||
| if (evalResult.getValue() != null) { | ||
| sendFlagRequestEvent(featureKey, user, evalResult.getValue(), defaultValue, featureFlag.getVersion()); | ||
| return evalResult.getValue(); | ||
| } | ||
| FeatureFlag.EvalResult evalResult = featureFlag.evaluate(user, config.featureStore); | ||
| if (!isOffline()) { | ||
| for (FeatureRequestEvent event : evalResult.getPrerequisiteEvents()) { | ||
| eventProcessor.sendEvent(event); | ||
| } | ||
| } | ||
| JsonElement offVariation = featureFlag.getOffVariationValue(); | ||
| if (offVariation != null) { | ||
| sendFlagRequestEvent(featureKey, user, offVariation, defaultValue, featureFlag.getVersion()); | ||
| return offVariation; | ||
| if (evalResult.getValue() != null) { | ||
| sendFlagRequestEvent(featureKey, user, evalResult.getValue(), defaultValue, featureFlag.getVersion()); | ||
| return evalResult.getValue(); | ||
| } | ||
| } catch (Exception e) { | ||
| logger.error("Encountered exception in LaunchDarkly client", e); | ||
|
|
@@ -368,6 +380,25 @@ public boolean isOffline() { | |
| return config.offline; | ||
| } | ||
|
|
||
| /** | ||
| * For more info: <a href=https://github.com/launchdarkly/js-client#secure-mode>https://github.com/launchdarkly/js-client#secure-mode</a> | ||
| * @param user The User to be hashed along with the sdk key | ||
| * @return the hash, or null if the hash could not be calculated. | ||
| */ | ||
| public String secureModeHash(LDUser user) { | ||
| if (user == null || user.getKeyAsString().isEmpty()) { | ||
| return null; | ||
| } | ||
| try { | ||
| Mac mac = Mac.getInstance(HMAC_ALGORITHM); | ||
| mac.init(new SecretKeySpec(sdkKey.getBytes(), HMAC_ALGORITHM)); | ||
| return Hex.encodeHexString(mac.doFinal(user.getKeyAsString().getBytes("UTF8"))); | ||
| } catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e) { | ||
| logger.error("Could not generate secure mode hash", e); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static String getClientVersion() { | ||
| Class clazz = LDConfig.class; | ||
| String className = clazz.getSimpleName() + ".class"; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this change about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved all flag eval logic into the FeatureFlag class. This enables us to call flag.evaluate() in the client's allFLags() method without additional logic around on/off behavior. Also, from an OO perspective it makes more sense: the object contains the data + the behavior.