Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get treatments #278

Merged
merged 13 commits into from
Dec 14, 2021

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
121 changes: 117 additions & 4 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,46 @@ 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) {
Map<String, SplitResult> results = getTreatmentsWithConfigInternal(key, null, splits, attributes, MethodEnum.TREATMENTS);
if(results == null) {
return null;
}
return results.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) {
Map<String, SplitResult> results = getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), splits, attributes, MethodEnum.TREATMENTS);
Map<String, String> resultsWrapped = new HashMap<>();
for(String split : results.keySet()) {
resultsWrapped.put(split, results.get(split).treatment());
}
return resultsWrapped;
}

@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 +229,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 Down Expand Up @@ -245,6 +285,66 @@ 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) {
Map<String, SplitResult> result = new HashMap<>();
long initTime = System.currentTimeMillis();
if(splits == null) {
_log.error("getTreatments: split_names must be a non-empty array");
return null;
}
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("getTreatments: split_names must be a non-empty array");
return result;
}
splits = SplitNameValidator.areValid(splits, methodEnum.getMethod());
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeatures(matchingKey, bucketingKey, splits, attributes);
List<Impression> impressions = new ArrayList<>();

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(
"getTreatment: you passed \"" + t + "\" that does not exist in this environment, " +
"please double check what Splits exist in the web console.");
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);
}
} catch (Exception e) {
try {
_telemetryEvaluationProducer.recordException(methodEnum);
_log.error("CatchAll Exception", e);
} catch (Exception e1) {
// ignore
}
return createMapControl(splits);

}
return result;
}

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 +362,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