diff --git a/.gitignore b/.gitignore index d37791c7..637df6ef 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ Gemfile.lock /tmp/ *.DS_Store .ruby-version -vendor/bundle \ No newline at end of file +vendor/bundle +*.code-workspace diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 9ef4895d..7b1896f4 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -140,7 +140,10 @@ def activate(experiment_key, user_id, attributes = nil) # Create and dispatch impression event experiment = config.get_experiment_from_key(experiment_key) - send_impression(config, experiment, variation_key, user_id, attributes) + send_impression( + config, experiment, variation_key, '', experiment_key, + Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes + ) variation_key end @@ -316,14 +319,23 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil) experiment_key: decision.experiment['key'], variation_key: variation['key'] } - # Send event if Decision came from an experiment. - send_impression(config, decision.experiment, variation['key'], user_id, attributes) - else - @logger.log(Logger::DEBUG, - "The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.") + # Send event if Decision came from a feature test. + send_impression( + config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], source_string, user_id, attributes + ) + elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions + send_impression( + config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], source_string, user_id, attributes + ) end end + if decision.nil? && config.send_flag_decisions + send_impression( + config, nil, '', feature_flag_key, '', source_string, user_id, attributes + ) + end + @notification_center.send_notifications( NotificationCenter::NOTIFICATION_TYPES[:DECISION], Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'], @@ -867,15 +879,42 @@ def validate_instantiation_options raise InvalidInputError, 'event_dispatcher' end - def send_impression(config, experiment, variation_key, user_id, attributes = nil) + def send_impression(config, experiment, variation_key, flag_key, rule_key, rule_type, user_id, attributes = nil) + if experiment.nil? + experiment = { + 'id' => '', + 'key' => '', + 'layerId' => '', + 'status' => '', + 'variations' => [], + 'trafficAllocation' => [], + 'audienceIds' => [], + 'audienceConditions' => [], + 'forcedVariations' => {} + } + end + experiment_key = experiment['key'] - variation_id = config.get_variation_id_from_key(experiment_key, variation_key) - user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, user_id, attributes) + + variation_id = '' + variation_id = config.get_variation_id_from_key(experiment_key, variation_key) if experiment_key != '' + + metadata = { + flag_key: flag_key, + rule_key: rule_key, + rule_type: rule_type, + variation_key: variation_key + } + + user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes) @event_processor.process(user_event) return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive? @logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.") - variation = config.get_variation_from_id(experiment_key, variation_id) + + experiment = nil if experiment_key == '' + variation = nil + variation = config.get_variation_from_id(experiment_key, variation_id) unless experiment.nil? log_event = EventFactory.create_log_event(user_event, @logger) @notification_center.send_notifications( NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE], diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 5dab58bd..0babb9e5 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -40,6 +40,7 @@ class DatafileProjectConfig < ProjectConfig attr_reader :revision attr_reader :rollouts attr_reader :version + attr_reader :send_flag_decisions attr_reader :attribute_key_map attr_reader :audience_id_map @@ -83,6 +84,7 @@ def initialize(datafile, logger, error_handler) @bot_filtering = config['botFiltering'] @revision = config['revision'] @rollouts = config.fetch('rollouts', []) + @send_flag_decisions = config.fetch('sendFlagDecisions', false) # Json type is represented in datafile as a subtype of string for the sake of backwards compatibility. # Converting it to a first-class json type while creating Project Config diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index e5fab77a..2b8e0d67 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -40,6 +40,7 @@ class DecisionService Decision = Struct.new(:experiment, :variation, :source) DECISION_SOURCES = { + 'EXPERIMENT' => 'experiment', 'FEATURE_TEST' => 'feature-test', 'ROLLOUT' => 'rollout' }.freeze diff --git a/lib/optimizely/event/entity/decision.rb b/lib/optimizely/event/entity/decision.rb index 533a91d0..44c607de 100644 --- a/lib/optimizely/event/entity/decision.rb +++ b/lib/optimizely/event/entity/decision.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,19 +17,21 @@ # module Optimizely class Decision - attr_reader :campaign_id, :experiment_id, :variation_id + attr_reader :campaign_id, :experiment_id, :variation_id, :metadata - def initialize(campaign_id:, experiment_id:, variation_id:) + def initialize(campaign_id:, experiment_id:, variation_id:, metadata:) @campaign_id = campaign_id @experiment_id = experiment_id @variation_id = variation_id + @metadata = metadata end def as_json { campaign_id: @campaign_id, experiment_id: @experiment_id, - variation_id: @variation_id + variation_id: @variation_id, + metadata: @metadata } end end diff --git a/lib/optimizely/event/entity/impression_event.rb b/lib/optimizely/event/entity/impression_event.rb index e0dd3ad9..4a27ef39 100644 --- a/lib/optimizely/event/entity/impression_event.rb +++ b/lib/optimizely/event/entity/impression_event.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ require 'optimizely/helpers/date_time_utils' module Optimizely class ImpressionEvent < UserEvent - attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id, + attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id, :metadata, :visitor_attributes, :bot_filtering def initialize( @@ -28,6 +28,7 @@ def initialize( experiment_layer_id:, experiment_id:, variation_id:, + metadata:, visitor_attributes:, bot_filtering: ) @@ -38,6 +39,7 @@ def initialize( @experiment_layer_id = experiment_layer_id @experiment_id = experiment_id @variation_id = variation_id + @metadata = metadata @visitor_attributes = visitor_attributes @bot_filtering = bot_filtering end diff --git a/lib/optimizely/event/event_factory.rb b/lib/optimizely/event/event_factory.rb index 754117b5..a416ff66 100644 --- a/lib/optimizely/event/event_factory.rb +++ b/lib/optimizely/event/event_factory.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -101,10 +101,11 @@ def build_attribute_list(user_attributes, project_config) private def create_impression_event_visitor(impression_event) - decision = Optimizely::Decision.new( + decision = Decision.new( campaign_id: impression_event.experiment_layer_id, experiment_id: impression_event.experiment_id, - variation_id: impression_event.variation_id + variation_id: impression_event.variation_id, + metadata: impression_event.metadata ) snapshot_event = Optimizely::SnapshotEvent.new( diff --git a/lib/optimizely/event/user_event_factory.rb b/lib/optimizely/event/user_event_factory.rb index b1dd8f52..f7852341 100644 --- a/lib/optimizely/event/user_event_factory.rb +++ b/lib/optimizely/event/user_event_factory.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ module Optimizely class UserEventFactory # UserEventFactory builds ImpressionEvent and ConversionEvent objects from a given user_event. - def self.create_impression_event(project_config, experiment, variation_id, user_id, user_attributes) + def self.create_impression_event(project_config, experiment, variation_id, metadata, user_id, user_attributes) # Create impression Event to be sent to the logging endpoint. # # project_config - Instance of ProjectConfig @@ -42,13 +42,14 @@ def self.create_impression_event(project_config, experiment, variation_id, user_ ).as_json visitor_attributes = Optimizely::EventFactory.build_attribute_list(user_attributes, project_config) - experiment_layer_id = project_config.experiment_key_map[experiment['key']]['layerId'] + experiment_layer_id = experiment['layerId'] Optimizely::ImpressionEvent.new( event_context: event_context, user_id: user_id, experiment_layer_id: experiment_layer_id, experiment_id: experiment['id'], variation_id: variation_id, + metadata: metadata, visitor_attributes: visitor_attributes, bot_filtering: project_config.bot_filtering ) diff --git a/lib/optimizely/project_config.rb b/lib/optimizely/project_config.rb index 1ee3868a..5a4e2115 100644 --- a/lib/optimizely/project_config.rb +++ b/lib/optimizely/project_config.rb @@ -46,6 +46,8 @@ def bot_filtering; end def revision; end + def send_flag_decisions; end + def rollouts; end def experiment_running?(experiment); end diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index eb7a4cdf..43d7fdac 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require 'json' require 'spec_helper' require 'optimizely/config/datafile_project_config' require 'optimizely/exceptions' @@ -50,6 +51,7 @@ expect(project_config.groups).to eq(config_body['groups']) expect(project_config.project_id).to eq(config_body['projectId']) expect(project_config.revision).to eq(config_body['revision']) + expect(project_config.send_flag_decisions).to eq(config_body['sendFlagDecisions']) expected_attribute_key_map = { 'browser_type' => config_body['attributes'][0], @@ -769,6 +771,15 @@ expect(project_config.audience_id_map).to eq(expected_audience_id_map) end + + it 'should initialize send_flag_decisions to false when not in datafile' do + config_body_without_flag_decision = config_body.dup + config_body_without_flag_decision.delete('sendFlagDecisions') + config_body_json = JSON.dump(config_body_without_flag_decision) + project_config = Optimizely::DatafileProjectConfig.new(config_body_json, logger, error_handler) + + expect(project_config.send_flag_decisions).to eq(false) + end end describe '@logger' do diff --git a/spec/event/batch_event_processor_spec.rb b/spec/event/batch_event_processor_spec.rb index 8968ff48..14e0d01c 100644 --- a/spec/event/batch_event_processor_spec.rb +++ b/spec/event/batch_event_processor_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -310,7 +310,15 @@ ) experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', nil) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event( + project_config, experiment, '111128', metadata, 'test_user', nil + ) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) allow(Optimizely::EventFactory).to receive(:create_log_event).with(any_args).and_return(log_event) diff --git a/spec/event/event_entities_spec.rb b/spec/event/event_entities_spec.rb index 4ce12062..7d747ed2 100644 --- a/spec/event/event_entities_spec.rb +++ b/spec/event/event_entities_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,7 +49,13 @@ decisions: [{ campaign_id: '7719770039', experiment_id: '111127', - variation_id: '111128' + variation_id: '111128', + metadata: { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } }], events: [{ entity_id: '7719770039', @@ -128,7 +134,13 @@ key: 'campaign_activated' ) - decision = Optimizely::Decision.new(campaign_id: '7719770039', experiment_id: '111127', variation_id: '111128') + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + decision = Optimizely::Decision.new(campaign_id: '7719770039', experiment_id: '111127', variation_id: '111128', metadata: metadata) snapshot = Optimizely::Snapshot.new(events: [snapshot_event.as_json], decisions: [decision.as_json]) visitor = Optimizely::Visitor.new( snapshots: [snapshot.as_json], diff --git a/spec/event/event_factory_spec.rb b/spec/event/event_factory_spec.rb index 9c5d5467..b92661be 100644 --- a/spec/event/event_factory_spec.rb +++ b/spec/event/event_factory_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -50,7 +50,13 @@ decisions: [{ campaign_id: '1', experiment_id: '111127', - variation_id: '111128' + variation_id: '111128', + metadata: { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } }], events: [{ entity_id: '1', @@ -96,7 +102,13 @@ it 'should create valid Event when create_impression_event is called without attributes' do experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', nil) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', nil) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -112,7 +124,13 @@ ) experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', 'browser_type' => 'firefox') log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) @@ -157,7 +175,13 @@ 'double_key' => 5.5 } - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', attributes) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', attributes) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -192,7 +216,13 @@ 'double_key' => {} } - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', attributes) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', attributes) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -208,7 +238,13 @@ ) experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', 'browser_type' => false) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) @@ -225,7 +261,13 @@ ) experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', 'browser_type' => 0) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', 'browser_type' => 0) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -234,7 +276,13 @@ it 'should create a valid Event when create_impression_event is called with attributes is not in the datafile' do experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', invalid_attribute: 'sorry_not_sorry') log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) @@ -463,7 +511,13 @@ '$opt_bucketing_id' => 'variation' } experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', user_attributes) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', user_attributes) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -493,7 +547,13 @@ } experiment = project_config.get_experiment_from_key('test_experiment') expect(project_config.send(:bot_filtering)).to eq(true) - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', user_attributes) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', metadata, 'test_user', user_attributes) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) @@ -523,7 +583,15 @@ } experiment = project_config.get_experiment_from_key('test_experiment') allow(project_config).to receive(:bot_filtering).and_return(false) - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', user_attributes) + metadata = { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: '111128' + } + impression_event = Optimizely::UserEventFactory.create_impression_event( + project_config, experiment, '111128', metadata, 'test_user', user_attributes + ) log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger) expect(log_event.params).to eq(@expected_impression_params) expect(log_event.url).to eq(@expected_endpoint) diff --git a/spec/event/user_event_factory_spec.rb b/spec/event/user_event_factory_spec.rb index a5fe09e8..f9876c23 100644 --- a/spec/event/user_event_factory_spec.rb +++ b/spec/event/user_event_factory_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2019, Optimizely and contributors +# Copyright 2016-2020, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,19 @@ describe '.create_impression_event' do it 'should return Impression Event when called without attributes' do experiment = project_config.get_experiment_from_key('test_experiment') - impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', nil) + impression_event = Optimizely::UserEventFactory.create_impression_event( + project_config, + experiment, + '111128', + { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: 'control' + }, + 'test_user', + nil + ) expect(impression_event.event_context[:account_id]).to eq(project_config.account_id) expect(impression_event.event_context[:project_id]).to eq(project_config.project_id) expect(impression_event.event_context[:revision]).to eq(project_config.revision) @@ -51,6 +63,12 @@ project_config, experiment, '111128', + { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: 'control' + }, 'test_user', user_attributes ) diff --git a/spec/project_spec.rb b/spec/project_spec.rb index deb6caaa..957add6e 100644 --- a/spec/project_spec.rb +++ b/spec/project_spec.rb @@ -154,7 +154,13 @@ class InvalidErrorHandler; end decisions: [{ campaign_id: '1', experiment_id: '111127', - variation_id: '111128' + variation_id: '111128', + metadata: { + flag_key: '', + rule_key: 'test_experiment', + rule_type: 'experiment', + variation_key: 'control' + } }], events: [{ entity_id: '1', @@ -222,6 +228,13 @@ class InvalidErrorHandler; end }] params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '3' + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'test_experiment_with_audience', + rule_type: 'experiment', + variation_key: 'control_with_audience' + } + variation_to_return = project_config.get_variation_from_id('test_experiment_with_audience', '122228') allow(project_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -289,6 +302,14 @@ class InvalidErrorHandler; end params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '1630555627' variation_to_return = @project_config.get_variation_from_id('typed_audience_experiment', '1423767503') + + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'typed_audience_experiment', + rule_type: 'experiment', + variation_key: variation_to_return['key'] + } + allow(@project_typed_audience_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(@project_typed_audience_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -316,6 +337,14 @@ class InvalidErrorHandler; end params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '1630555627' variation_to_return = @project_config.get_variation_from_id('typed_audience_experiment', '1423767503') + + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'typed_audience_experiment', + rule_type: 'experiment', + variation_key: variation_to_return['key'] + } + allow(@project_typed_audience_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(@project_typed_audience_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -365,6 +394,14 @@ class InvalidErrorHandler; end params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '1323241598' variation_to_return = @project_config.get_variation_from_id('audience_combinations_experiment', '1423767504') + + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'audience_combinations_experiment', + rule_type: 'experiment', + variation_key: variation_to_return['key'] + } + allow(@project_typed_audience_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(@project_typed_audience_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -418,6 +455,13 @@ class InvalidErrorHandler; end }] params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '3' + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'test_experiment_with_audience', + rule_type: 'experiment', + variation_key: 'control_with_audience' + } + variation_to_return = project_config.get_variation_from_id('test_experiment_with_audience', '122228') allow(project_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -456,6 +500,13 @@ class InvalidErrorHandler; end }] params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '3' + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'test_experiment_with_audience', + rule_type: 'experiment', + variation_key: 'control_with_audience' + } + variation_to_return = project_config.get_variation_from_id('test_experiment_with_audience', '122228') allow(project_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -490,6 +541,14 @@ class InvalidErrorHandler; end project_instance.decision_service.set_forced_variation(project_config, 'test_experiment_with_audience', 'test_user', 'variation_with_audience') variation_to_return = project_instance.decision_service.get_forced_variation(project_config, 'test_experiment', 'test_user') + + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'test_experiment_with_audience', + rule_type: 'experiment', + variation_key: 'variation_with_audience' + } + allow(project_instance.decision_service.bucketer).to receive(:bucket).and_return(variation_to_return) allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) @@ -655,6 +714,13 @@ def callback(_args); end }] params[:visitors][0][:snapshots][0][:events][0][:entity_id] = '3' + params[:visitors][0][:snapshots][0][:decisions][0][:metadata] = { + flag_key: '', + rule_key: 'test_experiment_with_audience', + rule_type: 'experiment', + variation_key: 'variation_with_audience' + } + allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) allow(Optimizely::Audience).to receive(:user_in_experiment?) @@ -1396,14 +1462,16 @@ def callback(_args); end ) end - it 'should return false when the user is not bucketed into any variation' do + it 'should return false and send an impression when the user is not bucketed into any variation' do + allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(nil) expect(project_instance.is_feature_enabled('multi_variate_feature', 'test_user')).to be(false) expect(spy_logger).to have_received(:log).once.with(Logger::INFO, "Feature 'multi_variate_feature' is not enabled for user 'test_user'.") + expect(project_instance.event_dispatcher).to have_received(:dispatch_event).with(instance_of(Optimizely::Event)).once end - it 'should return true but not send an impression if the user is not bucketed into a feature experiment' do + it 'should return true and send an impression if the user is not bucketed into a feature experiment' do experiment_to_return = config_body['rollouts'][0]['experiments'][0] variation_to_return = experiment_to_return['variations'][0] @@ -1412,11 +1480,13 @@ def callback(_args); end variation_to_return, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] ) + + allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return) expect(project_instance.is_feature_enabled('boolean_single_variable_feature', 'test_user')).to be true - expect(spy_logger).to have_received(:log).once.with(Logger::DEBUG, "The user 'test_user' is not being experimented on in feature 'boolean_single_variable_feature'.") expect(spy_logger).to have_received(:log).once.with(Logger::INFO, "Feature 'boolean_single_variable_feature' is enabled for user 'test_user'.") + expect(project_instance.event_dispatcher).to have_received(:dispatch_event).with(instance_of(Optimizely::Event)).once end it 'should return false, if the user is bucketed into a feature rollout but the featureEnabled property is false' do @@ -1427,11 +1497,13 @@ def callback(_args); end variation_to_return, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] ) + allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return) expect(variation_to_return['featureEnabled']).to be false expect(project_instance.is_feature_enabled('boolean_single_variable_feature', 'test_user')).to be false expect(spy_logger).to have_received(:log).once.with(Logger::INFO, "Feature 'boolean_single_variable_feature' is not enabled for user 'test_user'.") + expect(project_instance.event_dispatcher).to have_received(:dispatch_event).with(instance_of(Optimizely::Event)).once end it 'should return true, if the user is bucketed into a feature rollout when featureEnabled property is true' do @@ -1442,17 +1514,19 @@ def callback(_args); end variation_to_return, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] ) + allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return) expect(variation_to_return['featureEnabled']).to be true expect(project_instance.is_feature_enabled('boolean_single_variable_feature', 'test_user')).to be true - expect(spy_logger).to have_received(:log).once.with(Logger::DEBUG, "The user 'test_user' is not being experimented on in feature 'boolean_single_variable_feature'.") expect(spy_logger).to have_received(:log).once.with(Logger::INFO, "Feature 'boolean_single_variable_feature' is enabled for user 'test_user'.") + expect(project_instance.event_dispatcher).to have_received(:dispatch_event).with(instance_of(Optimizely::Event)).once end describe '.typed audiences' do before(:example) do @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler) + stub_request(:post, impression_log_url) end it 'should return true for feature rollout when typed audience matched' do @@ -1468,7 +1542,6 @@ def callback(_args); end 'lasers' => -3 )).to be true - expect(spy_logger).to have_received(:log).twice.with(Logger::DEBUG, "The user 'test_user' is not being experimented on in feature 'feat'.") expect(spy_logger).to have_received(:log).twice.with(Logger::INFO, "Feature 'feat' is enabled for user 'test_user'.") end @@ -1487,7 +1560,6 @@ def callback(_args); end 'feat2', 'test_user', user_attributes )).to be true - expect(spy_logger).to have_received(:log).once.with(Logger::DEBUG, "The user 'test_user' is not being experimented on in feature 'feat2'.") expect(spy_logger).to have_received(:log).once.with(Logger::INFO, "Feature 'feat2' is enabled for user 'test_user'.") end @@ -1558,6 +1630,10 @@ def callback(_args); end end describe '.decision listener' do + before(:example) do + stub_request(:post, impression_log_url) + end + it 'should call decision listener when user is bucketed into a feature experiment with featureEnabled property is true' do allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) experiment_to_return = config_body['experiments'][3] @@ -1633,6 +1709,8 @@ def callback(_args); end # DECISION listener called when the user is in rollout with variation feature true. expect(variation_to_return['featureEnabled']).to be true + + expect(project_instance.notification_center).to receive(:send_notifications).ordered expect(project_instance.notification_center).to receive(:send_notifications).once.with( Optimizely::NotificationCenter::NOTIFICATION_TYPES[:DECISION], 'feature', 'test_user', {'browser_type' => 'firefox'}, @@ -1663,8 +1741,9 @@ def callback(_args); end it 'call decision listener when the user is not bucketed into any experiment or rollout' do allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(nil) + expect(project_instance.notification_center).to receive(:send_notifications).ordered - expect(project_instance.notification_center).to receive(:send_notifications).once.with( + expect(project_instance.notification_center).to receive(:send_notifications).with( Optimizely::NotificationCenter::NOTIFICATION_TYPES[:DECISION], 'feature', 'test_user', {'browser_type' => 'firefox'}, feature_enabled: false, @@ -1790,15 +1869,15 @@ def callback(_args); end ), nil, nil, - Optimizely::DecisionService::Decision, + nil, nil ) - expect(project_instance.notification_center).to receive(:send_notifications).twice.with( + expect(project_instance.notification_center).to receive(:send_notifications).exactly(10).times.with( Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args ) - expect(project_instance.notification_center).to receive(:send_notifications).twice.with( + expect(project_instance.notification_center).to receive(:send_notifications).exactly(10).times.with( Optimizely::NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE], any_args ) diff --git a/spec/spec_params.rb b/spec/spec_params.rb index a24c7da1..d28da0bc 100644 --- a/spec/spec_params.rb +++ b/spec/spec_params.rb @@ -25,6 +25,7 @@ module OptimizelySpec 'botFiltering' => true, 'revision' => '42', 'version' => '2', + 'sendFlagDecisions' => true, 'events' => [{ 'key' => 'test_event', 'experimentIds' => %w[111127 122230], @@ -1123,7 +1124,8 @@ module OptimizelySpec 'experimentIds' => %w[1323241598 1323241599] } ], - 'revision' => '3' + 'revision' => '3', + 'sendFlagDecisions' => true }.freeze VALID_CONFIG_BODY_JSON = JSON.dump(VALID_CONFIG_BODY) @@ -1131,4 +1133,7 @@ module OptimizelySpec INVALID_CONFIG_BODY = VALID_CONFIG_BODY.dup INVALID_CONFIG_BODY['version'] = '5' INVALID_CONFIG_BODY_JSON = JSON.dump(INVALID_CONFIG_BODY) + + # SEND_FLAG_DECISIONS_DISABLED_CONFIG = VALID_CONFIG_BODY.dup + # SEND_FLAG_DECISIONS_DISABLED_CONFIG['sendFlagDecisions'] = false end