Skip to content

Commit 480e98e

Browse files
authored
Merge pull request #604 from splitio/development
Release 4.18.0
2 parents 5d33eb3 + 8bf147c commit 480e98e

30 files changed

+1468
-237
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
4.18.0 (Sep 12, 2025)
2+
- 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.
3+
14
4.17.0 (Aug 22, 2025)
25
- Added a maximum size payload when posting unique keys telemetry in batches
36
- 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.

client/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
<parent>
66
<groupId>io.split.client</groupId>
77
<artifactId>java-client-parent</artifactId>
8-
<version>4.17.0</version>
8+
<version>4.18.0</version>
99
</parent>
10-
<version>4.17.0</version>
10+
<version>4.18.0</version>
1111
<artifactId>java-client</artifactId>
1212
<packaging>jar</packaging>
1313
<name>Java Client</name>

client/src/main/java/io/split/client/SplitClientConfig.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.split.client;
22

3+
import io.split.client.dtos.FallbackTreatment;
4+
import io.split.client.dtos.FallbackTreatmentsConfiguration;
35
import io.split.client.dtos.ProxyConfiguration;
46
import io.split.client.impressions.ImpressionListener;
57
import io.split.client.impressions.ImpressionsManager;
@@ -16,10 +18,13 @@
1618
import java.util.HashSet;
1719
import java.util.LinkedHashSet;
1820
import java.util.List;
21+
import java.util.Map;
1922
import java.util.Properties;
2023
import java.util.concurrent.ThreadFactory;
2124
import java.io.InputStream;
2225

26+
import static io.split.inputValidation.FallbackTreatmentValidator.isValidByFlagTreatment;
27+
import static io.split.inputValidation.FallbackTreatmentValidator.isValidTreatment;
2328
import static io.split.inputValidation.FlagSetsValidator.cleanup;
2429

2530
/**
@@ -91,6 +96,7 @@ private HttpScheme() {
9196
private final CustomStorageWrapper _customStorageWrapper;
9297
private final StorageMode _storageMode;
9398
private final ThreadFactory _threadFactory;
99+
private final FallbackTreatmentsConfiguration _fallbackTreatments;
94100

95101
// Proxy configs
96102
private final ProxyConfiguration _proxyConfiguration;
@@ -163,7 +169,8 @@ private SplitClientConfig(String endpoint,
163169
HashSet<String> flagSetsFilter,
164170
int invalidSets,
165171
CustomHeaderDecorator customHeaderDecorator,
166-
CustomHttpModule alternativeHTTPModule) {
172+
CustomHttpModule alternativeHTTPModule,
173+
FallbackTreatmentsConfiguration fallbackTreatments) {
167174
_endpoint = endpoint;
168175
_eventsEndpoint = eventsEndpoint;
169176
_featuresRefreshRate = pollForFeatureChangesEveryNSeconds;
@@ -218,6 +225,7 @@ private SplitClientConfig(String endpoint,
218225
_invalidSets = invalidSets;
219226
_customHeaderDecorator = customHeaderDecorator;
220227
_alternativeHTTPModule = alternativeHTTPModule;
228+
_fallbackTreatments = fallbackTreatments;
221229

222230
Properties props = new Properties();
223231
try {
@@ -436,6 +444,8 @@ public boolean isSdkEndpointOverridden() {
436444

437445
public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; }
438446

447+
public FallbackTreatmentsConfiguration fallbackTreatments() { return _fallbackTreatments; }
448+
439449
public static final class Builder {
440450
private String _endpoint = SDK_ENDPOINT;
441451
private boolean _endpointSet = false;
@@ -494,6 +504,7 @@ public static final class Builder {
494504
private int _invalidSetsCount = 0;
495505
private CustomHeaderDecorator _customHeaderDecorator = null;
496506
private CustomHttpModule _alternativeHTTPModule = null;
507+
private FallbackTreatmentsConfiguration _fallbackTreatments;
497508

498509
public Builder() {
499510
}
@@ -1022,6 +1033,17 @@ public Builder alternativeHTTPModule(CustomHttpModule alternativeHTTPModule) {
10221033
return this;
10231034
}
10241035

1036+
/**
1037+
* Fallback Treatments
1038+
*
1039+
* @param fallbackTreatments
1040+
* @return this builder
1041+
*/
1042+
public Builder fallbackTreatments(FallbackTreatmentsConfiguration fallbackTreatments) {
1043+
_fallbackTreatments = fallbackTreatments;
1044+
return this;
1045+
}
1046+
10251047
/**
10261048
* Thread Factory
10271049
*
@@ -1158,6 +1180,25 @@ private void verifyProxy() {
11581180
}
11591181
}
11601182

1183+
private void verifyFallbackTreatments() {
1184+
if (_fallbackTreatments == null)
1185+
return;
1186+
1187+
FallbackTreatment processedGlobalFallbackTreatment = _fallbackTreatments.getGlobalFallbackTreatment();
1188+
Map<String, FallbackTreatment> processedByFlagFallbackTreatment = _fallbackTreatments.getByFlagFallbackTreatment();
1189+
1190+
if (_fallbackTreatments.getGlobalFallbackTreatment() != null) {
1191+
processedGlobalFallbackTreatment = new FallbackTreatment(
1192+
isValidTreatment(_fallbackTreatments.getGlobalFallbackTreatment().getTreatment(), "Fallback treatments"),
1193+
_fallbackTreatments.getGlobalFallbackTreatment().getConfig());
1194+
}
1195+
1196+
if (_fallbackTreatments.getByFlagFallbackTreatment() != null) {
1197+
processedByFlagFallbackTreatment = isValidByFlagTreatment(_fallbackTreatments.getByFlagFallbackTreatment(), "config");
1198+
}
1199+
_fallbackTreatments = new FallbackTreatmentsConfiguration(processedGlobalFallbackTreatment, processedByFlagFallbackTreatment);
1200+
}
1201+
11611202
public SplitClientConfig build() {
11621203

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

11731214
verifyProxy();
11741215

1216+
verifyFallbackTreatments();
1217+
11751218
if (_numThreadsForSegmentFetch <= 0) {
11761219
throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero");
11771220
}
@@ -1230,7 +1273,8 @@ public SplitClientConfig build() {
12301273
_flagSetsFilter,
12311274
_invalidSetsCount,
12321275
_customHeaderDecorator,
1233-
_alternativeHTTPModule);
1276+
_alternativeHTTPModule,
1277+
_fallbackTreatments);
12341278
}
12351279
}
12361280
}

client/src/main/java/io/split/client/SplitClientImpl.java

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import com.google.gson.GsonBuilder;
44
import io.split.client.api.Key;
55
import io.split.client.api.SplitResult;
6-
import io.split.client.dtos.DecoratedImpression;
7-
import io.split.client.dtos.EvaluationOptions;
8-
import io.split.client.dtos.Event;
6+
import io.split.client.dtos.*;
97
import io.split.client.events.EventsStorageProducer;
108
import io.split.client.impressions.Impression;
119
import io.split.client.impressions.ImpressionsManager;
@@ -14,7 +12,6 @@
1412
import io.split.engine.evaluator.Evaluator;
1513
import io.split.engine.evaluator.EvaluatorImp;
1614
import io.split.engine.evaluator.Labels;
17-
import io.split.grammar.Treatments;
1815
import io.split.inputValidation.EventsValidator;
1916
import io.split.inputValidation.KeyValidator;
2017
import io.split.inputValidation.SplitNameValidator;
@@ -49,7 +46,6 @@
4946
* @author adil
5047
*/
5148
public final class SplitClientImpl implements SplitClient {
52-
public static final SplitResult SPLIT_RESULT_CONTROL = new SplitResult(Treatments.CONTROL, null);
5349
private static final String CLIENT_DESTROY = "Client has already been destroyed - no calls possible";
5450
private static final String CATCHALL_EXCEPTION = "CatchAll Exception";
5551
private static final String MATCHING_KEY = "matchingKey";
@@ -66,6 +62,7 @@ public final class SplitClientImpl implements SplitClient {
6662
private final TelemetryEvaluationProducer _telemetryEvaluationProducer;
6763
private final TelemetryConfigProducer _telemetryConfigProducer;
6864
private final FlagSetsFilter _flagSetsFilter;
65+
private final FallbackTreatmentCalculator _fallbackTreatmentCalculator;
6966

7067
public SplitClientImpl(SplitFactory container,
7168
SplitCacheConsumer splitCacheConsumer,
@@ -76,7 +73,8 @@ public SplitClientImpl(SplitFactory container,
7673
Evaluator evaluator,
7774
TelemetryEvaluationProducer telemetryEvaluationProducer,
7875
TelemetryConfigProducer telemetryConfigProducer,
79-
FlagSetsFilter flagSetsFilter) {
76+
FlagSetsFilter flagSetsFilter,
77+
FallbackTreatmentCalculator fallbackTreatmentCalculator) {
8078
_container = container;
8179
_splitCacheConsumer = checkNotNull(splitCacheConsumer);
8280
_impressionManager = checkNotNull(impressionManager);
@@ -87,6 +85,7 @@ public SplitClientImpl(SplitFactory container,
8785
_telemetryEvaluationProducer = checkNotNull(telemetryEvaluationProducer);
8886
_telemetryConfigProducer = checkNotNull(telemetryConfigProducer);
8987
_flagSetsFilter = flagSetsFilter;
88+
_fallbackTreatmentCalculator = fallbackTreatmentCalculator;
9089
}
9190

9291
@Override
@@ -492,31 +491,34 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
492491

493492
if (_container.isDestroyed()) {
494493
_log.error(CLIENT_DESTROY);
495-
return SPLIT_RESULT_CONTROL;
494+
return checkFallbackTreatment(featureFlag);
496495
}
497496

498497
if (!KeyValidator.isValid(matchingKey, MATCHING_KEY, _config.maxStringLength(), methodEnum.getMethod())) {
499-
return SPLIT_RESULT_CONTROL;
498+
return checkFallbackTreatment(featureFlag);
500499
}
501500

502501
if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
503-
return SPLIT_RESULT_CONTROL;
502+
return checkFallbackTreatment(featureFlag);
504503
}
505504

506505
Optional<String> splitNameResult = SplitNameValidator.isValid(featureFlag, methodEnum.getMethod());
507506
if (!splitNameResult.isPresent()) {
508-
return SPLIT_RESULT_CONTROL;
507+
return checkFallbackTreatment(featureFlag);
509508
}
510509
featureFlag = splitNameResult.get();
511510
long start = System.currentTimeMillis();
512511

513512
EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(matchingKey, bucketingKey, featureFlag, attributes);
514-
515-
if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
516-
_log.warn(String.format(
517-
"%s: you passed \"%s\" that does not exist in this environment, " +
518-
"please double check what feature flags exist in the Split user interface.", methodEnum.getMethod(), featureFlag));
519-
return SPLIT_RESULT_CONTROL;
513+
String label = result.label;
514+
if (result.label != null && result.label.contains(Labels.DEFINITION_NOT_FOUND)) {
515+
if (_gates.isSDKReady()) {
516+
_log.warn(String.format(
517+
"%s: you passed \"%s\" that does not exist in this environment, " +
518+
"please double check what feature flags exist in the Split user interface.", methodEnum.getMethod(), featureFlag));
519+
return checkFallbackTreatment(featureFlag);
520+
}
521+
label = result.label.replace(Labels.DEFINITION_NOT_FOUND, Labels.NOT_READY);
520522
}
521523

522524
recordStats(
@@ -526,7 +528,7 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
526528
start,
527529
result.treatment,
528530
String.format("sdk.%s", methodEnum.getMethod()),
529-
_config.labelsEnabled() ? result.label : null,
531+
_config.labelsEnabled() ? label : null,
530532
result.changeNumber,
531533
attributes,
532534
result.track,
@@ -541,8 +543,17 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu
541543
} catch (Exception e1) {
542544
// ignore
543545
}
544-
return SPLIT_RESULT_CONTROL;
546+
return checkFallbackTreatment(featureFlag);
547+
}
548+
}
549+
550+
private SplitResult checkFallbackTreatment(String featureName) {
551+
FallbackTreatment fallbackTreatment = _fallbackTreatmentCalculator.resolve(featureName, "");
552+
String config = null;
553+
if (fallbackTreatment.getConfig() != null) {
554+
config = fallbackTreatment.getConfig();
545555
}
556+
return new SplitResult(fallbackTreatment.getTreatment(), config);
546557
}
547558

548559
private String validateProperties(Map<String, Object> properties) {
@@ -563,6 +574,7 @@ private Map<String, SplitResult> getTreatmentsWithConfigInternal(String matching
563574
_log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
564575
return new HashMap<>();
565576
}
577+
566578
try {
567579
checkSDKReady(methodEnum, featureFlagNames);
568580
Map<String, SplitResult> result = validateBeforeEvaluate(featureFlagNames, matchingKey, methodEnum, bucketingKey);
@@ -601,47 +613,47 @@ private Map<String, SplitResult> getTreatmentsBySetsWithConfigInternal(String ma
601613
if (cleanFlagSets.isEmpty()) {
602614
return new HashMap<>();
603615
}
604-
List<String> featureFlagNames = new ArrayList<>();
605-
try {
606-
checkSDKReady(methodEnum);
607-
Map<String, SplitResult> result = validateBeforeEvaluateByFlagSets(matchingKey, methodEnum,bucketingKey);
608-
if(result != null) {
609-
return result;
610-
}
611-
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey,
612-
bucketingKey, new ArrayList<>(cleanFlagSets), attributes);
616+
checkSDKReady(methodEnum);
617+
Map<String, SplitResult> result = validateBeforeEvaluateByFlagSets(matchingKey, methodEnum,bucketingKey);
618+
if(result != null) {
619+
return result;
620+
}
621+
Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey,
622+
bucketingKey, new ArrayList<>(cleanFlagSets), attributes);
613623

614-
return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime,
615-
validateProperties(evaluationOptions.getProperties()));
616-
} catch (Exception e) {
617-
try {
624+
evaluatorResult.entrySet().forEach(flag -> {
625+
if (flag.getValue().label != null &&
626+
flag.getValue().label.contains(io.split.engine.evaluator.Labels.EXCEPTION)) {
618627
_telemetryEvaluationProducer.recordException(methodEnum);
619-
_log.error(CATCHALL_EXCEPTION, e);
620-
} catch (Exception e1) {
621-
// ignore
622628
}
623-
return createMapControl(featureFlagNames);
624-
}
629+
});
630+
return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime,
631+
validateProperties(evaluationOptions.getProperties()));
625632
}
633+
626634
private Map<String, SplitResult> processEvaluatorResult(Map<String, EvaluatorImp.TreatmentLabelAndChangeNumber> evaluatorResult,
627635
MethodEnum methodEnum, String matchingKey, String bucketingKey, Map<String,
628636
Object> attributes, long initTime, String properties){
629637
List<DecoratedImpression> decoratedImpressions = new ArrayList<>();
630638
Map<String, SplitResult> result = new HashMap<>();
631-
evaluatorResult.keySet().forEach(t -> {
632-
if (evaluatorResult.get(t).treatment.equals(Treatments.CONTROL) && evaluatorResult.get(t).label.
633-
equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
634-
_log.warn(String.format("%s: you passed \"%s\" that does not exist in this environment please double check " +
635-
"what feature flags exist in the Split user interface.", methodEnum.getMethod(), t));
636-
result.put(t, SPLIT_RESULT_CONTROL);
637-
} else {
638-
result.put(t, new SplitResult(evaluatorResult.get(t).treatment, evaluatorResult.get(t).configurations));
639-
decoratedImpressions.add(
640-
new DecoratedImpression(
641-
new Impression(matchingKey, bucketingKey, t, evaluatorResult.get(t).treatment, System.currentTimeMillis(),
642-
evaluatorResult.get(t).label, evaluatorResult.get(t).changeNumber, attributes, properties),
643-
evaluatorResult.get(t).track));
639+
evaluatorResult.keySet().forEach(flag -> {
640+
String label = evaluatorResult.get(flag).label;
641+
if (evaluatorResult.get(flag).label != null &&
642+
evaluatorResult.get(flag).label.contains(Labels.DEFINITION_NOT_FOUND)) {
643+
if (_gates.isSDKReady()) {
644+
_log.warn(String.format("%s: you passed \"%s\" that does not exist in this environment please double check " +
645+
"what feature flags exist in the Split user interface.", methodEnum.getMethod(), flag));
646+
result.put(flag, checkFallbackTreatment(flag));
647+
return;
648+
}
649+
label = evaluatorResult.get(flag).label.replace(Labels.DEFINITION_NOT_FOUND, Labels.NOT_READY);
644650
}
651+
result.put(flag, new SplitResult(evaluatorResult.get(flag).treatment, evaluatorResult.get(flag).configurations));
652+
decoratedImpressions.add(
653+
new DecoratedImpression(
654+
new Impression(matchingKey, bucketingKey, flag, evaluatorResult.get(flag).treatment, System.currentTimeMillis(),
655+
label, evaluatorResult.get(flag).changeNumber, attributes, properties),
656+
evaluatorResult.get(flag).track));
645657
});
646658
_telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis() - initTime);
647659
if (!decoratedImpressions.isEmpty()) {
@@ -735,7 +747,7 @@ private void checkSDKReady(MethodEnum methodEnum) {
735747

736748
private Map<String, SplitResult> createMapControl(List<String> featureFlags) {
737749
Map<String, SplitResult> result = new HashMap<>();
738-
featureFlags.forEach(s -> result.put(s, SPLIT_RESULT_CONTROL));
750+
featureFlags.forEach(s -> result.put(s, checkFallbackTreatment(s)));
739751
return result;
740752
}
741753
}

0 commit comments

Comments
 (0)