Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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,5 +1,8 @@
CHANGES

8.9.0 (Oct 8, 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.

8.8.0 (Sep 26, 2025)
- Added a maximum size payload when posting unique keys telemetry in batches

Expand Down
3 changes: 3 additions & 0 deletions lib/splitclient-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@
require 'splitclient-rb/engine/models/treatment'
require 'splitclient-rb/engine/models/split_http_response'
require 'splitclient-rb/engine/models/evaluation_options'
require 'splitclient-rb/engine/models/fallback_treatment.rb'
require 'splitclient-rb/engine/models/fallback_treatments_configuration.rb'
require 'splitclient-rb/engine/auth_api_client'
require 'splitclient-rb/engine/back_off'
require 'splitclient-rb/engine/fallback_treatment_calculator.rb'
require 'splitclient-rb/engine/push_manager'
require 'splitclient-rb/engine/status_manager'
require 'splitclient-rb/engine/sync_manager'
Expand Down
89 changes: 67 additions & 22 deletions lib/splitclient-rb/clients/split_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class SplitClient
# @param sdk_key [String] the SDK key for your split account
#
# @return [SplitIoClient] split.io client instance
def initialize(sdk_key, repositories, status_manager, config, impressions_manager, telemetry_evaluation_producer, evaluator, split_validator)
def initialize(sdk_key, repositories, status_manager, config, impressions_manager, telemetry_evaluation_producer, evaluator, split_validator, fallback_treatment_calculator)
@api_key = sdk_key
@splits_repository = repositories[:splits]
@segments_repository = repositories[:segments]
Expand All @@ -32,6 +32,7 @@ def initialize(sdk_key, repositories, status_manager, config, impressions_manage
@telemetry_evaluation_producer = telemetry_evaluation_producer
@split_validator = split_validator
@evaluator = evaluator
@fallback_treatment_calculator = fallback_treatment_calculator
end

def get_treatment(
Expand All @@ -50,7 +51,9 @@ def get_treatment_with_config(
multiple = false, evaluator = nil
)
log_deprecated_warning(GET_TREATMENT, evaluator, 'evaluator')
treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple, evaluation_options)
result = treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple, evaluation_options)

{ :config => result[:config], :treatment => result[:treatment] }
end

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

def get_treatments_with_config(key, split_names, attributes = {}, evaluation_options = nil)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG)
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG)

results.map{|key, value|
[key, { treatment: value[:treatment], config: value[:config] }]
}.to_h
end

def get_treatments_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
Expand All @@ -89,13 +96,21 @@ def get_treatments_by_flag_sets(key, flag_sets, attributes = {}, evaluation_opti
def get_treatments_with_config_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, [flag_set])
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)

results.map{|key, value|
[key, { treatment: value[:treatment], config: value[:config] }]
}.to_h
end

def get_treatments_with_config_by_flag_sets(key, flag_sets, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, flag_sets)
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)
results = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)

results.map{|key, value|
[key, { treatment: value[:treatment], config: value[:config] }]
}.to_h
end

def destroy
Expand Down Expand Up @@ -277,7 +292,7 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
if [email protected]_validator.valid_get_treatments_parameters(calling_method, key, sanitized_feature_flag_names, matching_key, bucketing_key, attributes)
to_return = Hash.new
sanitized_feature_flag_names.each {|name|
to_return[name.to_sym] = control_treatment_with_config
to_return[name.to_sym] = check_fallback_treatment(name, '')
}
return to_return
end
Expand All @@ -286,9 +301,11 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
impressions = []
to_return = Hash.new
sanitized_feature_flag_names.each {|name|
to_return[name.to_sym] = control_treatment_with_config
treatment_data = check_fallback_treatment(name, Engine::Models::Label::NOT_READY)
to_return[name.to_sym] = treatment_data

impressions << { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, name.to_sym,
control_treatment_with_config.merge({ :label => Engine::Models::Label::NOT_READY }), false, { attributes: attributes, time: nil },
get_treatment_without_config(treatment_data), false, { attributes: attributes, time: nil },
evaluation_options), :disabled => false }
}
@impressions_manager.track(impressions)
Expand All @@ -308,7 +325,7 @@ def treatments(key, feature_flag_names, attributes = {}, evaluation_options = ni
if feature_flag.nil?
@config.logger.warn("#{calling_method}: you passed #{key} that " \
'does not exist in this environment, please double check what feature flags exist in the Split user interface')
invalid_treatments[key] = control_treatment_with_config
invalid_treatments[key] = check_fallback_treatment(key, Engine::Models::Label::NOT_FOUND)
next
end
treatments_labels_change_numbers, impressions = evaluate_treatment(feature_flag, key, bucketing_key, matching_key, attributes, calling_method, false, evaluation_options)
Expand Down Expand Up @@ -344,7 +361,7 @@ def treatment(key, feature_flag_name, attributes = {}, split_data = nil, store_i

attributes = parsed_attributes(attributes)

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)
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)

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

if !feature_flag.nil? && ready?
Expand All @@ -383,7 +400,7 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
impressions_disabled = feature_flag[:impressionsDisabled]
else
@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.")
treatment_data = control_treatment.merge({ :label => Engine::Models::Label::NOT_READY })
treatment_data = check_fallback_treatment(feature_flag_name, Engine::Models::Label::NOT_READY)
impressions_disabled = false
end

Expand All @@ -396,22 +413,16 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
rescue StandardError => e
@config.log_found_exception(__method__.to_s, e)
record_exception(calling_method)
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 }
treatment_data = check_fallback_treatment(feature_flag_name, Engine::Models::Label::EXCEPTION)
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 }

impressions_decorator << impression_decorator unless impression_decorator.nil?

return parsed_treatment(control_treatment.merge({ :label => Engine::Models::Label::EXCEPTION }), multiple), impressions_decorator
return parsed_treatment(treatment_data, multiple), impressions_decorator
end
return parsed_treatment(treatment_data, multiple), impressions_decorator
end

def control_treatment
{ :treatment => Engine::Models::Treatment::CONTROL }
end

def control_treatment_with_config
{:treatment => Engine::Models::Treatment::CONTROL, :config => nil}
end

def variable_size(value)
value.is_a?(String) ? value.length : 0
end
Expand Down Expand Up @@ -472,5 +483,39 @@ def record_exception(method)
@telemetry_evaluation_producer.record_exception(Telemetry::Domain::Constants::TRACK)
end
end

def check_fallback_treatment(feature_name, label)
return {
label: (label != '')? label : nil,
treatment: Engine::Models::Treatment::CONTROL,
config: nil,
change_number: nil
} unless feature_name.is_a?(Symbol) || feature_name.is_a?(String)

fallback_treatment = @fallback_treatment_calculator.resolve(feature_name.to_sym, label)

{
label: (label != '')? fallback_treatment.label : nil,
treatment: fallback_treatment.treatment,
config: get_fallback_config(fallback_treatment),
change_number: nil
}
end

def get_treatment_without_config(treatment)
{
label: treatment[:label],
treatment: treatment[:treatment],
}
end

def get_fallback_config(fallback_treatment)
if fallback_treatment.config != nil
return fallback_treatment.config
end

return nil
end

end
end
48 changes: 48 additions & 0 deletions lib/splitclient-rb/engine/fallback_treatment_calculator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module SplitIoClient
module Engine
class FallbackTreatmentCalculator
attr_accessor :fallback_treatments_configuration, :label_prefix

def initialize(fallback_treatment_configuration)
@label_prefix = 'fallback - '
@fallback_treatments_configuration = fallback_treatment_configuration
end

def resolve(flag_name, label)
default_fallback_treatment = Engine::Models::FallbackTreatment.new(
Engine::Models::Treatment::CONTROL,
nil,
label
)
return default_fallback_treatment if @fallback_treatments_configuration.nil?

if !@fallback_treatments_configuration.by_flag_fallback_treatment.nil? \
&& !@fallback_treatments_configuration.by_flag_fallback_treatment.fetch(flag_name, nil).nil?
return copy_with_label(
@fallback_treatments_configuration.by_flag_fallback_treatment[flag_name],
resolve_label(label)
)
end

return copy_with_label(@fallback_treatments_configuration.global_fallback_treatment, resolve_label(label)) \
unless @fallback_treatments_configuration.global_fallback_treatment.nil?

default_fallback_treatment
end

private

def resolve_label(label)
return nil if label.nil?

@label_prefix + label
end

def copy_with_label(fallback_treatment, label)
Engine::Models::FallbackTreatment.new(fallback_treatment.treatment, fallback_treatment.config, label)
end
end
end
end
11 changes: 11 additions & 0 deletions lib/splitclient-rb/engine/models/fallback_treatment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module SplitIoClient::Engine::Models
class FallbackTreatment
attr_accessor :treatment, :config, :label

def initialize(treatment, config=nil, label=nil)
@treatment = treatment
@config = config
@label = label
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module SplitIoClient::Engine::Models
class FallbackTreatmentsConfiguration
attr_accessor :global_fallback_treatment, :by_flag_fallback_treatment

def initialize(global_fallback_treatment=nil, by_flag_fallback_treatment=nil)
@global_fallback_treatment = build_global_fallback_treatment(global_fallback_treatment)
@by_flag_fallback_treatment = build_by_flag_fallback_treatment(by_flag_fallback_treatment)
end

private

def build_global_fallback_treatment(global_fallback_treatment)
if global_fallback_treatment.is_a? String
return FallbackTreatment.new(global_fallback_treatment)
end

global_fallback_treatment
end

def build_by_flag_fallback_treatment(by_flag_fallback_treatment)
return nil unless by_flag_fallback_treatment.is_a?(Hash)
processed_by_flag_fallback_treatment = Hash.new

by_flag_fallback_treatment.each do |key, value|
if value.is_a? String
processed_by_flag_fallback_treatment[key] = FallbackTreatment.new(value)
next
end

processed_by_flag_fallback_treatment[key] = value
end

processed_by_flag_fallback_treatment
end
end
end
36 changes: 36 additions & 0 deletions lib/splitclient-rb/split_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def initialize(opts = {})
@on_demand_fetch_max_retries = SplitConfig.default_on_demand_fetch_max_retries

@flag_sets_filter = SplitConfig.sanitize_flag_set_filter(opts[:flag_sets_filter], @split_validator, opts[:cache_adapter], @logger)

@fallback_treatments_configuration = SplitConfig.sanitize_fallback_config(opts[:fallback_treatments], @split_validator, @logger)
startup_log
end

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

attr_accessor :fallback_treatments_configuration

def self.default_counter_refresh_rate(adapter)
return 300 if adapter == :redis # Send bulk impressions count - Refresh rate: 5 min.

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

return ''.freeze
end

def self.sanitize_fallback_config(fallback_config, validator, logger)
return fallback_config if fallback_config.nil?

processed = Engine::Models::FallbackTreatmentsConfiguration.new
if !fallback_config.is_a?(Engine::Models::FallbackTreatmentsConfiguration)
logger.warn('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.')
return processed
end

sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment
if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment('Config', fallback_config.global_fallback_treatment)
logger.warn('Config: global fallbacktreatment parameter is discarded.')
sanitized_global_fallback_treatment = nil
end

sanitized_flag_fallback_treatments = nil
if !fallback_config.by_flag_fallback_treatment.nil? && fallback_config.by_flag_fallback_treatment.is_a?(Hash)
sanitized_flag_fallback_treatments = Hash.new
for feature_name in fallback_config.by_flag_fallback_treatment.keys()
if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment('Config', fallback_config.by_flag_fallback_treatment[feature_name])
logger.warn("Config: fallback treatment parameter for feature flag #{feature_name} is discarded.")
next
end

sanitized_flag_fallback_treatments[feature_name] = fallback_config.by_flag_fallback_treatment[feature_name]
end
end
processed = Engine::Models::FallbackTreatmentsConfiguration.new(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments)

processed
end
end
end
4 changes: 2 additions & 2 deletions lib/splitclient-rb/split_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def initialize(api_key, config_hash = {})
@evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, @rule_based_segment_repository, @config)

start!

@client = SplitClient.new(@api_key, repositories, @status_manager, @config, @impressions_manager, @evaluation_producer, @evaluator, @split_validator)
fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(@config.fallback_treatments_configuration)
@client = SplitClient.new(@api_key, repositories, @status_manager, @config, @impressions_manager, @evaluation_producer, @evaluator, @split_validator, fallback_treatment_calculator)
@manager = SplitManager.new(@splits_repository, @status_manager, @config)
end

Expand Down
Loading
Loading