Skip to content

Commit a48b6c4

Browse files
authored
Merge pull request #587 from splitio/feature/fallback-treatments
Pepare for release 8.9.0
2 parents 5d8f3cc + 5652487 commit a48b6c4

19 files changed

+740
-56
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
CHANGES
22

3+
8.9.0 (Oct 8, 2025)
4+
- 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.
5+
36
8.8.0 (Sep 26, 2025)
47
- Added a maximum size payload when posting unique keys telemetry in batches
58

lib/splitclient-rb.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,11 @@
110110
require 'splitclient-rb/engine/models/treatment'
111111
require 'splitclient-rb/engine/models/split_http_response'
112112
require 'splitclient-rb/engine/models/evaluation_options'
113+
require 'splitclient-rb/engine/models/fallback_treatment.rb'
114+
require 'splitclient-rb/engine/models/fallback_treatments_configuration.rb'
113115
require 'splitclient-rb/engine/auth_api_client'
114116
require 'splitclient-rb/engine/back_off'
117+
require 'splitclient-rb/engine/fallback_treatment_calculator.rb'
115118
require 'splitclient-rb/engine/push_manager'
116119
require 'splitclient-rb/engine/status_manager'
117120
require 'splitclient-rb/engine/sync_manager'

lib/splitclient-rb/clients/split_client.rb

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class SplitClient
1818
# @param sdk_key [String] the SDK key for your split account
1919
#
2020
# @return [SplitIoClient] split.io client instance
21-
def initialize(sdk_key, repositories, status_manager, config, impressions_manager, telemetry_evaluation_producer, evaluator, split_validator)
21+
def initialize(sdk_key, repositories, status_manager, config, impressions_manager, telemetry_evaluation_producer, evaluator, split_validator, fallback_treatment_calculator)
2222
@api_key = sdk_key
2323
@splits_repository = repositories[:splits]
2424
@segments_repository = repositories[:segments]
@@ -32,6 +32,7 @@ def initialize(sdk_key, repositories, status_manager, config, impressions_manage
3232
@telemetry_evaluation_producer = telemetry_evaluation_producer
3333
@split_validator = split_validator
3434
@evaluator = evaluator
35+
@fallback_treatment_calculator = fallback_treatment_calculator
3536
end
3637

3738
def get_treatment(
@@ -50,7 +51,9 @@ def get_treatment_with_config(
5051
multiple = false, evaluator = nil
5152
)
5253
log_deprecated_warning(GET_TREATMENT, evaluator, 'evaluator')
53-
treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple, evaluation_options)
54+
result = treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple, evaluation_options)
55+
56+
{ :config => result[:config], :treatment => result[:treatment] }
5457
end
5558

5659
def get_treatments(key, split_names, attributes = {}, evaluation_options = nil)
@@ -63,7 +66,11 @@ def get_treatments(key, split_names, attributes = {}, evaluation_options = nil)
6366
end
6467

6568
def get_treatments_with_config(key, split_names, attributes = {}, evaluation_options = nil)
66-
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG)
69+
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG)
70+
71+
results.map{|key, value|
72+
[key, { treatment: value[:treatment], config: value[:config] }]
73+
}.to_h
6774
end
6875

6976
def get_treatments_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
@@ -89,13 +96,21 @@ def get_treatments_by_flag_sets(key, flag_sets, attributes = {}, evaluation_opti
8996
def get_treatments_with_config_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
9097
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, [flag_set])
9198
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
92-
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)
99+
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)
100+
101+
results.map{|key, value|
102+
[key, { treatment: value[:treatment], config: value[:config] }]
103+
}.to_h
93104
end
94105

95106
def get_treatments_with_config_by_flag_sets(key, flag_sets, attributes = {}, evaluation_options = nil)
96107
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, flag_sets)
97108
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
98-
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)
109+
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)
110+
111+
results.map{|key, value|
112+
[key, { treatment: value[:treatment], config: value[:config] }]
113+
}.to_h
99114
end
100115

101116
def destroy
@@ -277,7 +292,7 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
277292
if !@config.split_validator.valid_get_treatments_parameters(calling_method, key, sanitized_feature_flag_names, matching_key, bucketing_key, attributes)
278293
to_return = Hash.new
279294
sanitized_feature_flag_names.each {|name|
280-
to_return[name.to_sym] = control_treatment_with_config
295+
to_return[name.to_sym] = check_fallback_treatment(name, '')
281296
}
282297
return to_return
283298
end
@@ -286,9 +301,11 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
286301
impressions = []
287302
to_return = Hash.new
288303
sanitized_feature_flag_names.each {|name|
289-
to_return[name.to_sym] = control_treatment_with_config
304+
treatment_data = check_fallback_treatment(name, Engine::Models::Label::NOT_READY)
305+
to_return[name.to_sym] = treatment_data
306+
290307
impressions << { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, name.to_sym,
291-
control_treatment_with_config.merge({ :label => Engine::Models::Label::NOT_READY }), false, { attributes: attributes, time: nil },
308+
get_treatment_without_config(treatment_data), false, { attributes: attributes, time: nil },
292309
evaluation_options), :disabled => false }
293310
}
294311
@impressions_manager.track(impressions)
@@ -308,7 +325,7 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
308325
if feature_flag.nil?
309326
@config.logger.warn("#{calling_method}: you passed #{key} that " \
310327
'does not exist in this environment, please double check what feature flags exist in the Split user interface')
311-
invalid_treatments[key] = control_treatment_with_config
328+
invalid_treatments[key] = check_fallback_treatment(key, Engine::Models::Label::NOT_FOUND)
312329
next
313330
end
314331
treatments_labels_change_numbers, impressions = evaluate_treatment(feature_flag, key, bucketing_key, matching_key, attributes, calling_method, false, evaluation_options)
@@ -344,7 +361,7 @@ def treatment(key, feature_flag_name, attributes = {}, split_data = nil, store_i
344361

345362
attributes = parsed_attributes(attributes)
346363

347-
return parsed_treatment(control_treatment, multiple) unless valid_client && @config.split_validator.valid_get_treatment_parameters(calling_method, key, feature_flag_name, matching_key, bucketing_key, attributes)
364+
return parsed_treatment(check_fallback_treatment(feature_flag_name, ""), multiple) unless valid_client && @config.split_validator.valid_get_treatment_parameters(calling_method, key, feature_flag_name, matching_key, bucketing_key, attributes)
348365

349366
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
350367
matching_key = matching_key.to_s
@@ -373,7 +390,7 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
373390
if feature_flag.nil? && ready?
374391
@config.logger.warn("#{calling_method}: you passed #{feature_flag_name} that " \
375392
'does not exist in this environment, please double check what feature flags exist in the Split user interface')
376-
return parsed_treatment(control_treatment.merge({ :label => Engine::Models::Label::NOT_FOUND }), multiple), nil
393+
return check_fallback_treatment(feature_flag_name, Engine::Models::Label::NOT_FOUND), nil
377394
end
378395

379396
if !feature_flag.nil? && ready?
@@ -383,7 +400,7 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
383400
impressions_disabled = feature_flag[:impressionsDisabled]
384401
else
385402
@config.logger.error("#{calling_method}: the SDK is not ready, results may be incorrect for feature flag #{feature_flag_name}. Make sure to wait for SDK readiness before using this method.")
386-
treatment_data = control_treatment.merge({ :label => Engine::Models::Label::NOT_READY })
403+
treatment_data = check_fallback_treatment(feature_flag_name, Engine::Models::Label::NOT_READY)
387404
impressions_disabled = false
388405
end
389406

@@ -396,22 +413,16 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
396413
rescue StandardError => e
397414
@config.log_found_exception(__method__.to_s, e)
398415
record_exception(calling_method)
399-
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, control_treatment, false, { attributes: attributes, time: nil }, evaluation_options), :disabled => false }
416+
treatment_data = check_fallback_treatment(feature_flag_name, Engine::Models::Label::EXCEPTION)
417+
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, get_treatment_without_config(treatment_data), false, { attributes: attributes, time: nil }, evaluation_options), :disabled => false }
418+
400419
impressions_decorator << impression_decorator unless impression_decorator.nil?
401420

402-
return parsed_treatment(control_treatment.merge({ :label => Engine::Models::Label::EXCEPTION }), multiple), impressions_decorator
421+
return parsed_treatment(treatment_data, multiple), impressions_decorator
403422
end
404423
return parsed_treatment(treatment_data, multiple), impressions_decorator
405424
end
406425

407-
def control_treatment
408-
{ :treatment => Engine::Models::Treatment::CONTROL }
409-
end
410-
411-
def control_treatment_with_config
412-
{:treatment => Engine::Models::Treatment::CONTROL, :config => nil}
413-
end
414-
415426
def variable_size(value)
416427
value.is_a?(String) ? value.length : 0
417428
end
@@ -472,5 +483,39 @@ def record_exception(method)
472483
@telemetry_evaluation_producer.record_exception(Telemetry::Domain::Constants::TRACK)
473484
end
474485
end
486+
487+
def check_fallback_treatment(feature_name, label)
488+
return {
489+
label: (label != '')? label : nil,
490+
treatment: Engine::Models::Treatment::CONTROL,
491+
config: nil,
492+
change_number: nil
493+
} unless feature_name.is_a?(Symbol) || feature_name.is_a?(String)
494+
495+
fallback_treatment = @fallback_treatment_calculator.resolve(feature_name.to_sym, label)
496+
497+
{
498+
label: (label != '')? fallback_treatment.label : nil,
499+
treatment: fallback_treatment.treatment,
500+
config: get_fallback_config(fallback_treatment),
501+
change_number: nil
502+
}
503+
end
504+
505+
def get_treatment_without_config(treatment)
506+
{
507+
label: treatment[:label],
508+
treatment: treatment[:treatment],
509+
}
510+
end
511+
512+
def get_fallback_config(fallback_treatment)
513+
if fallback_treatment.config != nil
514+
return fallback_treatment.config
515+
end
516+
517+
return nil
518+
end
519+
475520
end
476521
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
module SplitIoClient
4+
module Engine
5+
class FallbackTreatmentCalculator
6+
attr_accessor :fallback_treatments_configuration, :label_prefix
7+
8+
def initialize(fallback_treatment_configuration)
9+
@label_prefix = 'fallback - '
10+
@fallback_treatments_configuration = fallback_treatment_configuration
11+
end
12+
13+
def resolve(flag_name, label)
14+
default_fallback_treatment = Engine::Models::FallbackTreatment.new(
15+
Engine::Models::Treatment::CONTROL,
16+
nil,
17+
label
18+
)
19+
return default_fallback_treatment if @fallback_treatments_configuration.nil?
20+
21+
if !@fallback_treatments_configuration.by_flag_fallback_treatment.nil? \
22+
&& !@fallback_treatments_configuration.by_flag_fallback_treatment.fetch(flag_name, nil).nil?
23+
return copy_with_label(
24+
@fallback_treatments_configuration.by_flag_fallback_treatment[flag_name],
25+
resolve_label(label)
26+
)
27+
end
28+
29+
return copy_with_label(@fallback_treatments_configuration.global_fallback_treatment, resolve_label(label)) \
30+
unless @fallback_treatments_configuration.global_fallback_treatment.nil?
31+
32+
default_fallback_treatment
33+
end
34+
35+
private
36+
37+
def resolve_label(label)
38+
return nil if label.nil?
39+
40+
@label_prefix + label
41+
end
42+
43+
def copy_with_label(fallback_treatment, label)
44+
Engine::Models::FallbackTreatment.new(fallback_treatment.treatment, fallback_treatment.config, label)
45+
end
46+
end
47+
end
48+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module SplitIoClient::Engine::Models
2+
class FallbackTreatment
3+
attr_accessor :treatment, :config, :label
4+
5+
def initialize(treatment, config=nil, label=nil)
6+
@treatment = treatment
7+
@config = config
8+
@label = label
9+
end
10+
end
11+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module SplitIoClient::Engine::Models
2+
class FallbackTreatmentsConfiguration
3+
attr_accessor :global_fallback_treatment, :by_flag_fallback_treatment
4+
5+
def initialize(global_fallback_treatment=nil, by_flag_fallback_treatment=nil)
6+
@global_fallback_treatment = build_global_fallback_treatment(global_fallback_treatment)
7+
@by_flag_fallback_treatment = build_by_flag_fallback_treatment(by_flag_fallback_treatment)
8+
end
9+
10+
private
11+
12+
def build_global_fallback_treatment(global_fallback_treatment)
13+
if global_fallback_treatment.is_a? String
14+
return FallbackTreatment.new(global_fallback_treatment)
15+
end
16+
17+
global_fallback_treatment
18+
end
19+
20+
def build_by_flag_fallback_treatment(by_flag_fallback_treatment)
21+
return nil unless by_flag_fallback_treatment.is_a?(Hash)
22+
processed_by_flag_fallback_treatment = Hash.new
23+
24+
by_flag_fallback_treatment.each do |key, value|
25+
if value.is_a? String
26+
processed_by_flag_fallback_treatment[key] = FallbackTreatment.new(value)
27+
next
28+
end
29+
30+
processed_by_flag_fallback_treatment[key] = value
31+
end
32+
33+
processed_by_flag_fallback_treatment
34+
end
35+
end
36+
end

lib/splitclient-rb/split_config.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def initialize(opts = {})
123123
@on_demand_fetch_max_retries = SplitConfig.default_on_demand_fetch_max_retries
124124

125125
@flag_sets_filter = SplitConfig.sanitize_flag_set_filter(opts[:flag_sets_filter], @split_validator, opts[:cache_adapter], @logger)
126+
127+
@fallback_treatments_configuration = SplitConfig.sanitize_fallback_config(opts[:fallback_treatments], @split_validator, @logger)
126128
startup_log
127129
end
128130

@@ -303,6 +305,8 @@ def initialize(opts = {})
303305
# @return [Array]
304306
attr_accessor :flag_sets_filter
305307

308+
attr_accessor :fallback_treatments_configuration
309+
306310
def self.default_counter_refresh_rate(adapter)
307311
return 300 if adapter == :redis # Send bulk impressions count - Refresh rate: 5 min.
308312

@@ -697,5 +701,37 @@ def self.machine_ip(ip_addresses_enabled, ip, adapter)
697701

698702
return ''.freeze
699703
end
704+
705+
def self.sanitize_fallback_config(fallback_config, validator, logger)
706+
return fallback_config if fallback_config.nil?
707+
708+
processed = Engine::Models::FallbackTreatmentsConfiguration.new
709+
if !fallback_config.is_a?(Engine::Models::FallbackTreatmentsConfiguration)
710+
logger.warn('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.')
711+
return processed
712+
end
713+
714+
sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment
715+
if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment('Config', fallback_config.global_fallback_treatment)
716+
logger.warn('Config: global fallbacktreatment parameter is discarded.')
717+
sanitized_global_fallback_treatment = nil
718+
end
719+
720+
sanitized_flag_fallback_treatments = nil
721+
if !fallback_config.by_flag_fallback_treatment.nil? && fallback_config.by_flag_fallback_treatment.is_a?(Hash)
722+
sanitized_flag_fallback_treatments = Hash.new
723+
for feature_name in fallback_config.by_flag_fallback_treatment.keys()
724+
if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment('Config', fallback_config.by_flag_fallback_treatment[feature_name])
725+
logger.warn("Config: fallback treatment parameter for feature flag #{feature_name} is discarded.")
726+
next
727+
end
728+
729+
sanitized_flag_fallback_treatments[feature_name] = fallback_config.by_flag_fallback_treatment[feature_name]
730+
end
731+
end
732+
processed = Engine::Models::FallbackTreatmentsConfiguration.new(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments)
733+
734+
processed
735+
end
700736
end
701737
end

lib/splitclient-rb/split_factory.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ def initialize(api_key, config_hash = {})
5858
@evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, @rule_based_segment_repository, @config)
5959

6060
start!
61-
62-
@client = SplitClient.new(@api_key, repositories, @status_manager, @config, @impressions_manager, @evaluation_producer, @evaluator, @split_validator)
61+
fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(@config.fallback_treatments_configuration)
62+
@client = SplitClient.new(@api_key, repositories, @status_manager, @config, @impressions_manager, @evaluation_producer, @evaluator, @split_validator, fallback_treatment_calculator)
6363
@manager = SplitManager.new(@splits_repository, @status_manager, @config)
6464
end
6565

0 commit comments

Comments
 (0)