Skip to content
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 !@config.split_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