diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 7299129e..336cd151 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -25,7 +25,7 @@ from .decision_service import Decision from .error_handler import NoOpErrorHandler as noop_error_handler from .event import event_factory, user_event_factory -from .event.event_processor import ForwardingEventProcessor +from .event.event_processor import BatchEventProcessor from .event_dispatcher import EventDispatcher as default_event_dispatcher from .helpers import enums, validator from .helpers.enums import DecisionSources @@ -50,7 +50,8 @@ def __init__( notification_center=None, event_processor=None, datafile_access_token=None, - default_decide_options=None + default_decide_options=None, + event_processor_options=None ): """ Optimizely init method for managing Custom projects. @@ -78,6 +79,7 @@ def __init__( optimizely.event.event_processor.BatchEventProcessor. datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment. default_decide_options: Optional list of decide options used with the decide APIs. + event_processor_options: Optional dict of options to be passed to the default batch event processor. """ self.logger_name = '.'.join([__name__, self.__class__.__name__]) self.is_valid = True @@ -86,8 +88,19 @@ def __init__( self.error_handler = error_handler or noop_error_handler self.config_manager = config_manager self.notification_center = notification_center or NotificationCenter(self.logger) - self.event_processor = event_processor or ForwardingEventProcessor( - self.event_dispatcher, logger=self.logger, notification_center=self.notification_center, + event_processor_defaults = { + 'batch_size': 1, + 'flush_interval': 30, + 'timeout_interval': 5, + 'start_on_init': True + } + if event_processor_options: + event_processor_defaults.update(event_processor_options) + self.event_processor = event_processor or BatchEventProcessor( + self.event_dispatcher, + logger=self.logger, + notification_center=self.notification_center, + **event_processor_defaults ) if default_decide_options is None: diff --git a/tests/test_event_processor.py b/tests/test_event_processor.py index 0656453c..4e45e6fc 100644 --- a/tests/test_event_processor.py +++ b/tests/test_event_processor.py @@ -116,7 +116,7 @@ class BatchEventProcessorTest(base.BaseTest): MAX_BATCH_SIZE = 10 MAX_DURATION_SEC = 0.2 MAX_TIMEOUT_INTERVAL_SEC = 0.1 - TEST_TIMEOUT = 10 + TEST_TIMEOUT = 15 def setUp(self, *args, **kwargs): base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index a2a4e036..380a5088 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -307,7 +307,7 @@ def test_activate(self): ) as mock_decision, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) @@ -446,7 +446,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=variation, - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) @@ -487,7 +487,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=variation, - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: self.assertEqual( @@ -559,7 +559,7 @@ def on_track(event_key, user_id, attributes, event_tags, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: self.optimizely.track('test_event', 'test_user') @@ -581,7 +581,7 @@ def on_track(event_key, user_id, attributes, event_tags, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) @@ -608,7 +608,7 @@ def on_track(event_key, user_id, attributes, event_tags, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: self.optimizely.track( @@ -680,7 +680,7 @@ def on_activate(experiment, user_id, attributes, variation, event): return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) @@ -701,7 +701,7 @@ def test_activate__with_attributes__audience_match(self): ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.assertEqual( 'variation', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'}), @@ -772,7 +772,7 @@ def test_activate__with_attributes_of_different_types(self): ) as mock_bucket, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: attributes = { 'test_attribute': 'test_value_1', @@ -849,7 +849,7 @@ def test_activate__with_attributes__typed_audience_match(self): variation when attributes are provided and typed audience conditions are met. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via exact match string audience with id '3468206642' self.assertEqual( 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'house': 'Gryffindor'}), @@ -865,7 +865,7 @@ def test_activate__with_attributes__typed_audience_match(self): mock_process.reset() - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via exact match number audience with id '3468206646' self.assertEqual( 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'lasers': 45.5}), @@ -884,7 +884,7 @@ def test_activate__with_attributes__typed_audience_with_semver_match(self): variation when attributes are provided and typed audience conditions are met. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via exact match string audience with id '18278344267' self.assertEqual( 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'android-release': '1.0.1'}), @@ -900,7 +900,7 @@ def test_activate__with_attributes__typed_audience_with_semver_match(self): mock_process.reset() - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.assertEqual( 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'android-release': "1.2.2"}), ) @@ -935,7 +935,7 @@ def test_activate__with_attributes__complex_audience_match(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via substring match string audience with id '3988293898', and # exact match number audience with id '3468206646' user_attr = {'house': 'Welcome to Slytherin!', 'lasers': 45.5} @@ -978,7 +978,7 @@ def test_activate__with_attributes__audience_match__forced_bucketing(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'control')) self.assertEqual( 'control', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'}), @@ -1044,7 +1044,7 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.assertEqual( 'variation', @@ -1233,7 +1233,7 @@ def test_track__with_attributes(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) expected_params = { @@ -1283,7 +1283,7 @@ def test_track__with_attributes__typed_audience_match(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via substring match string audience with id '3988293898' opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Slytherin!'}) @@ -1303,7 +1303,7 @@ def test_track__with_attributes__typed_audience_mismatch(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Hufflepuff!'}) self.assertEqual(1, mock_process.call_count) @@ -1314,7 +1314,7 @@ def test_track__with_attributes__complex_audience_match(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be included via exact match string audience with id '3468206642', and # exact match boolean audience with id '3468206643' user_attr = {'house': 'Gryffindor', 'should_do_it': True} @@ -1345,7 +1345,7 @@ def test_track__with_attributes__complex_audience_mismatch(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: # Should be excluded - exact match boolean audience with id '3468206643' does not match, # so the overall conditions fail user_attr = {'house': 'Gryffindor', 'should_do_it': False} @@ -1359,7 +1359,7 @@ def test_track__with_attributes__bucketing_id_provided(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', 'test_user', @@ -1417,7 +1417,7 @@ def test_track__with_attributes__no_audience_match(self): """ Test that track calls process even if audience conditions do not match. """ with mock.patch('time.time', return_value=42), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.optimizely.track( 'test_event', 'test_user', attributes={'test_attribute': 'wrong_test_value'}, @@ -1441,7 +1441,7 @@ def test_track__with_event_tags(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', 'test_user', @@ -1498,7 +1498,7 @@ def test_track__with_event_tags_revenue(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', 'test_user', @@ -1553,7 +1553,7 @@ def test_track__with_event_tags_numeric_metric(self): """ Test that track calls process with right params when only numeric metric event tags are provided. """ - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + with mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', 'test_user', @@ -1584,7 +1584,7 @@ def test_track__with_event_tags__forced_bucketing(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) self.optimizely.track( 'test_event', @@ -1642,7 +1642,7 @@ def test_track__with_invalid_event_tags(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', 'test_user', @@ -1698,7 +1698,7 @@ def test_track__experiment_not_running(self): with mock.patch( 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running, mock.patch('time.time', return_value=42), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: self.optimizely.track('test_event', 'test_user') @@ -1722,7 +1722,7 @@ def test_track__whitelisted_user_overrides_audience_check(self): with mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'user_1') self.assertEqual(1, mock_process.call_count) @@ -1984,7 +1984,7 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2084,7 +2084,7 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2184,7 +2184,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2234,7 +2234,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2336,7 +2336,7 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl return_value=(decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2378,7 +2378,7 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -2422,7 +2422,7 @@ def test_is_feature_enabled__returns_false_when_variation_is_nil(self, ): 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision, mock.patch( @@ -3335,7 +3335,11 @@ def test_get_all_feature_variables_for_feature_in_rollout(self): def test_get_feature_variable_for_feature_in_rollout(self): """ Test that get_feature_variable returns value as expected and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + opt_obj = optimizely.Optimizely( + json.dumps(self.config_dict_with_features), + # prevent event processor from injecting notification calls + event_processor_options={'start_on_init': False} + ) mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') user_attributes = {'test_attribute': 'test_value'}