diff --git a/lib/splitclient-rb/clients/split_client.rb b/lib/splitclient-rb/clients/split_client.rb index b87c5899..2caccf84 100644 --- a/lib/splitclient-rb/clients/split_client.rb +++ b/lib/splitclient-rb/clients/split_client.rb @@ -51,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) @@ -64,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) @@ -90,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 @@ -471,12 +485,20 @@ def record_exception(method) 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: fallback_treatment.label, + label: (label != '')? fallback_treatment.label : nil, treatment: fallback_treatment.treatment, - config: get_fallback_config(fallback_treatment) + config: get_fallback_config(fallback_treatment), + change_number: nil } end diff --git a/lib/splitclient-rb/split_factory.rb b/lib/splitclient-rb/split_factory.rb index 5f3d0aac..ca8fb1bb 100644 --- a/lib/splitclient-rb/split_factory.rb +++ b/lib/splitclient-rb/split_factory.rb @@ -58,8 +58,7 @@ def initialize(api_key, config_hash = {}) @evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, @rule_based_segment_repository, @config) start! - - fallback_treatment_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + 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 diff --git a/spec/allocations/splitclient-rb/clients/split_client_spec.rb b/spec/allocations/splitclient-rb/clients/split_client_spec.rb index 5d38f377..40ab2543 100644 --- a/spec/allocations/splitclient-rb/clients/split_client_spec.rb +++ b/spec/allocations/splitclient-rb/clients/split_client_spec.rb @@ -21,6 +21,7 @@ let(:telemetry_api) { SplitIoClient::Api::TelemetryApi.new(config, api_key, runtime_producer) } let(:impressions_api) { SplitIoClient::Api::Impressions.new(api_key, config, runtime_producer) } let(:evaluator) { SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rule_based_segments_repository, config) } + let(:fallback_treatment_calculator) { SplitIoClient::Engine::FallbackTreatmentCalculator.new(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) } let(:sender_adapter) do SplitIoClient::Cache::Senders::ImpressionsSenderAdapter.new(config, telemetry_api, @@ -41,7 +42,7 @@ unique_keys_tracker) end let(:client) do - SplitIoClient::SplitClient.new('', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => nil}, nil, config, impressions_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config)) + SplitIoClient::SplitClient.new('', {:splits => splits_repository, :segments => segments_repository, :impressions => impressions_repository, :events => nil}, nil, config, impressions_manager, evaluation_producer, evaluator, SplitIoClient::Validators.new(config), fallback_treatment_calculator) end context 'control' do diff --git a/spec/integrations/in_memory_client_spec.rb b/spec/integrations/in_memory_client_spec.rb index 209bd3ac..6802710d 100644 --- a/spec/integrations/in_memory_client_spec.rb +++ b/spec/integrations/in_memory_client_spec.rb @@ -1443,6 +1443,122 @@ expect(client_old_spec.get_treatment('whitelisted_user', 'whitelist_feature')).to eq('control') end end + + context 'fallback treatments' do + it 'feature not found' do + splits_fallback = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return(status: 200, body: splits_fallback) + factory_fallback = + SplitIoClient::SplitFactory.new('test_api_key', + fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new( + SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-global", '{"prop": "global"}' + ), + {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-local", '{"prop": "local"}' + ) + } + ), + impressions_mode: :optimized, + features_refresh_rate: 9999, + telemetry_refresh_rate: 99999, + impressions_refresh_rate: 99999, + streaming_enabled: false + ) + + client_fallback = factory_fallback.client + client_fallback.block_until_ready + result = client_fallback.get_treatment_with_config('key2', 'feature') + expect(result[:treatment]).to eq('on-local') + expect(result[:config]).to eq('{"prop": "local"}') + + result = client_fallback.get_treatment_with_config('key3', 'feature2') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + + impressions_repository = client_fallback.instance_variable_get(:@impressions_repository) + imps = impressions_repository.batch + expect(imps.length()).to eq(0) + end + + it 'exception' do + splits_fallback = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return(status: 200, body: splits_fallback) + factory_fallback = + SplitIoClient::SplitFactory.new('test_api_key', + fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new( + SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-global", '{"prop": "global"}' + ), + {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-local", '{"prop": "local"}' + ) + } + ), + impressions_mode: :optimized, + features_refresh_rate: 9999, + telemetry_refresh_rate: 99999, + impressions_refresh_rate: 99999, + streaming_enabled: false + ) + + client_fallback = factory_fallback.client + client_fallback.block_until_ready + + splits_repository = client_fallback.instance_variable_get(:@splits_repository) + 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) + + result = client_fallback.get_treatment_with_config('key3', 'with_track_disabled') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + + impressions_repository = client_fallback.instance_variable_get(:@impressions_repository) + 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') + end + + it 'client not ready' do + splits_fallback = File.read(File.join(SplitIoClient.root, 'spec/test_data/splits/imp-toggle.json')) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1') + .to_return(status: 200, body: splits_fallback) + factory_fallback = + SplitIoClient::SplitFactory.new('test_api_key', + fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new( + SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-global", '{"prop": "global"}' + ), + {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-local", '{"prop": "local"}' + ) + } + ), + impressions_mode: :optimized, + features_refresh_rate: 9999, + telemetry_refresh_rate: 99999, + impressions_refresh_rate: 99999, + streaming_enabled: false + ) + + client_fallback = factory_fallback.client + + result = client_fallback.get_treatment_with_config('key3', 'with_track_disabled') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + + impressions_repository = client_fallback.instance_variable_get(:@impressions_repository) + 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 - not ready') + end + end end private diff --git a/spec/integrations/redis_client_spec.rb b/spec/integrations/redis_client_spec.rb index e23c1525..5197fde4 100644 --- a/spec/integrations/redis_client_spec.rb +++ b/spec/integrations/redis_client_spec.rb @@ -992,6 +992,94 @@ expect(events.size).to eq 0 end end + + context 'fallback treatments' do + it 'feature not found' do + factory_fallback = + SplitIoClient::SplitFactory.new('test_api_key', + fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new( + SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-global", '{"prop": "global"}' + ), + {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-local", '{"prop": "local"}' + ) + } + ), + logger: Logger.new(log), + cache_adapter: :redis, + redis_namespace: 'test', + mode: :consumer, + redis_url: 'redis://127.0.0.1:6379/0', + impression_listener: custom_impression_listener + ) + + client_fallback = factory_fallback.client + load_splits_redis(splits, client_fallback) + load_segment_redis(segment1, client_fallback) + load_segment_redis(segment2, client_fallback) + load_segment_redis(segment3, client_fallback) + load_flag_sets_redis(flag_sets, client_fallback) + client_fallback.block_until_ready + + result = client_fallback.get_treatment_with_config('key2', 'feature') + expect(result[:treatment]).to eq('on-local') + expect(result[:config]).to eq('{"prop": "local"}') + + result = client_fallback.get_treatment_with_config('key3', 'feature2') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + + sleep 0.5 + impressions = custom_impression_listener.queue + expect(impressions.size).to eq 0 + end + + it 'exception' do + factory_fallback = + SplitIoClient::SplitFactory.new('test_api_key', + fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new( + SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-global", '{"prop": "global"}' + ), + {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new( + "on-local", '{"prop": "local"}' + ) + } + ), + logger: Logger.new(log), + cache_adapter: :redis, + redis_namespace: 'test', + mode: :consumer, + redis_url: 'redis://127.0.0.1:6379/0', + impression_listener: custom_impression_listener + ) + + client_fallback = factory_fallback.client + load_splits_redis(splits, client_fallback) + load_segment_redis(segment1, client_fallback) + load_segment_redis(segment2, client_fallback) + load_segment_redis(segment3, client_fallback) + load_flag_sets_redis(flag_sets, client_fallback) + client_fallback.block_until_ready + + splits_repository = client_fallback.instance_variable_get(:@splits_repository) + 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) + + result = client_fallback.get_treatment_with_config('key3', 'with_track_disabled') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + + sleep 0.5 + impressions = custom_impression_listener.queue + expect(impressions.size).to eq 1 + expect(impressions[0][:split_name]).to eq('with_track_disabled') + expect(impressions[0][:treatment][:label]).to eq('fallback - exception') + end + end end private diff --git a/spec/splitclient/engine_localhost_spec.rb b/spec/splitclient/engine_localhost_spec.rb index d4a1a74c..16801348 100644 --- a/spec/splitclient/engine_localhost_spec.rb +++ b/spec/splitclient/engine_localhost_spec.rb @@ -160,4 +160,31 @@ end end end + + context 'fallback treatment' do + subject { SplitIoClient::SplitFactoryBuilder.build('localhost', + fallback_treatments: 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_file: split_file).client + } + + let(:split_file) { File.expand_path(File.join(File.dirname(__FILE__), '../test_data/local_treatments/split.yaml')) } + + it 'feature does not exist' do + result = subject.get_treatment_with_config('john_doe', 'feature') + expect(result[:treatment]).to eq('on-local') + expect(result[:config]).to eq('{"prop": "local"}') + + result = subject.get_treatment_with_config('john_doe', 'feature2') + expect(result[:treatment]).to eq('on-global') + expect(result[:config]).to eq('{"prop": "global"}') + end + end end