diff --git a/lib/optimizely/condition_tree_evaluator.rb b/lib/optimizely/condition_tree_evaluator.rb index cc63122c..724bc2d5 100644 --- a/lib/optimizely/condition_tree_evaluator.rb +++ b/lib/optimizely/condition_tree_evaluator.rb @@ -28,6 +28,8 @@ module ConditionTreeEvaluator NOT_CONDITION => :not_evaluator }.freeze + OPERATORS = [AND_CONDITION, OR_CONDITION, NOT_CONDITION].freeze + module_function def evaluate(conditions, leaf_evaluator) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index af2789d9..0e5163a6 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -87,8 +87,8 @@ def initialize(datafile, logger, error_handler) @anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false @bot_filtering = config['botFiltering'] @revision = config['revision'] - @sdk_key = config.fetch('sdkKey', nil) - @environment_key = config.fetch('environmentKey', nil) + @sdk_key = config.fetch('sdkKey', '') + @environment_key = config.fetch('environmentKey', '') @rollouts = config.fetch('rollouts', []) @send_flag_decisions = config.fetch('sendFlagDecisions', false) @@ -482,6 +482,16 @@ def feature_experiment?(experiment_id) @experiment_feature_map.key?(experiment_id) end + def rollout_experiment?(experiment_id) + # Determines if given experiment is a rollout test. + # + # experiment_id - String experiment ID + # + # Returns true if experiment belongs to any rollout, + # false otherwise. + @rollout_experiment_id_map.key?(experiment_id) + end + private def generate_key_map(array, key) diff --git a/lib/optimizely/optimizely_config.rb b/lib/optimizely/optimizely_config.rb index 8362656e..f3337752 100644 --- a/lib/optimizely/optimizely_config.rb +++ b/lib/optimizely/optimizely_config.rb @@ -16,56 +16,109 @@ # module Optimizely + require 'json' class OptimizelyConfig + include Optimizely::ConditionTreeEvaluator def initialize(project_config) @project_config = project_config + @rollouts = @project_config.rollouts + @audiences = [] + audience_id_lookup_dict = {} + + @project_config.typed_audiences.each do |typed_audience| + @audiences.push( + 'id' => typed_audience['id'], + 'name' => typed_audience['name'], + 'conditions' => typed_audience['conditions'].to_json + ) + audience_id_lookup_dict[typed_audience['id']] = typed_audience['id'] + end + + @project_config.audiences.each do |audience| + next unless !audience_id_lookup_dict.key?(audience['id']) && (audience['id'] != '$opt_dummy_audience') + + @audiences.push( + 'id' => audience['id'], + 'name' => audience['name'], + 'conditions' => audience['conditions'] + ) + end end def config experiments_map_object = experiments_map - features_map = get_features_map(experiments_map_object) + features_map = get_features_map(experiments_id_map) config = { + 'sdkKey' => @project_config.sdk_key, 'datafile' => @project_config.datafile, + # This experimentsMap is for experiments of legacy projects only. + # For flag projects, experiment keys are not guaranteed to be unique + # across multiple flags, so this map may not include all experiments + # when keys conflict. Use experimentRules and deliveryRules instead. 'experimentsMap' => experiments_map_object, 'featuresMap' => features_map, - 'revision' => @project_config.revision + 'revision' => @project_config.revision, + 'attributes' => get_attributes_list(@project_config.attributes), + 'audiences' => @audiences, + 'events' => get_events_list(@project_config.events), + 'environmentKey' => @project_config.environment_key } - config['sdkKey'] = @project_config.sdk_key if @project_config.sdk_key - config['environmentKey'] = @project_config.environment_key if @project_config.environment_key config end private - def experiments_map - feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature| - result_map.update(feature['id'] => feature['variables']) - end + def experiments_id_map + feature_variables_map = feature_variable_map + audiences_id_map = audiences_map @project_config.experiments.reduce({}) do |experiments_map, experiment| + feature_id = @project_config.experiment_feature_map.fetch(experiment['id'], []).first experiments_map.update( - experiment['key'] => { + experiment['id'] => { 'id' => experiment['id'], 'key' => experiment['key'], - 'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation| - variation_object = { - 'id' => variation['id'], - 'key' => variation['key'], - 'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map) - } - variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id']) - variations_map.update(variation['key'] => variation_object) - end + 'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map), + 'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || '' } ) end end + def audiences_map + @audiences.reduce({}) do |audiences_map, optly_audience| + audiences_map.update(optly_audience['id'] => optly_audience['name']) + end + end + + def experiments_map + experiments_id_map.values.reduce({}) do |experiments_key_map, experiment| + experiments_key_map.update(experiment['key'] => experiment) + end + end + + def feature_variable_map + @project_config.feature_flags.reduce({}) do |result_map, feature| + result_map.update(feature['id'] => feature['variables']) + end + end + + def get_variation_map(feature_id, experiment, feature_variables_map) + experiment['variations'].reduce({}) do |variations_map, variation| + variation_object = { + 'id' => variation['id'], + 'key' => variation['key'], + 'featureEnabled' => variation['featureEnabled'], + 'variablesMap' => get_merged_variables_map(variation, feature_id, feature_variables_map) + } + variations_map.update(variation['key'] => variation_object) + end + end + # Merges feature key and type from feature variables to variation variables. - def get_merged_variables_map(variation, experiment_id, feature_variables_map) - feature_ids = @project_config.experiment_feature_map[experiment_id] - return {} unless feature_ids + def get_merged_variables_map(variation, feature_id, feature_variables_map) + return {} unless feature_id - experiment_feature_variables = feature_variables_map[feature_ids[0]] + feature_variables = feature_variables_map[feature_id] # temporary variation variables map to get values to merge. temp_variables_id_map = {} if variation['variables'] @@ -78,7 +131,7 @@ def get_merged_variables_map(variation, experiment_id, feature_variables_map) ) end end - experiment_feature_variables.reduce({}) do |variables_map, feature_variable| + feature_variables.reduce({}) do |variables_map, feature_variable| variation_variable = temp_variables_id_map[feature_variable['id']] variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue'] variables_map.update( @@ -94,13 +147,15 @@ def get_merged_variables_map(variation, experiment_id, feature_variables_map) def get_features_map(all_experiments_map) @project_config.feature_flags.reduce({}) do |features_map, feature| + delivery_rules = get_delivery_rules(@rollouts, feature['rolloutId'], feature['id']) features_map.update( feature['key'] => { 'id' => feature['id'], 'key' => feature['key'], + # This experimentsMap is deprecated. Use experimentRules and deliveryRules instead. 'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id| experiment_key = @project_config.experiment_id_map[experiment_id]['key'] - experiments_map.update(experiment_key => all_experiments_map[experiment_key]) + experiments_map.update(experiment_key => experiments_id_map[experiment_id]) end, 'variablesMap' => feature['variables'].reduce({}) do |variables, variable| variables.update( @@ -111,10 +166,107 @@ def get_features_map(all_experiments_map) 'value' => variable['defaultValue'] } ) - end + end, + 'experimentRules' => feature['experimentIds'].reduce([]) do |experiments_map, experiment_id| + experiments_map.push(all_experiments_map[experiment_id]) + end, + 'deliveryRules' => delivery_rules } ) end end + + def get_attributes_list(attributes) + attributes.map do |attribute| + { + 'id' => attribute['id'], + 'key' => attribute['key'] + } + end + end + + def get_events_list(events) + events.map do |event| + { + 'id' => event['id'], + 'key' => event['key'], + 'experimentIds' => event['experimentIds'] + } + end + end + + def lookup_name_from_id(audience_id, audiences_map) + audiences_map[audience_id] || audience_id + end + + def stringify_conditions(conditions, audiences_map) + operand = 'OR' + conditions_str = '' + length = conditions.length() + return '' if length.zero? + return '"' + lookup_name_from_id(conditions[0], audiences_map) + '"' if length == 1 && !OPERATORS.include?(conditions[0]) + + # Edge cases for lengths 0, 1 or 2 + if length == 2 && OPERATORS.include?(conditions[0]) && !conditions[1].is_a?(Array) && !OPERATORS.include?(conditions[1]) + return '"' + lookup_name_from_id(conditions[1], audiences_map) + '"' if conditions[0] != 'not' + + return conditions[0].upcase + ' "' + lookup_name_from_id(conditions[1], audiences_map) + '"' + + end + if length > 1 + (0..length - 1).each do |n| + # Operand is handled here and made Upper Case + if OPERATORS.include?(conditions[n]) + operand = conditions[n].upcase + # Check if element is a list or not + elsif conditions[n].is_a?(Array) + # Check if at the end or not to determine where to add the operand + # Recursive call to call stringify on embedded list + conditions_str += if n + 1 < length + '(' + stringify_conditions(conditions[n], audiences_map) + ') ' + else + operand + ' (' + stringify_conditions(conditions[n], audiences_map) + ')' + end + # If the item is not a list, we process as an audience ID and retrieve the name + else + audience_name = lookup_name_from_id(conditions[n], audiences_map) + unless audience_name.nil? + # Below handles all cases for one ID or greater + conditions_str += if n + 1 < length - 1 + '"' + audience_name + '" ' + operand + ' ' + elsif n + 1 == length + operand + ' "' + audience_name + '"' + else + '"' + audience_name + '" ' + end + end + end + end + end + conditions_str || '' + end + + def replace_ids_with_names(conditions, audiences_map) + !conditions.empty? ? stringify_conditions(conditions, audiences_map) : '' + end + + def get_delivery_rules(rollouts, rollout_id, feature_id) + audiences_id_map = audiences_map + feature_variables_map = feature_variable_map + rollout = rollouts.select { |selected_rollout| selected_rollout['id'] == rollout_id } + if rollout.any? + rollout = rollout[0] + experiments = rollout['experiments'] + return experiments.map do |experiment| + { + 'id' => experiment['id'], + 'key' => experiment['key'], + 'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map), + 'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || '' + } + end + end + [] + end end end diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index 820f069c..82d8d221 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1061,4 +1061,16 @@ expect(config.feature_experiment?(experiment['id'])).to eq(false) end end + + describe '#rollout_experiment' do + let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler) } + + it 'should return true if the experiment is a rollout test' do + expect(config.rollout_experiment?('177770')).to eq(true) + end + + it 'should return false if the experiment is not a rollout test' do + expect(config.rollout_experiment?('177771')).to eq(false) + end + end end diff --git a/spec/optimizely_config_spec.rb b/spec/optimizely_config_spec.rb index f0b132dd..f95e8f10 100644 --- a/spec/optimizely_config_spec.rb +++ b/spec/optimizely_config_spec.rb @@ -20,38 +20,676 @@ describe Optimizely::OptimizelyConfig do let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON } - let(:config_typed_audience_JSON) { JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES) } + let(:similar_exp_keys_JSON) { OptimizelySpec::SIMILAR_EXP_KEYS_JSON } + let(:typed_audiences_JSON) { OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES_JSON } + let(:similar_rule_key_JSON) { OptimizelySpec::SIMILAR_RULE_KEYS_JSON } let(:error_handler) { Optimizely::NoOpErrorHandler.new } let(:spy_logger) { spy('logger') } let(:project_config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) } let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) } let(:optimizely_config) { project_instance.get_optimizely_config } + let(:project_config_sim_keys) { Optimizely::DatafileProjectConfig.new(similar_exp_keys_JSON, spy_logger, error_handler) } + let(:project_instance_sim_keys) { Optimizely::Project.new(similar_exp_keys_JSON, nil, spy_logger, error_handler) } + let(:optimizely_config_sim_keys) { project_instance_sim_keys.get_optimizely_config } + let(:project_config_typed_audiences) { Optimizely::DatafileProjectConfig.new(typed_audiences_JSON, spy_logger, error_handler) } + let(:project_instance_typed_audiences) { Optimizely::Project.new(typed_audiences_JSON, nil, spy_logger, error_handler) } + let(:optimizely_config_typed_audiences) { project_instance_typed_audiences.get_optimizely_config } + let(:project_config_similar_rule_keys) { Optimizely::DatafileProjectConfig.new(similar_rule_key_JSON, spy_logger, error_handler) } + let(:project_instance_similar_rule_keys) { Optimizely::Project.new(similar_rule_key_JSON, nil, spy_logger, error_handler) } + let(:optimizely_config_similar_rule_keys) { project_instance_similar_rule_keys.get_optimizely_config } it 'should return all experiments' do experiments_map = optimizely_config['experimentsMap'] expect(experiments_map.length).to eq(11) + + expected_experiment_map = { + 'group1_exp1' => { + 'audiences' => '', 'variationsMap' => { + 'g1_e1_v1' => { + 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => 'correlating_variation_name', + 'type' => 'string', 'value' => 'groupie_1_v1' + } + } + }, 'g1_e1_v2' => { + 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => 'correlating_variation_name', + 'type' => 'string', 'value' => 'groupie_1_v2' + } + } + } + } + }, + 'group1_exp2' => { + 'audiences' => '', 'variationsMap' => { + 'g1_e2_v1' => { + 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => 'correlating_variation_name', + 'type' => 'string', 'value' => 'groupie_2_v1' + } + } + }, 'g1_e2_v2' => { + 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => 'correlating_variation_name', + 'type' => 'string', 'value' => 'groupie_2_v2' + } + } + } + } + }, + 'group2_exp1' => { + 'audiences' => '', 'variationsMap' => { + 'g2_e1_v1' => { + 'variablesMap' => {} + }, 'g2_e1_v2' => { + 'variablesMap' => {} + } + } + }, + 'group2_exp2' => { + 'audiences' => '', 'variationsMap' => { + 'g2_e2_v1' => { + 'variablesMap' => {} + }, 'g2_e2_v2' => { + 'variablesMap' => {} + } + } + }, + 'test_experiment' => { + 'audiences' => '', 'variationsMap' => { + 'control' => { + 'variablesMap' => {} + }, 'variation' => { + 'variablesMap' => {} + } + } + }, + 'test_experiment_double_feature' => { + 'audiences' => '', 'variationsMap' => { + 'control' => { + 'variablesMap' => { + 'double_variable' => { + 'id' => '155551', 'key' => 'double_variable', 'type' => + 'double', 'value' => '42.42' + } + } + }, 'variation' => { + 'variablesMap' => { + 'double_variable' => { + 'id' => '155551', 'key' => 'double_variable', 'type' => + 'double', 'value' => '13.37' + } + } + } + } + }, + 'test_experiment_integer_feature' => { + 'audiences' => '', 'variationsMap' => { + 'control' => { + 'variablesMap' => { + 'integer_variable' => { + 'id' => '155553', 'key' => 'integer_variable', 'type' => + 'integer', 'value' => '42' + } + } + }, 'variation' => { + 'variablesMap' => { + 'integer_variable' => { + 'id' => '155553', 'key' => 'integer_variable', 'type' => + 'integer', 'value' => '13' + } + } + } + } + }, + 'test_experiment_multivariate' => { + 'audiences' => '', 'variationsMap' => { + 'Feorge' => { + 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'H' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'arry' + } + } + }, 'Fred' => { + 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'F' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'red' + } + } + }, 'George' => { + 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'G' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'eorge' + } + } + }, 'Gred' => { + 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'G' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'red' + } + } + } + } + }, + 'test_experiment_not_started' => { + 'audiences' => '', 'variationsMap' => { + 'control_not_started' => { + 'variablesMap' => {} + }, 'variation_not_started' => { + 'variablesMap' => {} + } + } + }, + 'test_experiment_with_audience' => { + 'audiences' => '', 'variationsMap' => { + 'control_with_audience' => { + 'variablesMap' => {} + }, 'variation_with_audience' => { + 'variablesMap' => {} + } + } + }, + 'test_experiment_with_feature_rollout' => { + 'audiences' => '', 'variationsMap' => { + 'control' => { + 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', 'type' => + 'string', 'value' => 'cta_1' + } + } + }, 'variation' => { + 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', 'type' => + 'string', 'value' => 'cta_2' + } + } + } + } + } + } project_config.experiments.each do |experiment| expect(experiments_map[experiment['key']]).to include( 'id' => experiment['id'], - 'key' => experiment['key'] + 'key' => experiment['key'], + 'audiences' => expected_experiment_map[experiment['key']]['audiences'] + ) + variations_map = experiments_map[experiment['key']]['variationsMap'] + experiment['variations'].each do |variation| + expect(variations_map[variation['key']]).to include( + 'id' => variation['id'], + 'key' => variation['key'], + 'variablesMap' => expected_experiment_map[experiment['key']]['variationsMap'][variation['key']]['variablesMap'] + ) + end + end + end + + it 'should return correct experiment ids with similar keys' do + experiments_map = optimizely_config_sim_keys['experimentsMap'] + expect(experiments_map.length).to eq(1) + + experiment_map_flag_1 = optimizely_config_sim_keys['featuresMap']['flag1']['experimentsMap'] + experiment_map_flag_2 = optimizely_config_sim_keys['featuresMap']['flag2']['experimentsMap'] + + expect(experiment_map_flag_1['targeted_delivery']['id']).to eq('9300000007569') + expect(experiment_map_flag_2['targeted_delivery']['id']).to eq('9300000007573') + end + + it 'should return all events' do + events = optimizely_config['events'] + expected_events = [{'experimentIds' => %w[111127 122230], 'id' => '111095', 'key' => 'test_event'}, + {'experimentIds' => ['111127'], 'id' => '111096', 'key' => 'Total Revenue'}, + {'experimentIds' => ['122227'], + 'id' => '111097', + 'key' => 'test_event_with_audience'}, + {'experimentIds' => ['100027'], + 'id' => '111098', + 'key' => 'test_event_not_running'}] + expect(events).to eq(expected_events) + end + + it 'should return all attributes' do + attributes = optimizely_config['attributes'] + expected_attributes = [{'id' => '111094', 'key' => 'browser_type'}, + {'id' => '111095', 'key' => 'boolean_key'}, + {'id' => '111096', 'key' => 'integer_key'}, + {'id' => '111097', 'key' => 'double_key'}] + expect(attributes).to eq(expected_attributes) + end + + it 'should return all experiments in typed audiences' do + experiments_map = optimizely_config_typed_audiences['experimentsMap'] + expect(experiments_map.length).to eq(4) + + expected_experiment_map = { + 'audience_combinations_experiment' => { + 'audiences' => + '("exactString" OR "substringString") AND ("exists" OR "exactNumber" OR "gtNumber" OR "ltNumber" OR "exactBoolean")', + 'variationsMap' => { + 'A' => { + 'id' => '1423767504', 'key' => 'A', 'variablesMap' => {} + } + } + }, + 'feat2_with_var_test' => { + 'audiences' => + '("exactString" OR "substringString") AND ("exists" OR "exactNumber" OR "gtNumber" OR "ltNumber" OR "exactBoolean")', + 'variationsMap' => { + 'variation_2' => { + 'variablesMap' => { + 'z' => { + 'id' => '11535264367', 'key' => 'z', 'type' => 'integer', + 'value' => '150' + } + } + } + } + }, + 'feat_with_var_test' => { + 'audiences' => '', 'variationsMap' => { + 'variation_2' => { + 'variablesMap' => { + 'x' => { + 'id' => '11535264366', 'key' => 'x', 'type' => 'string', + 'value' => 'xyz' + } + } + } + } + }, + 'typed_audience_experiment' => { + 'audiences' => '', 'variationsMap' => { + 'A' => { + 'id' => '1423767503', 'key' => 'A', 'variablesMap' => {} + } + } + } + } + project_config_typed_audiences.experiments.each do |experiment| + expect(experiments_map[experiment['key']]).to include( + 'id' => experiment['id'], + 'key' => experiment['key'], + 'audiences' => expected_experiment_map[experiment['key']]['audiences'] ) variations_map = experiments_map[experiment['key']]['variationsMap'] experiment['variations'].each do |variation| expect(variations_map[variation['key']]).to include( 'id' => variation['id'], - 'key' => variation['key'] + 'key' => variation['key'], + 'variablesMap' => expected_experiment_map[experiment['key']]['variationsMap'][variation['key']]['variablesMap'] ) end end end + it 'should return all rollouts with similar keys' do + experiments_map = optimizely_config_similar_rule_keys['experimentsMap'] + expect(experiments_map.length).to eq(0) + + rollout_flag_1 = optimizely_config_similar_rule_keys['featuresMap']['flag_1']['deliveryRules'][0] + rollout_flag_2 = optimizely_config_similar_rule_keys['featuresMap']['flag_2']['deliveryRules'][0] + rollout_flag_3 = optimizely_config_similar_rule_keys['featuresMap']['flag_3']['deliveryRules'][0] + + expect(rollout_flag_1['id']).to eq('9300000004977') + expect(rollout_flag_1['key']).to eq('targeted_delivery') + expect(rollout_flag_2['id']).to eq('9300000004979') + expect(rollout_flag_2['key']).to eq('targeted_delivery') + expect(rollout_flag_3['id']).to eq('9300000004981') + expect(rollout_flag_3['key']).to eq('targeted_delivery') + end + it 'should return all feature flags' do features_map = optimizely_config['featuresMap'] expect(features_map.length).to eq(10) + expected_features_map = { + 'all_variables_feature' => { + 'deliveryRules' => [], 'experimentRules' => [] + }, + 'boolean_feature' => { + 'deliveryRules' => [], 'experimentRules' => [{ + 'audiences' => '', 'id' => '122227', 'key' => + 'test_experiment_with_audience', 'variationsMap' => { + 'control_with_audience' => { + 'featureEnabled' => true, 'id' => '122228', 'key' => + 'control_with_audience', 'variablesMap' => {} + }, 'variation_with_audience' => { + 'featureEnabled' => true, 'id' => '122229', 'key' => + 'variation_with_audience', 'variablesMap' => {} + } + } + }] + }, + 'boolean_single_variable_feature' => { + 'deliveryRules' => [{ + 'audiences' => '', 'id' => '177770', 'key' => '177770', + 'variationsMap' => { + '177771' => { + 'featureEnabled' => true, 'id' => '177771', 'key' => + '177771', 'variablesMap' => { + 'boolean_variable' => { + 'id' => '155556', 'key' => 'boolean_variable', + 'type' => 'boolean', 'value' => 'true' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177772', 'key' => '177772', + 'variationsMap' => { + '177773' => { + 'featureEnabled' => false, 'id' => '177773', 'key' => + '177773', 'variablesMap' => { + 'boolean_variable' => { + 'id' => '155556', 'key' => 'boolean_variable', + 'type' => 'boolean', 'value' => 'true' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177776', 'key' => '177776', + 'variationsMap' => { + '177778' => { + 'featureEnabled' => true, 'id' => '177778', 'key' => + '177778', 'variablesMap' => { + 'boolean_variable' => { + 'id' => '155556', 'key' => 'boolean_variable', + 'type' => 'boolean', 'value' => 'false' + } + } + } + } + }], 'experimentRules' => [] + }, + 'double_single_variable_feature' => { + 'deliveryRules' => [], 'experimentRules' => [{ + 'audiences' => '', 'id' => '122238', 'key' => + 'test_experiment_double_feature', 'variationsMap' => { + 'control' => { + 'featureEnabled' => true, 'id' => '122239', 'key' => + 'control', 'variablesMap' => { + 'double_variable' => { + 'id' => '155551', 'key' => 'double_variable', + 'type' => 'double', 'value' => '42.42' + } + } + }, 'variation' => { + 'featureEnabled' => true, 'id' => '122240', 'key' => + 'variation', 'variablesMap' => { + 'double_variable' => { + 'id' => '155551', 'key' => 'double_variable', + 'type' => 'double', 'value' => '13.37' + } + } + } + } + }] + }, + 'empty_feature' => { + 'deliveryRules' => [], 'experimentRules' => [] + }, + 'integer_single_variable_feature' => { + 'deliveryRules' => [], 'experimentRules' => [{ + 'audiences' => '', 'id' => '122241', 'key' => + 'test_experiment_integer_feature', 'variationsMap' => { + 'control' => { + 'featureEnabled' => true, 'id' => '122242', 'key' => + 'control', 'variablesMap' => { + 'integer_variable' => { + 'id' => '155553', 'key' => 'integer_variable', + 'type' => 'integer', 'value' => '42' + } + } + }, 'variation' => { + 'featureEnabled' => true, 'id' => '122243', 'key' => + 'variation', 'variablesMap' => { + 'integer_variable' => { + 'id' => '155553', 'key' => 'integer_variable', + 'type' => 'integer', 'value' => '13' + } + } + } + } + }] + }, + 'json_single_variable_feature' => { + 'deliveryRules' => [{ + 'audiences' => '', 'id' => '177774', 'key' => '177774', + 'variationsMap' => { + '177775' => { + 'featureEnabled' => true, 'id' => '177775', 'key' => + '177775', 'variablesMap' => { + 'json_variable' => { + 'id' => '1555588', 'key' => 'json_variable', 'type' => + 'json', 'value' => + '{ "val": "wingardium leviosa" }' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177779', 'key' => '177779', + 'variationsMap' => { + '177780' => { + 'featureEnabled' => true, 'id' => '177780', 'key' => + '177780', 'variablesMap' => { + 'json_variable' => { + 'id' => '1555588', 'key' => 'json_variable', 'type' => + 'json', 'value' => + '{ "val": "wingardium leviosa" }' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177780', 'key' => + 'rollout_exp_with_diff_id_and_key', 'variationsMap' => { + 'rollout_var_with_diff_id_and_key' => { + 'featureEnabled' => true, 'id' => '177781', 'key' => + 'rollout_var_with_diff_id_and_key', 'variablesMap' => { + 'json_variable' => { + 'id' => '1555588', 'key' => 'json_variable', 'type' => + 'json', 'value' => + '{ "val": "wingardium leviosa" }' + } + } + } + } + }], 'experimentRules' => [] + }, + 'multi_variate_feature' => { + 'deliveryRules' => [], 'experimentRules' => [{ + 'audiences' => '', 'id' => '122230', 'key' => + 'test_experiment_multivariate', 'variationsMap' => { + 'Feorge' => { + 'featureEnabled' => false, 'id' => '122232', 'key' => + 'Feorge', 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'H' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'arry' + } + } + }, 'Fred' => { + 'featureEnabled' => true, 'id' => '122231', 'key' => + 'Fred', 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'F' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'red' + } + } + }, 'George' => { + 'featureEnabled' => true, 'id' => '122234', 'key' => + 'George', 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'G' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'eorge' + } + } + }, 'Gred' => { + 'featureEnabled' => true, 'id' => '122233', 'key' => + 'Gred', 'variablesMap' => { + 'first_letter' => { + 'id' => '155560', 'key' => 'first_letter', 'type' => + 'string', 'value' => 'G' + }, 'rest_of_name' => { + 'id' => '155561', 'key' => 'rest_of_name', 'type' => + 'string', 'value' => 'red' + } + } + } + } + }] + }, + 'mutex_group_feature' => { + 'deliveryRules' => [], 'experimentRules' => [{ + 'audiences' => '', 'id' => '133331', 'key' => 'group1_exp1', + 'variationsMap' => { + 'g1_e1_v1' => { + 'featureEnabled' => true, 'id' => '130001', 'key' => + 'g1_e1_v1', 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => + 'correlating_variation_name', 'type' => 'string', + 'value' => 'groupie_1_v1' + } + } + }, 'g1_e1_v2' => { + 'featureEnabled' => true, 'id' => '130002', 'key' => + 'g1_e1_v2', 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => + 'correlating_variation_name', 'type' => 'string', + 'value' => 'groupie_1_v2' + } + } + } + } + }, { + 'audiences' => '', 'id' => '133332', 'key' => 'group1_exp2', + 'variationsMap' => { + 'g1_e2_v1' => { + 'featureEnabled' => true, 'id' => '130003', 'key' => + 'g1_e2_v1', 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => + 'correlating_variation_name', 'type' => 'string', + 'value' => 'groupie_2_v1' + } + } + }, 'g1_e2_v2' => { + 'featureEnabled' => true, 'id' => '130004', 'key' => + 'g1_e2_v2', 'variablesMap' => { + 'correlating_variation_name' => { + 'id' => '155563', 'key' => + 'correlating_variation_name', 'type' => 'string', + 'value' => 'groupie_2_v2' + } + } + } + } + }] + }, + 'string_single_variable_feature' => { + 'deliveryRules' => [{ + 'audiences' => '', 'id' => '177774', 'key' => '177774', + 'variationsMap' => { + '177775' => { + 'featureEnabled' => true, 'id' => '177775', 'key' => + '177775', 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', + 'type' => 'string', 'value' => 'wingardium leviosa' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177779', 'key' => '177779', + 'variationsMap' => { + '177780' => { + 'featureEnabled' => true, 'id' => '177780', 'key' => + '177780', 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', + 'type' => 'string', 'value' => 'wingardium leviosa' + } + } + } + } + }, { + 'audiences' => '', 'id' => '177780', 'key' => + 'rollout_exp_with_diff_id_and_key', 'variationsMap' => { + 'rollout_var_with_diff_id_and_key' => { + 'featureEnabled' => true, 'id' => '177781', 'key' => + 'rollout_var_with_diff_id_and_key', 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', + 'type' => 'string', 'value' => 'wingardium leviosa' + } + } + } + } + }], 'experimentRules' => [{ + 'audiences' => '', 'id' => '122235', 'key' => + 'test_experiment_with_feature_rollout', 'variationsMap' => { + 'control' => { + 'featureEnabled' => true, 'id' => '122236', 'key' => + 'control', 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', + 'type' => 'string', 'value' => 'cta_1' + } + } + }, 'variation' => { + 'featureEnabled' => true, 'id' => '122237', 'key' => + 'variation', 'variablesMap' => { + 'string_variable' => { + 'id' => '155558', 'key' => 'string_variable', + 'type' => 'string', 'value' => 'cta_2' + } + } + } + } + }] + } + } project_config.feature_flags.each do |feature_flag| expect(features_map[feature_flag['key']]).to include( 'id' => feature_flag['id'], - 'key' => feature_flag['key'] + 'key' => feature_flag['key'], + 'deliveryRules' => expected_features_map[feature_flag['key']]['deliveryRules'], + 'experimentRules' => expected_features_map[feature_flag['key']]['experimentRules'] ) experiments_map = features_map[feature_flag['key']]['experimentsMap'] feature_flag['experimentIds'].each do |experiment_id| @@ -91,6 +729,47 @@ end end + it 'should serialize audiences and replace ids with names' do + audience_conditions = + [ + %w[or 3468206642 3988293898], + %w[or 3468206642 3988293898 3468206646], + %w[not 3468206642], + %w[or 3468206642], + %w[and 3468206642], + ['3468206642'], + %w[3468206642 3988293898], + ['and', %w[or 3468206642 3988293898], '3468206646'], + ['and', ['or', '3468206642', %w[and 3988293898 3468206646]], ['and', '3988293899', %w[or 3468206647 3468206643]]], + %w[and and], + ['not', %w[and 3468206642 3988293898]], + [], + %w[or 3468206642 999999999] + ] + + expected_audience_outputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"' + ] + optimizely_config = Optimizely::OptimizelyConfig.new(project_instance_typed_audiences.send(:project_config)) + audiences_map = optimizely_config.send(:audiences_map) + audience_conditions.each_with_index do |audience_condition, index| + result = optimizely_config.send(:replace_ids_with_names, audience_condition, audiences_map) + expect(result).to eq(expected_audience_outputs[index]) + end + end + it 'should return correct config revision' do expect(project_config.revision).to eq(optimizely_config['revision']) end @@ -106,4 +785,9 @@ it 'should return correct datafile string' do expect(project_config.datafile).to eq(optimizely_config['datafile']) end + + it 'should return default sdk key and environment key' do + expect(optimizely_config_similar_rule_keys['sdkKey']).to eq('') + expect(optimizely_config_similar_rule_keys['environmentKey']).to eq('') + end end diff --git a/spec/spec_params.rb b/spec/spec_params.rb index ef21d0a8..4647f4ea 100644 --- a/spec/spec_params.rb +++ b/spec/spec_params.rb @@ -1132,12 +1132,330 @@ module OptimizelySpec 'sendFlagDecisions' => true }.freeze + SIMILAR_EXP_KEYS = { + 'version' => '4', + 'rollouts' => [], + 'sdkKey' => 'SIMILAR_KEYS', + 'environmentKey' => 'SIMILAR_KEYS_ENVIRONMENT', + 'typedAudiences' => [ + { + 'id' => '20415611520', + 'conditions' => ['and', ['or', ['or', + { + 'value' => true, + 'type' => 'custom_attribute', + 'name' => 'hiddenLiveEnabled', + 'match' => 'exact' + }]]], + 'name' => 'test1' + }, + { + 'id' => '20406066925', + 'conditions' => ['and', ['or', ['or', + { + 'value' => false, + 'type' => 'custom_attribute', + 'name' => 'hiddenLiveEnabled', + 'match' => 'exact' + }]]], + 'name' => 'test2' + } + ], 'anonymizeIP' => true, 'projectId' => '20430981610', + 'variables' => [], 'featureFlags' => [ + { + 'experimentIds' => ['9300000007569'], + 'rolloutId' => '', + 'variables' => [], + 'id' => '3045', + 'key' => 'flag1' + }, + { + 'experimentIds' => ['9300000007573'], + 'rolloutId' => '', + 'variables' => [], + 'id' => '3046', + 'key' => 'flag2' + } + ], 'experiments' => [ + { + 'status' => 'Running', + 'audienceConditions' => %w[or 20415611520], + 'audienceIds' => ['20415611520'], + 'variations' => [ + { + 'variables' => [], + 'id' => '8045', + 'key' => 'variation1', + 'featureEnabled' => true + } + ], + 'forcedVariations' => + {}, + 'key' => 'targeted_delivery', + 'layerId' => '9300000007569', + 'trafficAllocation' => [ + { + 'entityId' => '8045', + 'endOfRange' => 10_000 + } + ], + 'id' => '9300000007569' + }, + { + 'status' => 'Running', + 'audienceConditions' => %w[or 20406066925], + 'audienceIds' => ['20406066925'], + 'variations' => [ + { + 'variables' => [], + 'id' => '8048', + 'key' => 'variation2', + 'featureEnabled' => true + } + ], + 'forcedVariations' => + {}, + 'key' => 'targeted_delivery', + 'layerId' => '9300000007573', + 'trafficAllocation' => [ + { + 'entityId' => '8048', + 'endOfRange' => 10_000 + } + ], + 'id' => '9300000007573' + } + ], 'audiences' => [ + { + 'id' => '20415611520', + 'conditions' => + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + 'name' => 'test1' + }, + { + 'id' => '20406066925', + 'conditions' => + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + 'name' => 'test2' + }, + { + 'conditions' => + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + 'id' => '$opt_dummy_audience', + 'name' => + 'Optimizely-Generated Audience for Backwards Compatibility' + } + ], 'groups' => [], 'attributes' => [ + { + 'id' => '20408641883', + 'key' => 'hiddenLiveEnabled' + } + ], 'botFiltering' => false, 'accountId' => '17882702980', 'events' => [], + 'revision' => '25', 'sendFlagDecisions' => true + }.freeze + + SIMILAR_RULE_KEYS = { + 'version' => '4', + 'rollouts' => [ + { + 'experiments' => [ + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [{ + 'variables' => [], + 'id' => '5452', + 'key' => 'on', + 'featureEnabled' => true + }], + 'forcedVariations' => {}, + 'key' => 'targeted_delivery', + 'layerId' => '9300000004981', + 'trafficAllocation' => [{ + 'entityId' => '5452', 'endOfRange' => 10_000 + }], + 'id' => '9300000004981' + }, + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [{ + 'variables' => [], + 'id' => '5451', + 'key' => 'off', + 'featureEnabled' => false + }], + 'forcedVariations' => {}, + 'key' => 'default-rollout-2029-20301771717', + 'layerId' => 'default-layer-rollout-2029-20301771717', + 'trafficAllocation' => [{ + 'entityId' => '5451', 'endOfRange' => 10_000 + }], + 'id' => 'default-rollout-2029-20301771717' + } + ], + 'id' => 'rollout-2029-20301771717' + }, + { + 'experiments' => [ + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [ + { + 'variables' => [], + 'id' => '5450', + 'key' => 'on', + 'featureEnabled' => true + } + ], + 'forcedVariations' => {}, + 'key' => 'targeted_delivery', + 'layerId' => '9300000004979', + 'trafficAllocation' => [ + { + 'entityId' => '5450', + 'endOfRange' => 10_000 + } + ], + 'id' => '9300000004979' + }, + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [ + { + 'variables' => [], + 'id' => '5449', + 'key' => 'off', + 'featureEnabled' => false + } + ], + 'forcedVariations' => {}, + 'key' => 'default-rollout-2028-20301771717', + 'layerId' => 'default-layer-rollout-2028-20301771717', + 'trafficAllocation' => [ + { + 'entityId' => '5449', + 'endOfRange' => 10_000 + } + ], + 'id' => 'default-rollout-2028-20301771717' + } + ], + 'id' => 'rollout-2028-20301771717' + }, + { + 'experiments' => [ + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [ + { + 'variables' => [], + 'id' => '5448', + 'key' => 'on', + 'featureEnabled' => true + } + ], + 'forcedVariations' => {}, + 'key' => 'targeted_delivery', + 'layerId' => '9300000004977', + 'trafficAllocation' => [ + { + 'entityId' => '5448', + 'endOfRange' => 10_000 + } + ], 'id' => '9300000004977' + }, + { + 'status' => 'Running', + 'audienceConditions' => [], + 'audienceIds' => [], + 'variations' => [ + { + 'variables' => [], + 'id' => '5447', + 'key' => 'off', + 'featureEnabled' => false + } + ], + 'forcedVariations' => {}, + 'key' => 'default-rollout-2027-20301771717', + 'layerId' => 'default-layer-rollout-2027-20301771717', + 'trafficAllocation' => [ + { + 'entityId' => '5447', 'endOfRange' => 10_000 + } + ], + 'id' => 'default-rollout-2027-20301771717' + } + ], + 'id' => 'rollout-2027-20301771717' + } + ], + 'typedAudiences' => [], + 'anonymizeIP' => true, + 'projectId' => '20286295225', + 'variables' => [], + 'featureFlags' => [ + { + 'experimentIds' => [], + 'rolloutId' => + 'rollout-2029-20301771717', + 'variables' => [], + 'id' => '2029', + 'key' => 'flag_3' + }, + { + 'experimentIds' => [], + 'rolloutId' => 'rollout-2028-20301771717', + 'variables' => [], + 'id' => '2028', + 'key' => 'flag_2' + }, + { + 'experimentIds' => [], + 'rolloutId' => 'rollout-2027-20301771717', + 'variables' => [], + 'id' => '2027', + 'key' => 'flag_1' + } + ], + 'experiments' => [], + 'audiences' => [ + { + 'conditions' => + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + 'id' => '$opt_dummy_audience', 'name' => + 'Optimizely-Generated Audience for Backwards Compatibility' + } + ], + 'groups' => [], + 'attributes' => [], + 'botFiltering' => false, + 'accountId' => '19947277778', + 'events' => [], + 'revision' => '11', + 'sendFlagDecisions' => true + }.freeze + VALID_CONFIG_BODY_JSON = JSON.dump(VALID_CONFIG_BODY) INVALID_CONFIG_BODY = VALID_CONFIG_BODY.dup INVALID_CONFIG_BODY['version'] = '5' INVALID_CONFIG_BODY_JSON = JSON.dump(INVALID_CONFIG_BODY) + SIMILAR_EXP_KEYS_JSON = JSON.dump(SIMILAR_EXP_KEYS) + + CONFIG_DICT_WITH_TYPED_AUDIENCES_JSON = JSON.dump(CONFIG_DICT_WITH_TYPED_AUDIENCES) + SIMILAR_RULE_KEYS_JSON = JSON.dump(SIMILAR_RULE_KEYS) + # SEND_FLAG_DECISIONS_DISABLED_CONFIG = VALID_CONFIG_BODY.dup # SEND_FLAG_DECISIONS_DISABLED_CONFIG['sendFlagDecisions'] = false end