diff --git a/CHANGES.txt b/CHANGES.txt index 2546ab36..f899ea72 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index 30e0a2b3..5f4ebcc9 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -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' diff --git a/lib/splitclient-rb/clients/split_client.rb b/lib/splitclient-rb/clients/split_client.rb index 31a40742..2caccf84 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( @@ -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) @@ -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) @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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? @@ -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 @@ -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 @@ -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 diff --git a/lib/splitclient-rb/engine/fallback_treatment_calculator.rb b/lib/splitclient-rb/engine/fallback_treatment_calculator.rb new file mode 100644 index 00000000..d736c84c --- /dev/null +++ b/lib/splitclient-rb/engine/fallback_treatment_calculator.rb @@ -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 diff --git a/lib/splitclient-rb/engine/models/fallback_treatment.rb b/lib/splitclient-rb/engine/models/fallback_treatment.rb new file mode 100644 index 00000000..c89f439d --- /dev/null +++ b/lib/splitclient-rb/engine/models/fallback_treatment.rb @@ -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 diff --git a/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb new file mode 100644 index 00000000..dbeb68ce --- /dev/null +++ b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb @@ -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 diff --git a/lib/splitclient-rb/split_config.rb b/lib/splitclient-rb/split_config.rb index f1967c93..fe518bf0 100644 --- a/lib/splitclient-rb/split_config.rb +++ b/lib/splitclient-rb/split_config.rb @@ -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 @@ -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. @@ -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 diff --git a/lib/splitclient-rb/split_factory.rb b/lib/splitclient-rb/split_factory.rb index 7c8a4306..ca8fb1bb 100644 --- a/lib/splitclient-rb/split_factory.rb +++ b/lib/splitclient-rb/split_factory.rb @@ -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 diff --git a/lib/splitclient-rb/validators.rb b/lib/splitclient-rb/validators.rb index 0061115b..a95a8627 100644 --- a/lib/splitclient-rb/validators.rb +++ b/lib/splitclient-rb/validators.rb @@ -4,6 +4,8 @@ module SplitIoClient class Validators Flagset_regex = /^[a-z0-9][_a-z0-9]{0,49}$/ + Fallback_treatment_regex = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/ + Fallback_treatment_size = 100 def initialize(config) @config = config @@ -55,8 +57,12 @@ def valid_flag_sets(method, flag_sets) end without_nil = Array.new flag_sets.each { |flag_set| - without_nil.push(flag_set) if !flag_set.nil? - log_nil("flag set", method) if flag_set.nil? + if !flag_set.nil? + without_nil.push(flag_set) + next + end + + log_nil("flag set", method) } if without_nil.length() == 0 log_invalid_flag_set_type(method) @@ -68,7 +74,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) + 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) @@ -77,6 +83,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) @@ -91,9 +137,9 @@ def number_or_string?(value) (value.is_a?(Numeric) && !value.to_f.nan?) || string?(value) end - def string_match?(value, method) - if Flagset_regex.match(value) == nil - log_invalid_match(value, method) + def string_match?(value, method, regex_exp, log_if_invalid) + if regex_exp.match(value) == nil + method(log_if_invalid).call(value, method) false else true @@ -132,25 +178,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) @@ -326,5 +353,9 @@ def valid_properties?(properties) true end + + def log_invalid_fallback_treatment(key, method) + @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex #{Fallback_treatment_regex}") + end end end diff --git a/lib/splitclient-rb/version.rb b/lib/splitclient-rb/version.rb index 1cab2036..1afbdd0a 100644 --- a/lib/splitclient-rb/version.rb +++ b/lib/splitclient-rb/version.rb @@ -1,3 +1,3 @@ module SplitIoClient - VERSION = '8.8.0' + VERSION = '8.9.0' 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/engine/fallback_treatment_calculator_spec.rb b/spec/engine/fallback_treatment_calculator_spec.rb new file mode 100644 index 00000000..4c02056a --- /dev/null +++ b/spec/engine/fallback_treatment_calculator_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::FallbackTreatmentCalculator do + context 'works' do + it 'process fallback treatments' do + fallback_config = SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on" ,"{}")) + fallback_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(fallback_config) + expect(fallback_calculator.fallback_treatments_configuration).to be fallback_config + expect(fallback_calculator.label_prefix).to eq("fallback - ") + + fallback_treatment = fallback_calculator.resolve("feature", "not ready") + expect(fallback_treatment.treatment).to eq("on") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq("{}") + + fallback_calculator.fallback_treatments_configuration = SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on" ,"{}"), {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new("off" , '{"prop": "val"}')}) + fallback_treatment = fallback_calculator.resolve(:feature, "not ready") + expect(fallback_treatment.treatment).to eq("off") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq('{"prop": "val"}') + + fallback_treatment = fallback_calculator.resolve(:feature2, "not ready") + expect(fallback_treatment.treatment).to eq("on") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq("{}") + end + end +end diff --git a/spec/engine/fallback_treatments_configuration_spec.rb b/spec/engine/fallback_treatments_configuration_spec.rb new file mode 100644 index 00000000..cd732f63 --- /dev/null +++ b/spec/engine/fallback_treatments_configuration_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration do + context 'works' do + it 'it converts string to fallback treatment' do + fb_config = described_class.new("global", {:feature => "local"}) + expect(fb_config.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true + expect(fb_config.global_fallback_treatment.treatment).to be "global" + + expect(fb_config.by_flag_fallback_treatment[:feature].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true + expect(fb_config.by_flag_fallback_treatment[:feature].treatment).to be "local" + end + end +end 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/push_client_spec.rb b/spec/integrations/push_client_spec.rb index ca206024..689793a5 100644 --- a/spec/integrations/push_client_spec.rb +++ b/spec/integrations/push_client_spec.rb @@ -140,6 +140,7 @@ expect(a_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1')).to have_been_made.times(1) expect(a_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=1585948850109&rbSince=-1')).to have_been_made.times(1) expect(a_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=1585948850110&rbSince=-1')).to have_been_made.times(0) + client.destroy end end @@ -177,6 +178,7 @@ end expect(treatment).to eq('off') + client.destroy end end @@ -206,6 +208,7 @@ sleep(2) expect(client.get_treatment('admin', 'bilal_split')).to eq('control') expect(client.get_treatment('admin', 'push_test')).to eq('after_fetch') + client.destroy end end @@ -235,6 +238,7 @@ sleep(2) expect(client.get_treatment('admin', 'bilal_split')).to eq('control') expect(client.get_treatment('admin', 'push_test')).to eq('after_fetch') + client.destroy end end @@ -264,6 +268,7 @@ sleep(2) expect(client.get_treatment('admin', 'bilal_split')).to eq('control') expect(client.get_treatment('admin', 'push_test')).to eq('after_fetch') + client.destroy end end end 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 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",