Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
57cac15
Added models and updated config and validator
chillaq Sep 4, 2025
5f2084d
Updated evaluator
chillaq Sep 4, 2025
455cf27
Update client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java
chillaq Sep 4, 2025
1f3e613
Update client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java
chillaq Sep 4, 2025
6004e55
Update client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java
chillaq Sep 4, 2025
cda0248
Update client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java
chillaq Sep 4, 2025
6a74292
Update client/src/main/java/io/split/client/SplitClientConfig.java
chillaq Sep 5, 2025
0ba014e
Update client/src/main/java/io/split/client/dtos/FallbackTreatmentsCo…
chillaq Sep 5, 2025
0815b1a
Update client/src/main/java/io/split/inputValidation/FallbackTreatmen…
chillaq Sep 5, 2025
092207b
polish
chillaq Sep 5, 2025
be872d2
added fallback calculator
chillaq Sep 5, 2025
23e25e6
Merge pull request #597 from splitio/FME-9875-fallback-config
chillaq Sep 8, 2025
d0c503b
Merge pull request #598 from splitio/FME-9876-fallback-evaluator
chillaq Sep 8, 2025
4af6f4f
Update client class
chillaq Sep 8, 2025
c26b3fa
polish
chillaq Sep 8, 2025
a639d54
polishing
chillaq Sep 9, 2025
480d1f3
polishing
chillaq Sep 9, 2025
076a850
Convert fallback config to String
chillaq Sep 9, 2025
92c8b1b
Merge pull request #599 from splitio/FME-9878-fallback-client
chillaq Sep 9, 2025
f29078e
Updated Factory
chillaq Sep 9, 2025
f837b0d
Merge pull request #600 from splitio/FME-9874-fallback-factory
chillaq Sep 9, 2025
567d2f1
Added label not ready for fallback
chillaq Sep 10, 2025
ae44a04
Merge pull request #601 from splitio/fallback-label-not-ready-fix
chillaq Sep 10, 2025
f93b8b1
Updated version and changes
chillaq Sep 12, 2025
d29359b
Merge pull request #602 from splitio/FME-9880-changes
chillaq Sep 12, 2025
5a87a2e
polish
chillaq Sep 12, 2025
fb169e8
polish
chillaq Sep 12, 2025
8bf147c
Merge pull request #603 from splitio/feature/fallback-treatments
chillaq Sep 12, 2025
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
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
4.18.0 (Sep 12, 2025)
- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.

4.17.0 (Aug 22, 2025)
- Added a maximum size payload when posting unique keys telemetry in batches
- Added ProxyConfiguration parameter to support proxies, including Harness Forward Proxy, allowing also for more secured authentication options: MTLS, Bearer token and user/password authentication. Read more in our docs.
Expand Down
4 changes: 2 additions & 2 deletions client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<parent>
<groupId>io.split.client</groupId>
<artifactId>java-client-parent</artifactId>
<version>4.17.0</version>
<version>4.18.0</version>
</parent>
<version>4.17.0</version>
<version>4.18.0</version>
<artifactId>java-client</artifactId>
<packaging>jar</packaging>
<name>Java Client</name>
Expand Down
48 changes: 46 additions & 2 deletions client/src/main/java/io/split/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.split.client;

import io.split.client.dtos.FallbackTreatment;
import io.split.client.dtos.FallbackTreatmentsConfiguration;
import io.split.client.dtos.ProxyConfiguration;
import io.split.client.impressions.ImpressionListener;
import io.split.client.impressions.ImpressionsManager;
Expand All @@ -16,10 +18,13 @@
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ThreadFactory;
import java.io.InputStream;

import static io.split.inputValidation.FallbackTreatmentValidator.isValidByFlagTreatment;
import static io.split.inputValidation.FallbackTreatmentValidator.isValidTreatment;
import static io.split.inputValidation.FlagSetsValidator.cleanup;

/**
Expand Down Expand Up @@ -91,6 +96,7 @@ private HttpScheme() {
private final CustomStorageWrapper _customStorageWrapper;
private final StorageMode _storageMode;
private final ThreadFactory _threadFactory;
private final FallbackTreatmentsConfiguration _fallbackTreatments;

// Proxy configs
private final ProxyConfiguration _proxyConfiguration;
Expand Down Expand Up @@ -163,7 +169,8 @@ private SplitClientConfig(String endpoint,
HashSet<String> flagSetsFilter,
int invalidSets,
CustomHeaderDecorator customHeaderDecorator,
CustomHttpModule alternativeHTTPModule) {
CustomHttpModule alternativeHTTPModule,
FallbackTreatmentsConfiguration fallbackTreatments) {
_endpoint = endpoint;
_eventsEndpoint = eventsEndpoint;
_featuresRefreshRate = pollForFeatureChangesEveryNSeconds;
Expand Down Expand Up @@ -218,6 +225,7 @@ private SplitClientConfig(String endpoint,
_invalidSets = invalidSets;
_customHeaderDecorator = customHeaderDecorator;
_alternativeHTTPModule = alternativeHTTPModule;
_fallbackTreatments = fallbackTreatments;

Properties props = new Properties();
try {
Expand Down Expand Up @@ -436,6 +444,8 @@ public boolean isSdkEndpointOverridden() {

public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; }

public FallbackTreatmentsConfiguration fallbackTreatments() { return _fallbackTreatments; }

public static final class Builder {
private String _endpoint = SDK_ENDPOINT;
private boolean _endpointSet = false;
Expand Down Expand Up @@ -494,6 +504,7 @@ public static final class Builder {
private int _invalidSetsCount = 0;
private CustomHeaderDecorator _customHeaderDecorator = null;
private CustomHttpModule _alternativeHTTPModule = null;
private FallbackTreatmentsConfiguration _fallbackTreatments;

public Builder() {
}
Expand Down Expand Up @@ -1022,6 +1033,17 @@ public Builder alternativeHTTPModule(CustomHttpModule alternativeHTTPModule) {
return this;
}

/**
* Fallback Treatments
*
* @param fallbackTreatments
* @return this builder
*/
public Builder fallbackTreatments(FallbackTreatmentsConfiguration fallbackTreatments) {
_fallbackTreatments = fallbackTreatments;
return this;
}

/**
* Thread Factory
*
Expand Down Expand Up @@ -1158,6 +1180,25 @@ private void verifyProxy() {
}
}

private void verifyFallbackTreatments() {
if (_fallbackTreatments == null)
return;

FallbackTreatment processedGlobalFallbackTreatment = _fallbackTreatments.getGlobalFallbackTreatment();
Map<String, FallbackTreatment> processedByFlagFallbackTreatment = _fallbackTreatments.getByFlagFallbackTreatment();

if (_fallbackTreatments.getGlobalFallbackTreatment() != null) {
processedGlobalFallbackTreatment = new FallbackTreatment(
isValidTreatment(_fallbackTreatments.getGlobalFallbackTreatment().getTreatment(), "Fallback treatments"),
_fallbackTreatments.getGlobalFallbackTreatment().getConfig());
}

if (_fallbackTreatments.getByFlagFallbackTreatment() != null) {
processedByFlagFallbackTreatment = isValidByFlagTreatment(_fallbackTreatments.getByFlagFallbackTreatment(), "config");
}
_fallbackTreatments = new FallbackTreatmentsConfiguration(processedGlobalFallbackTreatment, processedByFlagFallbackTreatment);
}

public SplitClientConfig build() {

verifyRates();
Expand All @@ -1172,6 +1213,8 @@ public SplitClientConfig build() {

verifyProxy();

verifyFallbackTreatments();

if (_numThreadsForSegmentFetch <= 0) {
throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero");
}
Expand Down Expand Up @@ -1230,7 +1273,8 @@ public SplitClientConfig build() {
_flagSetsFilter,
_invalidSetsCount,
_customHeaderDecorator,
_alternativeHTTPModule);
_alternativeHTTPModule,
_fallbackTreatments);
}
}
}
112 changes: 62 additions & 50 deletions client/src/main/java/io/split/client/SplitClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import com.google.gson.GsonBuilder;
import io.split.client.api.Key;
import io.split.client.api.SplitResult;
import io.split.client.dtos.DecoratedImpression;
import io.split.client.dtos.EvaluationOptions;
import io.split.client.dtos.Event;
import io.split.client.dtos.*;
import io.split.client.events.EventsStorageProducer;
import io.split.client.impressions.Impression;
import io.split.client.impressions.ImpressionsManager;
Expand All @@ -14,7 +12,6 @@
import io.split.engine.evaluator.Evaluator;
import io.split.engine.evaluator.EvaluatorImp;
import io.split.engine.evaluator.Labels;
import io.split.grammar.Treatments;
import io.split.inputValidation.EventsValidator;
import io.split.inputValidation.KeyValidator;
import io.split.inputValidation.SplitNameValidator;
Expand Down Expand Up @@ -49,7 +46,6 @@
* @author adil
*/
public final class SplitClientImpl implements SplitClient {
public static final SplitResult SPLIT_RESULT_CONTROL = new SplitResult(Treatments.CONTROL, null);
private static final String CLIENT_DESTROY = "Client has already been destroyed - no calls possible";
private static final String CATCHALL_EXCEPTION = "CatchAll Exception";
private static final String MATCHING_KEY = "matchingKey";
Expand All @@ -66,6 +62,7 @@ public final class SplitClientImpl implements SplitClient {
private final TelemetryEvaluationProducer _telemetryEvaluationProducer;
private final TelemetryConfigProducer _telemetryConfigProducer;
private final FlagSetsFilter _flagSetsFilter;
private final FallbackTreatmentCalculator _fallbackTreatmentCalculator;

public SplitClientImpl(SplitFactory container,
SplitCacheConsumer splitCacheConsumer,
Expand All @@ -76,7 +73,8 @@ public SplitClientImpl(SplitFactory container,
Evaluator evaluator,
TelemetryEvaluationProducer telemetryEvaluationProducer,
TelemetryConfigProducer telemetryConfigProducer,
FlagSetsFilter flagSetsFilter) {
FlagSetsFilter flagSetsFilter,
FallbackTreatmentCalculator fallbackTreatmentCalculator) {
_container = container;
_splitCacheConsumer = checkNotNull(splitCacheConsumer);
_impressionManager = checkNotNull(impressionManager);
Expand All @@ -87,6 +85,7 @@ public SplitClientImpl(SplitFactory container,
_telemetryEvaluationProducer = checkNotNull(telemetryEvaluationProducer);
_telemetryConfigProducer = checkNotNull(telemetryConfigProducer);
_flagSetsFilter = flagSetsFilter;
_fallbackTreatmentCalculator = fallbackTreatmentCalculator;
}

@Override
Expand Down Expand Up @@ -492,31 +491,34 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu

if (_container.isDestroyed()) {
_log.error(CLIENT_DESTROY);
return SPLIT_RESULT_CONTROL;
return checkFallbackTreatment(featureFlag);
}

if (!KeyValidator.isValid(matchingKey, MATCHING_KEY, _config.maxStringLength(), methodEnum.getMethod())) {
return SPLIT_RESULT_CONTROL;
return checkFallbackTreatment(featureFlag);
}

if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
return SPLIT_RESULT_CONTROL;
return checkFallbackTreatment(featureFlag);
}

Optional<String> splitNameResult = SplitNameValidator.isValid(featureFlag, methodEnum.getMethod());
if (!splitNameResult.isPresent()) {
return SPLIT_RESULT_CONTROL;
return checkFallbackTreatment(featureFlag);
}
featureFlag = splitNameResult.get();
long start = System.currentTimeMillis();

EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(matchingKey, bucketingKey, featureFlag, attributes);

if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
_log.warn(String.format(
"%s: you passed \"%s\" that does not exist in this environment, " +
"please double check what feature flags exist in the Split user interface.", methodEnum.getMethod(), featureFlag));
return SPLIT_RESULT_CONTROL;
String label = result.label;
if (result.label != null && result.label.contains(Labels.DEFINITION_NOT_FOUND)) {
if (_gates.isSDKReady()) {
_log.warn(String.format(
"%s: you passed \"%s\" that does not exist in this environment, " +
"please double check what feature flags exist in the Split user interface.", methodEnum.getMethod(), featureFlag));
return checkFallbackTreatment(featureFlag);
}
label = result.label.replace(Labels.DEFINITION_NOT_FOUND, Labels.NOT_READY);
}

recordStats(
Expand All @@ -526,7 +528,7 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
start,
result.treatment,
String.format("sdk.%s", methodEnum.getMethod()),
_config.labelsEnabled() ? result.label : null,
_config.labelsEnabled() ? label : null,
result.changeNumber,
attributes,
result.track,
Expand All @@ -541,8 +543,17 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
} catch (Exception e1) {
// ignore
}
return SPLIT_RESULT_CONTROL;
return checkFallbackTreatment(featureFlag);
}
}

private SplitResult checkFallbackTreatment(String featureName) {
FallbackTreatment fallbackTreatment = _fallbackTreatmentCalculator.resolve(featureName, "");
String config = null;
if (fallbackTreatment.getConfig() != null) {
config = fallbackTreatment.getConfig();
}
return new SplitResult(fallbackTreatment.getTreatment(), config);
}

private String validateProperties(Map<String, Object> properties) {
Expand All @@ -563,6 +574,7 @@ private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matching
_log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
return new HashMap<>();
}

try {
checkSDKReady(methodEnum, featureFlagNames);
Map<String, SplitResult> result = validateBeforeEvaluate(featureFlagNames, matchingKey, methodEnum, bucketingKey);
Expand Down Expand Up @@ -601,47 +613,47 @@ private Map<String, SplitResult> getTreatmentsBySetsWithConfigInternal(String ma
if (cleanFlagSets.isEmpty()) {
return new HashMap<>();
}
List<String> featureFlagNames = new ArrayList<>();
try {
checkSDKReady(methodEnum);
Map<String, SplitResult> result = validateBeforeEvaluateByFlagSets(matchingKey, methodEnum,bucketingKey);
if(result != null) {
return result;
}
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey,
bucketingKey, new ArrayList<>(cleanFlagSets), attributes);
checkSDKReady(methodEnum);
Map<String, SplitResult> result = validateBeforeEvaluateByFlagSets(matchingKey, methodEnum,bucketingKey);
if(result != null) {
return result;
}
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey,
bucketingKey, new ArrayList<>(cleanFlagSets), attributes);

return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime,
validateProperties(evaluationOptions.getProperties()));
} catch (Exception e) {
try {
evaluatorResult.entrySet().forEach(flag -> {
if (flag.getValue().label != null &&
flag.getValue().label.contains(io.split.engine.evaluator.Labels.EXCEPTION)) {
_telemetryEvaluationProducer.recordException(methodEnum);
_log.error(CATCHALL_EXCEPTION, e);
} catch (Exception e1) {
// ignore
}
return createMapControl(featureFlagNames);
}
});
return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime,
validateProperties(evaluationOptions.getProperties()));
}

private Map<String, SplitResult> processEvaluatorResult(Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult,
MethodEnum methodEnum, String matchingKey, String bucketingKey, Map<String,
Object> attributes, long initTime, String properties){
List<DecoratedImpression> decoratedImpressions = 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(String.format("%s: you passed \"%s\" that does not exist in this environment please double check " +
"what feature flags exist in the Split user interface.", methodEnum.getMethod(), t));
result.put(t, SPLIT_RESULT_CONTROL);
} else {
result.put(t, new SplitResult(evaluatorResult.get(t).treatment, evaluatorResult.get(t).configurations));
decoratedImpressions.add(
new DecoratedImpression(
new Impression(matchingKey, bucketingKey, t, evaluatorResult.get(t).treatment, System.currentTimeMillis(),
evaluatorResult.get(t).label, evaluatorResult.get(t).changeNumber, attributes, properties),
evaluatorResult.get(t).track));
evaluatorResult.keySet().forEach(flag -> {
String label = evaluatorResult.get(flag).label;
if (evaluatorResult.get(flag).label != null &&
evaluatorResult.get(flag).label.contains(Labels.DEFINITION_NOT_FOUND)) {
if (_gates.isSDKReady()) {
_log.warn(String.format("%s: you passed \"%s\" that does not exist in this environment please double check " +
"what feature flags exist in the Split user interface.", methodEnum.getMethod(), flag));
result.put(flag, checkFallbackTreatment(flag));
return;
}
label = evaluatorResult.get(flag).label.replace(Labels.DEFINITION_NOT_FOUND, Labels.NOT_READY);
}
result.put(flag, new SplitResult(evaluatorResult.get(flag).treatment, evaluatorResult.get(flag).configurations));
decoratedImpressions.add(
new DecoratedImpression(
new Impression(matchingKey, bucketingKey, flag, evaluatorResult.get(flag).treatment, System.currentTimeMillis(),
label, evaluatorResult.get(flag).changeNumber, attributes, properties),
evaluatorResult.get(flag).track));
});
_telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis() - initTime);
if (!decoratedImpressions.isEmpty()) {
Expand Down Expand Up @@ -735,7 +747,7 @@ private void checkSDKReady(MethodEnum methodEnum) {

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