Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ Quick setup

import com.launchdarkly.client.*;

2. Create a new LDClient with your API key:
2. Create a new LDClient with your SDK key:

LDClient ldClient = new LDClient("YOUR_API_KEY");
LDClient ldClient = new LDClient("YOUR_SDK_KEY");

Your first feature flag
-----------------------
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/launchdarkly/client/EventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ class EventProcessor implements Closeable {
private final ScheduledExecutorService scheduler;
private final Random random = new Random();
private final BlockingQueue<Event> queue;
private final String apiKey;
private final String sdkKey;
private final LDConfig config;
private final Consumer consumer;

EventProcessor(String apiKey, LDConfig config) {
this.apiKey = apiKey;
EventProcessor(String sdkKey, LDConfig config) {
this.sdkKey = sdkKey;
this.queue = new ArrayBlockingQueue<>(config.capacity);
this.consumer = new Consumer(config);
this.config = config;
Expand Down Expand Up @@ -85,7 +85,7 @@ private void postEvents(List<Event> events) {
Gson gson = new Gson();
String json = gson.toJson(events);

HttpPost request = config.postEventsRequest(apiKey, "/bulk");
HttpPost request = config.postEventsRequest(sdkKey, "/bulk");
StringEntity entity = new StringEntity(json, "UTF-8");
entity.setContentType("application/json");
request.setEntity(entity);
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/com/launchdarkly/client/FeatureFlag.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,19 @@ static Map<String, FeatureFlag> fromJsonMap(String json) {
}

EvalResult evaluate(LDUser user, FeatureStore featureStore) throws EvaluationException {
if (user == null || user.getKeyAsString().isEmpty()) {
logger.warn("Null user or null/empty user key when evaluating flag: " + key + "; returning null");
return null;
}
List<FeatureRequestEvent> prereqEvents = new ArrayList<>();
JsonElement value = evaluate(user, featureStore, prereqEvents);
return new EvalResult(value, prereqEvents);
if (isOn()) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

JsonElement value = evaluate(user, featureStore, prereqEvents);
if (value != null) {
return new EvalResult(value, prereqEvents);
}
}
JsonElement offVariation = getOffVariationValue();
return new EvalResult(offVariation, prereqEvents);
}

// Returning either a JsonElement or null indicating prereq failure/error.
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/com/launchdarkly/client/FeatureRequestor.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
class FeatureRequestor {

public static final String GET_LATEST_FLAGS_PATH = "/sdk/latest-flags";
private final String apiKey;
private final String sdkKey;
private final LDConfig config;
private final CloseableHttpClient client;
private static final Logger logger = LoggerFactory.getLogger(FeatureRequestor.class);

FeatureRequestor(String apiKey, LDConfig config) {
this.apiKey = apiKey;
FeatureRequestor(String sdkKey, LDConfig config) {
this.sdkKey = sdkKey;
this.config = config;
this.client = createClient();
}
Expand Down Expand Up @@ -58,7 +58,7 @@ protected CloseableHttpClient createClient() {
Map<String, FeatureFlag> makeAllRequest() throws IOException {
HttpCacheContext context = HttpCacheContext.create();

HttpGet request = config.getRequest(apiKey, GET_LATEST_FLAGS_PATH);
HttpGet request = config.getRequest(sdkKey, GET_LATEST_FLAGS_PATH);

CloseableHttpResponse response = null;
try {
Expand Down Expand Up @@ -107,7 +107,7 @@ FeatureFlag makeRequest(String featureKey, boolean latest) throws IOException {

String resource = latest ? "/api/eval/latest-features/" : "/api/eval/features/";

HttpGet request = config.getRequest(apiKey,resource + featureKey);
HttpGet request = config.getRequest(sdkKey,resource + featureKey);

CloseableHttpResponse response = null;
try {
Expand Down
111 changes: 71 additions & 40 deletions src/main/java/com/launchdarkly/client/LDClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand Down
23 changes: 4 additions & 19 deletions src/main/java/com/launchdarkly/client/LDConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@ private URIBuilder getEventsBuilder() {
.setPort(eventsURI.getPort());
}

HttpGet getRequest(String apiKey, String path) {
HttpGet getRequest(String sdkKey, String path) {
URIBuilder builder = this.getBuilder().setPath(path);

try {
HttpGet request = new HttpGet(builder.build());
request.addHeader("Authorization", "api_key " + apiKey);
request.addHeader("Authorization", sdkKey);
request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION);

return request;
Expand All @@ -391,27 +391,12 @@ HttpGet getRequest(String apiKey, String path) {
}
}

HttpPost postRequest(String apiKey, String path) {
URIBuilder builder = this.getBuilder().setPath(path);

try {
HttpPost request = new HttpPost(builder.build());
request.addHeader("Authorization", "api_key " + apiKey);
request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION);

return request;
} catch (Exception e) {
logger.error("Unhandled exception in LaunchDarkly client", e);
return null;
}
}

HttpPost postEventsRequest(String apiKey, String path) {
HttpPost postEventsRequest(String sdkKey, String path) {
URIBuilder builder = this.getEventsBuilder().setPath(eventsURI.getPath() + path);

try {
HttpPost request = new HttpPost(builder.build());
request.addHeader("Authorization", "api_key " + apiKey);
request.addHeader("Authorization", sdkKey);
request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION);

return request;
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/com/launchdarkly/client/StreamProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ class StreamProcessor implements UpdateProcessor {

private final FeatureStore store;
private final LDConfig config;
private final String apiKey;
private final String sdkKey;
private final FeatureRequestor requestor;
private EventSource es;
private AtomicBoolean initialized = new AtomicBoolean(false);


StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) {
StreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor) {
this.store = config.featureStore;
this.config = config;
this.apiKey = apiKey;
this.sdkKey = sdkKey;
this.requestor = requestor;
}

Expand All @@ -41,7 +41,7 @@ public Future<Void> start() {
final VeryBasicFuture initFuture = new VeryBasicFuture();

Headers headers = new Headers.Builder()
.add("Authorization", "api_key " + this.apiKey)
.add("Authorization", this.sdkKey)
.add("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION)
.add("Accept", "text/event-stream")
.build();
Expand Down Expand Up @@ -102,7 +102,8 @@ public void onMessage(String name, MessageEvent event) throws Exception {

@Override
public void onError(Throwable throwable) {
logger.warn("Encountered EventSource error", throwable);
logger.error("Encountered EventSource error: " + throwable.getMessage());
logger.debug("", throwable);
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/launchdarkly/client/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static boolean handleResponse(Logger logger, HttpRequestBase request, CloseableH
return true;
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
logger.error("[401] Invalid API key when accessing URI: " + request.getURI().toString());
logger.error("[401] Invalid SDK key when accessing URI: " + request.getURI().toString());
} else {
logger.error("[" + statusCode + "] " + response.getStatusLine().getReasonPhrase() + " When accessing URI: " + request.getURI().toString());
}
Expand Down
Loading