From 435e595506ee31e0ca8a4ad762423503e72572f4 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 3 Oct 2025 15:52:02 -0700 Subject: [PATCH] Updated client --- lib/splitclient-rb/clients/split_client.rb | 59 ++++-- .../fallback_treatments_configuration.rb | 2 +- lib/splitclient-rb/split_config.rb | 9 +- lib/splitclient-rb/split_factory.rb | 5 +- lib/splitclient-rb/validators.rb | 86 ++++----- spec/splitclient/split_client_spec.rb | 172 +++++++++++++++++- spec/splitclient/split_config_spec.rb | 28 +++ spec/test_data/splits/imp-toggle.json | 3 +- 8 files changed, 291 insertions(+), 73 deletions(-) diff --git a/lib/splitclient-rb/clients/split_client.rb b/lib/splitclient-rb/clients/split_client.rb index 31a40742..b87c5899 100644 --- a/lib/splitclient-rb/clients/split_client.rb +++ b/lib/splitclient-rb/clients/split_client.rb @@ -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] @@ -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( @@ -277,7 +278,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 @@ -286,9 +287,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) @@ -308,7 +311,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) @@ -344,7 +347,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 @@ -373,7 +376,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? @@ -383,7 +386,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 @@ -396,22 +399,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 @@ -472,5 +469,31 @@ def record_exception(method) @telemetry_evaluation_producer.record_exception(Telemetry::Domain::Constants::TRACK) end end + + def check_fallback_treatment(feature_name, label) + fallback_treatment = @fallback_treatment_calculator.resolve(feature_name.to_sym, label) + + { + label: fallback_treatment.label, + treatment: fallback_treatment.treatment, + config: get_fallback_config(fallback_treatment) + } + 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 diff --git a/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb index c4514fca..dbeb68ce 100644 --- a/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb +++ b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb @@ -18,7 +18,7 @@ def build_global_fallback_treatment(global_fallback_treatment) end def build_by_flag_fallback_treatment(by_flag_fallback_treatment) - return nil unless by_flag_fallback_treatment.is_a? Hash + 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| diff --git a/lib/splitclient-rb/split_config.rb b/lib/splitclient-rb/split_config.rb index ec14edd7..fe518bf0 100644 --- a/lib/splitclient-rb/split_config.rb +++ b/lib/splitclient-rb/split_config.rb @@ -712,15 +712,16 @@ def self.sanitize_fallback_config(fallback_config, validator, logger) end sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment - if !fallback_config.global_fallback_treatment.nil? && !validator.validate_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 = None + sanitized_global_fallback_treatment = nil end - sanitized_flag_fallback_treatments = Hash.new + 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(fallback_config.by_flag_fallback_treatment[feature_name]) + 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 diff --git a/lib/splitclient-rb/split_factory.rb b/lib/splitclient-rb/split_factory.rb index 7c8a4306..5f3d0aac 100644 --- a/lib/splitclient-rb/split_factory.rb +++ b/lib/splitclient-rb/split_factory.rb @@ -58,8 +58,9 @@ 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(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + @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 diff --git a/lib/splitclient-rb/validators.rb b/lib/splitclient-rb/validators.rb index 958e2e7c..20554848 100644 --- a/lib/splitclient-rb/validators.rb +++ b/lib/splitclient-rb/validators.rb @@ -70,7 +70,7 @@ def valid_flag_sets(method, flag_sets) log_invalid_flag_set_type(method) elsif flag_set.is_a?(String) && flag_set.empty? log_invalid_flag_set_type(method) - elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, log_invalid_match) + elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, :log_invalid_match) valid_flag_sets.add(flag_set.strip.downcase) else log_invalid_flag_set_type(method) @@ -79,6 +79,46 @@ def valid_flag_sets(method, flag_sets) !valid_flag_sets.empty? ? valid_flag_sets.to_a.sort : [] end + def validate_fallback_treatment(method, fallback_treatment) + if !fallback_treatment.is_a? Engine::Models::FallbackTreatment + @config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded") + return false + end + + if !fallback_treatment.treatment.is_a? String + @config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded") + return false + end + + return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex, :log_invalid_fallback_treatment) + + if fallback_treatment.treatment.size > Fallback_treatment_size + @config.logger.warn("#{method}: Fallback treatment size should not exceed #{Fallback_treatment_size} characters") + return false + end + + true + end + + def valid_split_name?(method, split_name) + if split_name.nil? + log_nil(:split_name, method) + return false + end + + unless string?(split_name) + log_invalid_type(:split_name, method) + return false + end + + if empty_string?(split_name) + log_empty_string(:split_name, method) + return false + end + + true + end + private def string?(value) @@ -95,7 +135,7 @@ def number_or_string?(value) def string_match?(value, method, regex_exp, log_if_invalid) if regex_exp.match(value) == nil - log_if_invalid(value, method) + method(log_if_invalid).call(value, method) false else true @@ -134,25 +174,6 @@ def log_key_too_long(key, method) @config.logger.error("#{method}: #{key} is too long - must be #{@config.max_key_size} characters or less") end - def valid_split_name?(method, split_name) - if split_name.nil? - log_nil(:split_name, method) - return false - end - - unless string?(split_name) - log_invalid_type(:split_name, method) - return false - end - - if empty_string?(split_name) - log_empty_string(:split_name, method) - return false - end - - true - end - def valid_key?(method, key) if key.nil? log_nil(:key, method) @@ -329,29 +350,8 @@ def valid_properties?(properties) true end - def validate_fallback_treatment(method, fallback_treatment) - if !fallback_treatment.is_a? Engine::Models::FallbackTreatment - @config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded") - return false - end - - if !fallback_treatment.treatment.is_a? String - @config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded") - return false - end - - return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex) - - if fallback_treatment.treatment.size > Fallback_treatment_size - @config.logger.warn("#{method}: Fallback treatment size should not exceed %s characters", Fallback_treatment_size) - return false - end - - true - end - def log_invalid_fallback_treatment(key, method) - @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex %s", Fallback_treatment_regex) + @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex #{Fallback_treatment_regex}") end end end diff --git a/spec/splitclient/split_client_spec.rb b/spec/splitclient/split_client_spec.rb index 6c49fffd..c0596f23 100644 --- a/spec/splitclient/split_client_spec.rb +++ b/spec/splitclient/split_client_spec.rb @@ -16,7 +16,8 @@ let(:impression_manager) { SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, SplitIoClient::Engine::Common::NoopImpressionCounter.new, runtime_producer, SplitIoClient::Observers::NoopImpressionObserver.new, SplitIoClient::Engine::Impressions::NoopUniqueKeysTracker.new) } let(:evaluation_producer) { SplitIoClient::Telemetry::EvaluationProducer.new(config) } let(:evaluator) { SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) } - let(:split_client) { SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository, :rule_based_segments => rule_based_segments_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config)) } + let(:fallback_treatment_calculator) { SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) } + let(:split_client) { SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository, :rule_based_segments => rule_based_segments_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) } let(:splits) do File.read(File.join(SplitIoClient.root, 'spec/test_data/integrations/splits.json')) @@ -128,7 +129,8 @@ impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) - split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config)) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) splits = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) splits_repository.update([JSON.parse(splits,:symbolize_names => true)[:ff][:d][0]], [], -1) @@ -168,7 +170,8 @@ impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) - split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config)) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) splits = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) splits_repository.update([JSON.parse(splits,:symbolize_names => true)[:ff][:d][0]], [], -1) @@ -208,7 +211,8 @@ impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) - split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config)) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) splits = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) splits_repository.update([JSON.parse(splits,:symbolize_names => true)[:ff][:d][0]], [], -1) @@ -235,6 +239,166 @@ end end +context 'fallback treatments' do + it 'feature not found ' do + config = SplitIoClient::SplitConfig.new(cache_adapter: :memory, impressions_mode: :debug) + segments_repository = SplitIoClient::Cache::Repositories::SegmentsRepository.new(config) + flag_sets_repository = SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) + flag_set_filter = SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) + splits_repository = SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) + impressions_repository = SplitIoClient::Cache::Repositories::ImpressionsRepository.new(config) + rule_based_segments_repository = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config) + runtime_producer = SplitIoClient::Telemetry::RuntimeProducer.new(config) + events_repository = SplitIoClient::Cache::Repositories::EventsRepository.new(config, 'sdk_key', runtime_producer) + impressions_counter = SplitIoClient::Engine::Common::ImpressionCounter.new + filter_adapter = SplitIoClient::Cache::Filter::FilterAdapter.new(config, SplitIoClient::Cache::Filter::BloomFilter.new(1_000)) + unique_keys_tracker = SplitIoClient::Engine::Impressions::UniqueKeysTracker.new(config, filter_adapter, nil, Concurrent::Hash.new) + impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) + evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) + evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on-global", '{"prop": "global"}'), {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new("on-local", '{"prop": "local"}')})) + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) + + treatment = split_client.get_treatment_with_config('key2', 'feature') + expect(treatment[:treatment]).to eq('on-local') + expect(treatment[:config]).to eq('{"prop": "local"}') + treatment = split_client.get_treatment_with_config('key3', 'feature2') + expect(treatment[:treatment]).to eq('on-global') + expect(treatment[:config]).to eq('{"prop": "global"}') + imps = impressions_repository.batch + expect(imps.length()).to eq(0) + + treatment = split_client.get_treatment('key2', 'feature') + expect(treatment).to eq('on-local') + treatment = split_client.get_treatment('key3', 'feature2') + expect(treatment).to eq('on-global') + + treatment = split_client.get_treatments('key2', ['feature', 'feature2']) + expect(treatment[:feature]).to eq('on-local') + expect(treatment[:feature2]).to eq('on-global') + + treatment = split_client.get_treatments_with_config('key2', ['feature', 'feature2']) + expect(treatment[:feature][:treatment]).to eq('on-local') + expect(treatment[:feature][:config]).to eq('{"prop": "local"}') + expect(treatment[:feature2][:treatment]).to eq('on-global') + expect(treatment[:feature2][:config]).to eq('{"prop": "global"}') + end + + it 'exception' do + config = SplitIoClient::SplitConfig.new(cache_adapter: :memory, impressions_mode: :debug) + segments_repository = SplitIoClient::Cache::Repositories::SegmentsRepository.new(config) + flag_sets_repository = SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) + flag_set_filter = SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) + splits_repository = SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) + impressions_repository = SplitIoClient::Cache::Repositories::ImpressionsRepository.new(config) + rule_based_segments_repository = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config) + runtime_producer = SplitIoClient::Telemetry::RuntimeProducer.new(config) + events_repository = SplitIoClient::Cache::Repositories::EventsRepository.new(config, 'sdk_key', runtime_producer) + impressions_counter = SplitIoClient::Engine::Common::ImpressionCounter.new + filter_adapter = SplitIoClient::Cache::Filter::FilterAdapter.new(config, SplitIoClient::Cache::Filter::BloomFilter.new(1_000)) + unique_keys_tracker = SplitIoClient::Engine::Impressions::UniqueKeysTracker.new(config, filter_adapter, nil, Concurrent::Hash.new) + impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) + evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) + evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on-global", '{"prop": "global"}'), {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new("on-local", '{"prop": "local"}')})) + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, nil, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) + + splits = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) + split = JSON.parse(splits,:symbolize_names => true)[:ff][:d][0] + split[:trafficAllocation] = nil + splits_repository.update([split], [], -1) + + treatment = split_client.get_treatment_with_config('key2', 'with_track_disabled') + expect(treatment[:treatment]).to eq('on-global') + expect(treatment[:config]).to eq('{"prop": "global"}') + imps = impressions_repository.batch + expect(imps.length()).to eq(1) + expect(imps[0][:i][:f]).to eq('with_track_disabled') + expect(imps[0][:i][:r]).to eq('fallback - exception') + + treatment = split_client.get_treatment('key3', 'with_track_disabled') + expect(treatment).to eq('on-global') + + treatment = split_client.get_treatments('key2', ['with_track_disabled']) + expect(treatment[:with_track_disabled]).to eq('on-global') + + treatment = split_client.get_treatments_with_config('key2', ['with_track_disabled']) + expect(treatment[:with_track_disabled][:treatment]).to eq('on-global') + expect(treatment[:with_track_disabled][:config]).to eq('{"prop": "global"}') + + treatment = split_client.get_treatments_by_flag_set('key2', 'set1') + expect(treatment[:with_track_disabled]).to eq('on-global') + + treatment = split_client.get_treatments_by_flag_sets('key2', ['set1']) + expect(treatment[:with_track_disabled]).to eq('on-global') + + treatment = split_client.get_treatments_with_config_by_flag_set('key2', 'set1') + expect(treatment[:with_track_disabled][:treatment]).to eq('on-global') + expect(treatment[:with_track_disabled][:config]).to eq('{"prop": "global"}') + + treatment = split_client.get_treatments_with_config_by_flag_sets('key2', ['set1']) + expect(treatment[:with_track_disabled][:treatment]).to eq('on-global') + expect(treatment[:with_track_disabled][:config]).to eq('{"prop": "global"}') + end + + it 'client not ready' do + config = SplitIoClient::SplitConfig.new(cache_adapter: :memory, impressions_mode: :debug) + segments_repository = SplitIoClient::Cache::Repositories::SegmentsRepository.new(config) + flag_sets_repository = SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) + flag_set_filter = SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) + splits_repository = SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) + impressions_repository = SplitIoClient::Cache::Repositories::ImpressionsRepository.new(config) + rule_based_segments_repository = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config) + runtime_producer = SplitIoClient::Telemetry::RuntimeProducer.new(config) + events_repository = SplitIoClient::Cache::Repositories::EventsRepository.new(config, 'sdk_key', runtime_producer) + impressions_counter = SplitIoClient::Engine::Common::ImpressionCounter.new + filter_adapter = SplitIoClient::Cache::Filter::FilterAdapter.new(config, SplitIoClient::Cache::Filter::BloomFilter.new(1_000)) + unique_keys_tracker = SplitIoClient::Engine::Impressions::UniqueKeysTracker.new(config, filter_adapter, nil, Concurrent::Hash.new) + impression_manager = SplitIoClient::Engine::Common::ImpressionManager.new(config, impressions_repository, impressions_counter, runtime_producer, SplitIoClient::Observers::ImpressionObserver.new, unique_keys_tracker) + evaluation_producer = SplitIoClient::Telemetry::EvaluationProducer.new(config) + evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) + fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on-global", '{"prop": "global"}'), {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new("on-local", '{"prop": "local"}')})) + + class MyStatusManager + def ready? + false + end + def wait_until_ready(time) + true + end + end + split_client = SplitIoClient::SplitClient.new('sdk_key', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => events_repository}, MyStatusManager.new, config, impression_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) + + treatment = split_client.get_treatment_with_config('key2', 'feature') + expect(treatment[:treatment]).to eq('on-local') + expect(treatment[:config]).to eq('{"prop": "local"}') + treatment = split_client.get_treatment_with_config('key3', 'feature2') + expect(treatment[:treatment]).to eq('on-global') + expect(treatment[:config]).to eq('{"prop": "global"}') + imps = impressions_repository.batch + expect(imps.length()).to eq(2) + expect(imps[0][:i][:f]).to eq('feature') + expect(imps[0][:i][:r]).to eq('fallback - not ready') + expect(imps[1][:i][:f]).to eq('feature2') + expect(imps[1][:i][:r]).to eq('fallback - not ready') + + treatment = split_client.get_treatment('key2', 'feature') + expect(treatment).to eq('on-local') + treatment = split_client.get_treatment('key3', 'feature2') + expect(treatment).to eq('on-global') + + treatment = split_client.get_treatments('key2', ['feature', 'feature2']) + expect(treatment[:feature]).to eq('on-local') + expect(treatment[:feature2]).to eq('on-global') + + treatment = split_client.get_treatments_with_config('key2', ['feature', 'feature2']) + expect(treatment[:feature][:treatment]).to eq('on-local') + expect(treatment[:feature][:config]).to eq('{"prop": "local"}') + expect(treatment[:feature2][:treatment]).to eq('on-global') + expect(treatment[:feature2][:config]).to eq('{"prop": "global"}') + end +end + def mock_segment_changes(segment_name, segment_json, since) stub_request(:get, "https://sdk.split.io/api/segmentChanges/#{segment_name}?since=#{since}") .to_return(status: 200, body: segment_json) diff --git a/spec/splitclient/split_config_spec.rb b/spec/splitclient/split_config_spec.rb index 1c867aa4..9dcf14bf 100644 --- a/spec/splitclient/split_config_spec.rb +++ b/spec/splitclient/split_config_spec.rb @@ -180,5 +180,33 @@ configs = SplitIoClient::SplitConfig.new(flag_sets_filter: ['1set', 12]) expect(configs.flag_sets_filter).to eq ['1set'] end + + it 'test fallback treatment validations' do + configs = SplitIoClient::SplitConfig.new(fallback_treatments: 0) + expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-global', {:feature => 'on_45-c'})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment.treatment).to eq 'on-global' + expect(configs.fallback_treatments_configuration.global_fallback_treatment.config).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment[:feature].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment[:feature].treatment).to eq 'on_45-c' + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment[:feature].config).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-gl/obal', {:feature => "0" * 300})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new('on-gl$#obal'), {"" => "treat"})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new + end end end diff --git a/spec/test_data/splits/imp-toggle.json b/spec/test_data/splits/imp-toggle.json index b70d6684..9deba4ae 100644 --- a/spec/test_data/splits/imp-toggle.json +++ b/spec/test_data/splits/imp-toggle.json @@ -48,7 +48,8 @@ ], "label": "default rule" } - ] + ], + "sets": ["set1"] }, { "trafficTypeName": "user",