From 5ef3c762625307c91643ff086227f576262ae11a Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 3 Jun 2025 14:59:59 -0500 Subject: [PATCH 1/9] [FSSDK-11140] Ruby: Update project config to track CMAB properties --- .../config/datafile_project_config.rb | 44 ++++++++++++++++++- lib/optimizely/helpers/constants.rb | 17 +++++++ lib/optimizely/project_config.rb | 4 ++ spec/config/datafile_project_config_spec.rb | 17 +++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 25357133..5f417628 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -27,7 +27,7 @@ class DatafileProjectConfig < ProjectConfig attr_reader :datafile, :account_id, :attributes, :audiences, :typed_audiences, :events, :experiments, :feature_flags, :groups, :project_id, :bot_filtering, :revision, :sdk_key, :environment_key, :rollouts, :version, :send_flag_decisions, - :attribute_key_map, :audience_id_map, :event_key_map, :experiment_feature_map, + :attribute_key_map, :attribute_id_to_key_map, :audience_id_map, :event_key_map, :experiment_feature_map, :experiment_id_map, :experiment_key_map, :feature_flag_key_map, :feature_variable_key_map, :group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map, :variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id, @@ -82,6 +82,10 @@ def initialize(datafile, logger, error_handler) # Utility maps for quick lookup @attribute_key_map = generate_key_map(@attributes, 'key') + @attribute_id_to_key_map = {} + for attribute in @attributes + @attribute_id_to_key_map[attribute['id']] = attribute['key'] + end @event_key_map = generate_key_map(@events, 'key') @group_id_map = generate_key_map(@groups, 'id') @group_id_map.each do |key, group| @@ -440,6 +444,44 @@ def get_attribute_id(attribute_key) nil end + def get_attribute_by_key(attribute_key) + # Get attribute for the provided attribute key. + # + # Args: + # Attribute key for which attribute is to be fetched. + # + # Returns: + # Attribute corresponding to the provided attribute key. + attribute = @attribute_key_map[attribute_key] + if attribute_key in @attribute_key_map + return attribute + end + + invalid_attribute_error = InvalidAttributeError.new(attribute_key) + @logger.log Logger::ERROR, invalid_attribute_error.message + @error_handler.handle_error invalid_attribute_error + nil + end + + def get_attribute_key_by_id(attribute_id) + # Get attribute key for the provided attribute ID. + # + # Args: + # Attribute ID for which attribute is to be fetched. + # + # Returns: + # Attribute key corresponding to the provided attribute ID. + attribute = @attribute_id_to_key_map[attribute_id] + if attribute_id in @attribute_id_to_key_map + return attribute + end + + invalid_attribute_error = InvalidAttributeError.new(attribute_id) + @logger.log Logger::ERROR, invalid_attribute_error.message + @error_handler.handle_error invalid_attribute_error + nil + end + def variation_id_exists?(experiment_id, variation_id) # Determines if a given experiment ID / variation ID pair exists in the datafile # diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index 02b815ae..bb6e1386 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -201,6 +201,9 @@ module Constants }, 'forcedVariations' => { 'type' => 'object' + }, + 'cmab' => { + 'type' => 'object', } }, 'required' => %w[ @@ -303,6 +306,20 @@ module Constants }, 'required' => %w[key] } + }, + 'cmab' => { + 'type' => 'object', + 'items' => { + 'type' => 'object', + 'properties' => { + 'attributeIds' => { + 'type' => 'array', + }, + 'trafficAllocation' => { + 'type' => 'integer', + } + } + } } }, 'required' => %w[ diff --git a/lib/optimizely/project_config.rb b/lib/optimizely/project_config.rb index b0d43aa3..43e86441 100644 --- a/lib/optimizely/project_config.rb +++ b/lib/optimizely/project_config.rb @@ -86,6 +86,10 @@ def get_whitelisted_variations(experiment_id); end def get_attribute_id(attribute_key); end + def get_attribute_by_key(attribute_key); end + + def get_attribute_key_by_id(attribute_id); end + def variation_id_exists?(experiment_id, variation_id); end def get_feature_flag_from_key(feature_flag_key); end diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index e30d07e1..079258d9 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1078,6 +1078,23 @@ end end + describe 'test_cmab_field_population' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = { 'attributeIds' => ['808797688', '808797689'], 'trafficAllocation' => 4000 } + config_dict['experiments'][0]['trafficAllocation'] = [] + + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + + it 'Should return CMAB details' do + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'attributeIds' => ['808797688', '808797689'], 'trafficAllocation' => 4000}) + + experiment2 = project_config.get_experiment_from_key('test_experiment_2') + expect(experiment2['cmab']).to eq(nil) + end + end + describe '#feature_experiment' do let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler) } From 42a5e506093a7f48ab224209062a82ca65c27ec2 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 3 Jun 2025 15:16:44 -0500 Subject: [PATCH 2/9] Fix errors --- lib/optimizely/config/datafile_project_config.rb | 4 ++-- lib/optimizely/helpers/constants.rb | 6 +++--- spec/config/datafile_project_config_spec.rb | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 5f417628..54d74802 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -453,7 +453,7 @@ def get_attribute_by_key(attribute_key) # Returns: # Attribute corresponding to the provided attribute key. attribute = @attribute_key_map[attribute_key] - if attribute_key in @attribute_key_map + if @attribute_key_map.key?(attribute_key) return attribute end @@ -472,7 +472,7 @@ def get_attribute_key_by_id(attribute_id) # Returns: # Attribute key corresponding to the provided attribute ID. attribute = @attribute_id_to_key_map[attribute_id] - if attribute_id in @attribute_id_to_key_map + if @attribute_id_to_key_map.key?(attribute_id) return attribute end diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index bb6e1386..eba265b3 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -203,7 +203,7 @@ module Constants 'type' => 'object' }, 'cmab' => { - 'type' => 'object', + 'type' => 'object' } }, 'required' => %w[ @@ -313,10 +313,10 @@ module Constants 'type' => 'object', 'properties' => { 'attributeIds' => { - 'type' => 'array', + 'type' => 'array' }, 'trafficAllocation' => { - 'type' => 'integer', + 'type' => 'integer' } } } diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index 079258d9..e2637224 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1080,7 +1080,7 @@ describe 'test_cmab_field_population' do config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) - config_dict['experiments'][0]['cmab'] = { 'attributeIds' => ['808797688', '808797689'], 'trafficAllocation' => 4000 } + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} config_dict['experiments'][0]['trafficAllocation'] = [] config_json = JSON.dump(config_dict) @@ -1088,7 +1088,7 @@ it 'Should return CMAB details' do experiment = project_config.get_experiment_from_key('test_experiment') - expect(experiment['cmab']).to eq({'attributeIds' => ['808797688', '808797689'], 'trafficAllocation' => 4000}) + expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000}) experiment2 = project_config.get_experiment_from_key('test_experiment_2') expect(experiment2['cmab']).to eq(nil) From 42bb090a6a2a9566a395814baf0f037fd9cd0473 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 3 Jun 2025 16:09:07 -0500 Subject: [PATCH 3/9] Fix errors --- lib/optimizely/config/datafile_project_config.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 54d74802..9be9ba5d 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -83,9 +83,9 @@ def initialize(datafile, logger, error_handler) # Utility maps for quick lookup @attribute_key_map = generate_key_map(@attributes, 'key') @attribute_id_to_key_map = {} - for attribute in @attributes + @attributes.each do |attribute| @attribute_id_to_key_map[attribute['id']] = attribute['key'] - end + end @event_key_map = generate_key_map(@events, 'key') @group_id_map = generate_key_map(@groups, 'id') @group_id_map.each do |key, group| @@ -453,9 +453,7 @@ def get_attribute_by_key(attribute_key) # Returns: # Attribute corresponding to the provided attribute key. attribute = @attribute_key_map[attribute_key] - if @attribute_key_map.key?(attribute_key) - return attribute - end + return attribute if @attribute_key_map.key?(attribute_key) invalid_attribute_error = InvalidAttributeError.new(attribute_key) @logger.log Logger::ERROR, invalid_attribute_error.message @@ -472,9 +470,7 @@ def get_attribute_key_by_id(attribute_id) # Returns: # Attribute key corresponding to the provided attribute ID. attribute = @attribute_id_to_key_map[attribute_id] - if @attribute_id_to_key_map.key?(attribute_id) - return attribute - end + return attribute if @attribute_id_to_key_map.key?(attribute_id) invalid_attribute_error = InvalidAttributeError.new(attribute_id) @logger.log Logger::ERROR, invalid_attribute_error.message From a2281051663741f46844e8b07682b328f3c68290 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 3 Jun 2025 16:53:04 -0500 Subject: [PATCH 4/9] Fix test --- spec/config/datafile_project_config_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index e2637224..06d290ee 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1078,19 +1078,19 @@ end end - describe 'test_cmab_field_population' do - config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) - config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} - config_dict['experiments'][0]['trafficAllocation'] = [] + describe '#test_cmab_field_population' do + it 'Should return CMAB details' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} + config_dict['experiments'][0]['trafficAllocation'] = [] - config_json = JSON.dump(config_dict) - project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) - it 'Should return CMAB details' do - experiment = project_config.get_experiment_from_key('test_experiment') + experiment = project_config.get_experiment_from_key('test_experiment_with_audience') expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000}) - experiment2 = project_config.get_experiment_from_key('test_experiment_2') + experiment2 = project_config.get_experiment_from_key('test_experiment') expect(experiment2['cmab']).to eq(nil) end end From 6cb34a49d931c7f2023b96927193872d00441468 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 3 Jun 2025 16:58:41 -0500 Subject: [PATCH 5/9] Correct the experiment --- spec/config/datafile_project_config_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index 06d290ee..b740afc2 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1087,10 +1087,10 @@ config_json = JSON.dump(config_dict) project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) - experiment = project_config.get_experiment_from_key('test_experiment_with_audience') + experiment = project_config.get_experiment_from_key('test_experiment') expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000}) - experiment2 = project_config.get_experiment_from_key('test_experiment') + experiment2 = project_config.get_experiment_from_key('test_experiment_with_audience') expect(experiment2['cmab']).to eq(nil) end end From 400584fa1255c9cef0a9604be4c02b2ebf4ee450 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 9 Jun 2025 10:13:23 -0500 Subject: [PATCH 6/9] Add new test cases related CMAB --- spec/config/datafile_project_config_spec.rb | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index b740afc2..362141d6 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1093,6 +1093,50 @@ experiment2 = project_config.get_experiment_from_key('test_experiment_with_audience') expect(experiment2['cmab']).to eq(nil) end + it 'should return nil if cmab field is missing' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0].delete('cmab') + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq(nil) + end + + it 'should handle empty cmab object' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({}) + end + + it 'should handle cmab with only attributeIds' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688]} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688]}) + end + + it 'should handle cmab with only trafficAllocation' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'trafficAllocation' => 1234} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'trafficAllocation' => 1234}) + end + + it 'should not affect other experiments when cmab is set' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment2 = project_config.get_experiment_from_key('test_experiment_with_audience') + expect(experiment2['cmab']).to eq(nil) + end end describe '#feature_experiment' do From b9317bb737b2afad3e78b8e41cac7ada7405340a Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 13 Jun 2025 11:21:04 -0500 Subject: [PATCH 7/9] Implement comments --- lib/optimizely/config/datafile_project_config.rb | 4 ++-- lib/optimizely/helpers/constants.rb | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 9be9ba5d..1f03171d 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -453,7 +453,7 @@ def get_attribute_by_key(attribute_key) # Returns: # Attribute corresponding to the provided attribute key. attribute = @attribute_key_map[attribute_key] - return attribute if @attribute_key_map.key?(attribute_key) + return attribute if attribute invalid_attribute_error = InvalidAttributeError.new(attribute_key) @logger.log Logger::ERROR, invalid_attribute_error.message @@ -470,7 +470,7 @@ def get_attribute_key_by_id(attribute_id) # Returns: # Attribute key corresponding to the provided attribute ID. attribute = @attribute_id_to_key_map[attribute_id] - return attribute if @attribute_id_to_key_map.key?(attribute_id) + return attribute if attribute invalid_attribute_error = InvalidAttributeError.new(attribute_id) @logger.log Logger::ERROR, invalid_attribute_error.message diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index eba265b3..55be9057 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -309,15 +309,12 @@ module Constants }, 'cmab' => { 'type' => 'object', - 'items' => { - 'type' => 'object', - 'properties' => { - 'attributeIds' => { - 'type' => 'array' - }, - 'trafficAllocation' => { - 'type' => 'integer' - } + 'properties' => { + 'attributeIds' => { + 'type' => 'array' + }, + 'trafficAllocation' => { + 'type' => 'integer' } } } From bf7718fb15b33e185d29e5cb0c9958dd3305b2a4 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 13 Jun 2025 11:22:10 -0500 Subject: [PATCH 8/9] Correct the type --- lib/optimizely/helpers/constants.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index 55be9057..35e9ead2 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -311,7 +311,8 @@ module Constants 'type' => 'object', 'properties' => { 'attributeIds' => { - 'type' => 'array' + 'type' => 'array', + 'items' => { 'type' => 'string' } }, 'trafficAllocation' => { 'type' => 'integer' From 8cd522a3dbafba90b4effa0e5deca12e83c71e6a Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 13 Jun 2025 11:23:16 -0500 Subject: [PATCH 9/9] Fix lint --- lib/optimizely/helpers/constants.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index 35e9ead2..7b57a268 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -312,7 +312,7 @@ module Constants 'properties' => { 'attributeIds' => { 'type' => 'array', - 'items' => { 'type' => 'string' } + 'items' => {'type' => 'string'} }, 'trafficAllocation' => { 'type' => 'integer'