Skip to content

Commit

Permalink
Merge branch 'SDKS-7439' into SDKS-7537
Browse files Browse the repository at this point in the history
  • Loading branch information
nmayorsplit committed Oct 2, 2023
2 parents 34c6f96 + b6a2588 commit 3b37861
Show file tree
Hide file tree
Showing 7 changed files with 524 additions and 220 deletions.
66 changes: 66 additions & 0 deletions client/src/main/java/io/split/client/SplitClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,72 @@ public interface SplitClient {
*/
Map<String, SplitResult> getTreatmentsWithConfig(Key key, List<String> featureFlagNames, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(String, List<String>, Map)} but it returns for each feature flag 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 or empty.
* @param flagSet the Flag Set name that you want to evaluate. MUST not be null or empty.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control'.
*/
Map<String, String> getTreatmentsByFlagSet(String key, String flagSet, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(String, List<String>, Map)} but it returns for each feature flag 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 or empty.
* @param flagSets the names of Flag Sets that you want to evaluate. MUST not be null or empty.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control'.
*/
Map<String, String> getTreatmentsByFlagSets(String key, List<String> flagSets, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(String, List<String>, Map)} but it returns for each feature flag 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 or empty.
* @param flagSet the Flag Set name that you want to evaluate. MUST not be null or empty.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return for each feature flag the evaluated treatment (the default treatment of this feature flag, or 'control') and a configuration
* associated to this treatment if set.
*/
Map<String, SplitResult> getTreatmentsWithConfigByFlagSet(String key, String flagSet, Map<String, Object> attributes);

/**
* Same as {@link #getTreatments(String, List<String>, Map)} but it returns for each feature flag 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 or empty.
* @param flagSets the names of Flag Sets that you want to evaluate. MUST not be null or empty.
* @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty.
* @return for each feature flag the evaluated treatment (the default treatment of this feature flag, or 'control') and a configuration
* associated to this treatment if set.
*/
Map<String, SplitResult> getTreatmentsWithConfigByFlagSets(String key, List<String> flagSets, Map<String, Object> attributes);

/**
* Destroys the background processes and clears the cache, releasing the resources used by
* the any instances of SplitClient or SplitManager generated by the client's parent SplitFactory
Expand Down
101 changes: 87 additions & 14 deletions client/src/main/java/io/split/client/SplitClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import io.split.engine.evaluator.Labels;
import io.split.grammar.Treatments;
import io.split.inputValidation.EventsValidator;
import io.split.inputValidation.FlagSetsValidResult;
import io.split.inputValidation.FlagSetsValidator;
import io.split.inputValidation.KeyValidator;
import io.split.inputValidation.SplitNameValidator;
import io.split.inputValidation.TrafficTypeValidator;
Expand All @@ -23,8 +25,10 @@
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -111,35 +115,61 @@ public Map<String, String> getTreatments(String key, List<String> featureFlagNam

@Override
public Map<String, String> getTreatments(String key, List<String> featureFlagNames, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, featureFlagNames, attributes, MethodEnum.TREATMENTS)
return getTreatmentsWithConfigInternal(key, null, featureFlagNames, null, 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> featureFlagNames, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagNames, attributes, MethodEnum.TREATMENTS)
return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagNames, null, 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> featureFlagNames) {
return getTreatmentsWithConfigInternal(key, null, featureFlagNames, Collections.<String, Object>emptyMap(),
return getTreatmentsWithConfigInternal(key, null, featureFlagNames, null, Collections.<String, Object>emptyMap(),
MethodEnum.TREATMENTS_WITH_CONFIG);
}

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

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

@Override
public Map<String, String> getTreatmentsByFlagSet(String key, String flagSet, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, null, new ArrayList<>(Arrays.asList(flagSet)),
attributes, MethodEnum.TREATMENTS_BY_FLAG_SET).entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
}

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

@Override
public Map<String, SplitResult> getTreatmentsWithConfigByFlagSet(String key, String flagSet, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, null, new ArrayList<>(Arrays.asList(flagSet)),
attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
}

@Override
public Map<String, SplitResult> getTreatmentsWithConfigByFlagSets(String key, List<String> flagSets, Map<String, Object> attributes) {
return getTreatmentsWithConfigInternal(key, null, null, flagSets,
attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
}

@Override
public boolean track(String key, String trafficType, String eventType) {
Event event = createEvent(key, trafficType, eventType);
Expand Down Expand Up @@ -283,9 +313,27 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
}

private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matchingKey, String bucketingKey, List<String> featureFlagNames,
Map<String, Object> attributes, MethodEnum methodEnum) {
List<String> sets, Map<String, Object> attributes, MethodEnum methodEnum) {

long initTime = System.currentTimeMillis();
if (featureFlagNames == null) {
FlagSetsValidResult flagSetsValidResult = null;
if (methodEnum == MethodEnum.TREATMENTS_BY_FLAG_SET || methodEnum == MethodEnum.TREATMENTS_BY_FLAG_SETS ||
methodEnum == MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SET || methodEnum == MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS) {
if (sets == null || sets.isEmpty()) {
_log.warn(String.format("%s: sets must be a non-empty array", methodEnum.getMethod()));
return new HashMap<>();
}
flagSetsValidResult = FlagSetsValidator.areValid(sets);
if (!flagSetsValidResult.getValid()) {
_log.warn("The sets are invalid");
return new HashMap<>();
}
if (filterSetsAreInConfig(flagSetsValidResult.getFlagSets()).isEmpty()) {
_log.warn("The sets are not in flagSetsFilter config");
return new HashMap<>();
}
featureFlagNames = getAllFlags(flagSetsValidResult.getFlagSets());
} else if (featureFlagNames == null) {
_log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
return new HashMap<>();
}
Expand All @@ -295,20 +343,24 @@ private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matching
_log.error("Client has already been destroyed - no calls possible");
return createMapControl(featureFlagNames);
}

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

if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
return createMapControl(featureFlagNames);
} else if (featureFlagNames.isEmpty()) {
_log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
return new HashMap<>();
}
featureFlagNames = SplitNameValidator.areValid(featureFlagNames, methodEnum.getMethod());
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeatures(matchingKey,
bucketingKey, featureFlagNames, attributes);
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult;
if (flagSetsValidResult != null) {
evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey, bucketingKey, new ArrayList<>(flagSetsValidResult.
getFlagSets()));
} else {
featureFlagNames = SplitNameValidator.areValid(featureFlagNames, methodEnum.getMethod());
evaluatorResult = _evaluator.evaluateFeatures(matchingKey, bucketingKey, featureFlagNames, attributes);
}

List<Impression> impressions = new ArrayList<>();
Map<String, SplitResult> result = new HashMap<>();
evaluatorResult.keySet().forEach(t -> {
Expand All @@ -323,9 +375,7 @@ private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matching
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);
}
Expand All @@ -341,6 +391,29 @@ private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matching
}
}

private List<String> filterSetsAreInConfig(HashSet<String> sets) {
HashSet<String> configSets = _config.getSetsFilter();
List<String> setsToReturn = new ArrayList<>();
for (String set : sets) {
if (!configSets.contains(set)) {
_log.warn(String.format("GetTreatmentsByFlagSets: you passed %s which is not part of the configured FlagSetsFilter, " +
"ignoring Flag Set.", set));
continue;
}
setsToReturn.add(set);
}
return setsToReturn;
}

private List<String> getAllFlags(HashSet<String> sets) {
Map<String, HashSet<String>> namesBySets = _splitCacheConsumer.getNamesByFlagSets(new ArrayList<>(sets));
HashSet<String> flags = new HashSet<>();
for (String set: namesBySets.keySet()) {
flags.addAll(namesBySets.get(set));
}
return new ArrayList<>(flags);
}

private void recordStats(String matchingKey, String bucketingKey, String featureFlagName, long start, String result,
String operation, String label, Long changeNumber, Map<String, Object> attributes) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ EvaluatorImp.TreatmentLabelAndChangeNumber evaluateFeature(String matchingKey, S
Map<String, Object> attributes);
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluateFeatures(String matchingKey, String bucketingKey,
List<String> featureFlags, Map<String, Object> attributes);
EvaluatorImp.ByFlagSetsResult evaluateFeaturesByFlagSets(String key, String bucketingKey, List<String> flagSets);
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluateFeaturesByFlagSets(String key, String bucketingKey, List<String> flagSets);
}
18 changes: 3 additions & 15 deletions client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,11 @@ public Map<String, TreatmentLabelAndChangeNumber> evaluateFeatures(String matchi
}

@Override
public ByFlagSetsResult evaluateFeaturesByFlagSets(String key, String bucketingKey, List<String> flagSets) {
Stopwatch stopwatch = Stopwatch.createStarted();
public Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluateFeaturesByFlagSets(String key, String bucketingKey,
List<String> flagSets) {
List<String> flagSetsWithNames = getFeatureFlagNamesByFlagSets(flagSets);
Map<String, TreatmentLabelAndChangeNumber> evaluations = evaluateFeatures(key, bucketingKey, flagSetsWithNames, null);
stopwatch.stop();
long millis = stopwatch.elapsed(TimeUnit.MILLISECONDS);
return new ByFlagSetsResult(evaluations, millis);
return evaluations;
}

private List<String> getFeatureFlagNamesByFlagSets(List<String> flagSets) {
Expand Down Expand Up @@ -155,16 +153,6 @@ private TreatmentLabelAndChangeNumber evaluateParsedSplit(String matchingKey, St
}
}

public static class ByFlagSetsResult {
public final Map<String, TreatmentLabelAndChangeNumber> evaluations;
public final long elapsedMilliseconds;

public ByFlagSetsResult(Map<String, TreatmentLabelAndChangeNumber> evaluations, long elapsed) {
this.evaluations = evaluations;
this.elapsedMilliseconds = elapsed;
}
}

public static final class TreatmentLabelAndChangeNumber {
public final String treatment;
public final String label;
Expand Down
Loading

0 comments on commit 3b37861

Please sign in to comment.