Skip to content

Commit

Permalink
Merge pull request #278 from splitio/get-treatments
Browse files Browse the repository at this point in the history
Get treatments
  • Loading branch information
ldecheverz-split committed Dec 14, 2021
2 parents 254a9c2 + e8fb730 commit ecb2198
Show file tree
Hide file tree
Showing 17 changed files with 699 additions and 172 deletions.

This file was deleted.

128 changes: 128 additions & 0 deletions client/src/main/java/io/split/client/SplitClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.split.client.api.Key;
import io.split.client.api.SplitResult;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;

Expand Down Expand Up @@ -139,6 +140,133 @@ public interface SplitClient {
*/
SplitResult getTreatmentWithConfig(Key key, String split, Map<String, Object> attributes);

/**
* Returns a map of feature name and treatments to show this key for these features. The set of treatments
* for a feature can be configured on the Split web console.
* <p/>
* <p/>
* This method returns for each feature the string 'control' if:
* <ol>
* <li>Any of the parameters were null</li>
* <li>There was an exception in evaluating the treatment</li>
* <li>The SDK does not know of the existence of this feature</li>
* <li>The feature was deleted through the web console.</li>
* </ol>
* 'control' is a reserved treatment (you cannot create a treatment with the
* same name) to highlight these exceptional circumstances.
* <p/>
* <p/>
* The sdk returns for each feature the default treatment of this feature if:
* <ol>
* <li>The feature was killed</li>
* <li>The key did not match any of the conditions in the feature roll-out plan</li>
* </ol>
* The default treatment of a feature is set on the Split web console.
* <p/>
* <p/>
* This method does not throw any exceptions. It also never returns null.
*
* @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @return for each feature the evaluated treatment, the default treatment for each feature, or 'control'.
*/
Map<String, String> getTreatments(String key, List<String> splits);

/**
* This method is useful when you want to determine the treatments to show
* to a customer (user, account etc.) based on an attribute of that customer
* instead of their key.
* <p/>
* <p/>
* Examples include showing different treatments to users on trial plan
* vs. premium plan. Another example is to show different treatments
* to users created after a certain date.
*
* @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return the evaluated treatment, the default treatment of this feature, or 'control'.
*/
Map<String, String> getTreatments(String key, List<String> splits, Map<String, Object> attributes);

/**
* To understand why this method is useful, consider the following simple Split as an example:
*
* if user is in segment employees then split 100%:on
* else if user is in segment all then split 20%:on,80%:off
*
* There are two concepts here: matching and bucketing. Matching
* refers to ‘user is in segment employees’ or ‘user is in segment
* all’ whereas bucketing refers to ‘100%:on’ or ‘20%:on,80%:off’.
*
* By default, the same customer key is used for both matching and
* bucketing. However, for some advanced use cases, you may want
* to use different keys. For such cases, use this method.
*
* As an example, suppose you want to rollout to percentages of
* users in specific accounts. You can achieve that by matching
* via account id, but bucketing by user id.
*
* Another example is when you want to ensure that a user continues to get
* the same treatment after they sign up for your product that they used
* to get when they were simply a visitor to your site. In that case,
* before they sign up, you can use their visitor id for both matching and bucketing, but
* post log-in you can use their user id for matching and visitor id for bucketing.
*
*
* @param key the matching and bucketing keys. MUST NOT be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @param attributes of the entity (user, account etc.) to use in evaluation. Can be null or empty.
*
* @return for each feature the evaluated treatment, the default treatment of the feature, or 'control'.
*/
Map<String, String> getTreatments(Key key, List<String> splits, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(String, List<String>)} but it returns the configuration associated to the
* matching treatments if any. Otherwise {@link SplitResult.configurations()} will be null.
* <p/>
* <p/>
* Examples include showing a different treatment to users on trial plan
* vs. premium plan. Another example is to show a different treatment
* to users created after a certain date.
*
* @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @return Map<String, SplitResult> containing for each feature the evaluated treatment (the default treatment of this feature, or 'control') and
* a configuration associated to this treatment if set.
*/
Map<String, SplitResult> getTreatmentsWithConfig(String key, List<String> splits);

/**
* Same as {@link #getTreatments(String, List<String>, Map)} but it returns for each feature the configuration associated to the
* matching treatment if any. Otherwise {@link SplitResult.configurations()} will be null.
* <p/>
* <p/>
* Examples include showing a different treatment to users on trial plan
* vs. premium plan. Another example is to show a different treatment
* to users created after a certain date.
*
* @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return for each feature a SplitResult containing the evaluated treatment (the default treatment of this feature, or 'control') and
* a configuration associated to this treatment if set.
*/
Map<String, SplitResult> getTreatmentsWithConfig(String key, List<String> splits, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(Key, List<String>, Map)} but it returns for each feature the configuration associated to the
* matching treatment if any. Otherwise {@link SplitResult.configurations()} will be null.
*
* @param key the matching and bucketing keys. MUST NOT be null.
* @param splits the features we want to evaluate. MUST NOT be null.
* @param attributes of the entity (user, account etc.) to use in evaluation. Can be null or empty.
*
* @return for each feature a SplitResult containing the evaluated treatment (the default treatment of this feature, or 'control') and
* a configuration associated to this treatment if set.
*/
Map<String, SplitResult> getTreatmentsWithConfig(Key key, List<String> splits, Map<String, Object> attributes);

/**
* Destroys the background processes and clears the cache, releasing the resources used by
Expand Down
10 changes: 10 additions & 0 deletions client/src/main/java/io/split/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,16 @@ public Builder operationMode(OperationMode mode) {
return this;
}

/**
*
* @param storage mode
* @return this builder
*/
public Builder storageMode(StorageMode mode) {
_storageMode = mode;
return this;
}

/**
* Storage wrapper
*
Expand Down
117 changes: 111 additions & 6 deletions client/src/main/java/io/split/client/SplitClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
Expand Down Expand Up @@ -102,6 +104,40 @@ public SplitResult getTreatmentWithConfig(Key key, String split, Map<String, Obj
return getTreatmentWithConfigInternal(key.matchingKey(), key.bucketingKey(), split, attributes, MethodEnum.TREATMENT_WITH_CONFIG);
}

@Override
public Map<String, String> getTreatments(String key, List<String> splits) {
return getTreatments(key, splits, Collections.emptyMap());
}

@Override
public Map<String, String> getTreatments(String key, List<String> splits, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, splits, attributes, MethodEnum.TREATMENTS)
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
}

@Override
public Map<String, String> getTreatments(Key key, List<String> splits, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), splits, attributes, MethodEnum.TREATMENTS)
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
}

@Override
public Map<String, SplitResult> getTreatmentsWithConfig(String key, List<String> splits) {
return getTreatmentsWithConfigInternal(key, null, splits, Collections.<String, Object>emptyMap(), MethodEnum.TREATMENTS_WITH_CONFIG);
}

@Override
public Map<String, SplitResult> getTreatmentsWithConfig(String key, List<String> splits, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, splits, attributes, MethodEnum.TREATMENTS_WITH_CONFIG);
}

@Override
public Map<String, SplitResult> getTreatmentsWithConfig(Key key, List<String> splits, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), splits, attributes, MethodEnum.TREATMENTS_WITH_CONFIG);
}

@Override
public boolean track(String key, String trafficType, String eventType) {
Event event = createEvent(key, trafficType, eventType);
Expand Down Expand Up @@ -187,10 +223,8 @@ private boolean track(Event event) {
private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bucketingKey, String split, Map<String, Object> attributes, MethodEnum methodEnum) {
long initTime = System.currentTimeMillis();
try {
if(!_gates.isSDKReady()){
_log.warn(methodEnum.getMethod() + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method");
_telemetryConfigProducer.recordNonReadyUsage();
}
checkSDKReady(methodEnum);

if (_container.isDestroyed()) {
_log.error("Client has already been destroyed - no calls possible");
return SPLIT_RESULT_CONTROL;
Expand All @@ -216,8 +250,8 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu

if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
_log.warn(
"getTreatment: you passed \"" + split + "\" that does not exist in this environment, " +
"please double check what Splits exist in the web console.");
"%s: you passed \"" + split + "\" that does not exist in this environment, " +
"please double check what Splits exist in the web console.", methodEnum.getMethod());
return SPLIT_RESULT_CONTROL;
}

Expand Down Expand Up @@ -245,6 +279,64 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
}
}

private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matchingKey, String bucketingKey, List<String> splits, Map<String, Object> attributes, MethodEnum methodEnum) {
long initTime = System.currentTimeMillis();
if(splits == null) {
_log.error("%s: split_names must be a non-empty array", methodEnum.getMethod());
return new HashMap<>();
}
try{
checkSDKReady(methodEnum);
if (_container.isDestroyed()) {
_log.error("Client has already been destroyed - no calls possible");
return createMapControl(splits);
}

if (!KeyValidator.isValid(matchingKey, "matchingKey", _config.maxStringLength(), methodEnum.getMethod())) {
return createMapControl(splits);
}

if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
return createMapControl(splits);
}
else if(splits.isEmpty()) {
_log.error("%s: split_names must be a non-empty array", methodEnum.getMethod());
return new HashMap<>();
}
splits = SplitNameValidator.areValid(splits, methodEnum.getMethod());
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeatures(matchingKey, bucketingKey, splits, attributes);
List<Impression> impressions = new ArrayList<>();
Map<String, SplitResult> result = new HashMap<>();
evaluatorResult.keySet().forEach(t -> {
if (evaluatorResult.get(t).treatment.equals(Treatments.CONTROL) && evaluatorResult.get(t).label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
_log.warn(
"%s: you passed \"" + t + "\" that does not exist in this environment, " +
"please double check what Splits exist in the web console.", methodEnum.getMethod());
result.put(t, SPLIT_RESULT_CONTROL);
}
else {
result.put(t,new SplitResult(evaluatorResult.get(t).treatment, evaluatorResult.get(t).configurations));
impressions.add(new Impression(matchingKey, bucketingKey, t, evaluatorResult.get(t).treatment, System.currentTimeMillis(), evaluatorResult.get(t).label, evaluatorResult.get(t).changeNumber, attributes));
}
});

_telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis()-initTime);
//Track of impressions
if(impressions.size() > 0) {
_impressionManager.track(impressions);
}
return result;
} catch (Exception e) {
try {
_telemetryEvaluationProducer.recordException(methodEnum);
_log.error("CatchAll Exception", e);
} catch (Exception e1) {
// ignore
}
return createMapControl(splits);
}
}

private void recordStats(String matchingKey, String bucketingKey, String split, long start, String result,
String operation, String label, Long changeNumber, Map<String, Object> attributes) {
try {
Expand All @@ -262,4 +354,17 @@ private Event createEvent(String key, String trafficType, String eventType) {
event.timestamp = System.currentTimeMillis();
return event;
}

private void checkSDKReady(MethodEnum methodEnum) {
if(!_gates.isSDKReady()){
_log.warn(methodEnum.getMethod() + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method");
_telemetryConfigProducer.recordNonReadyUsage();
}
}

private Map<String, SplitResult> createMapControl(List<String> splits) {
Map<String, SplitResult> result = new HashMap<>();
splits.forEach(s -> result.put(s, SPLIT_RESULT_CONTROL));
return result;
}
}
Loading

0 comments on commit ecb2198

Please sign in to comment.