diff --git a/optimizely/event/event_processor.py b/optimizely/event/event_processor.py index 823dd3f6..fa5683a8 100644 --- a/optimizely/event/event_processor.py +++ b/optimizely/event/event_processor.py @@ -117,11 +117,11 @@ def _validate_intantiation_props(self, prop, prop_name): prop_name: Property name. Returns: - False if property value is None or less than 1 or not a finite number. + False if property value is None or less than or equal to 0 or not a finite number. False if property name is batch_size and value is a floating point number. True otherwise. """ - if (prop_name == 'batch_size' and not isinstance(prop, int)) or prop is None or prop < 1 or \ + if (prop_name == 'batch_size' and not isinstance(prop, int)) or prop is None or prop <= 0 or \ not validator.is_finite_number(prop): self.logger.info('Using default value for {}.'.format(prop_name)) return False @@ -159,11 +159,11 @@ def _run(self): """ try: while True: - if self._get_time() > self.flushing_interval_deadline: + if self._get_time() >= self.flushing_interval_deadline: self._flush_queue() try: - item = self.event_queue.get(True, 0.05) + item = self.event_queue.get(False) except queue.Empty: time.sleep(0.05) @@ -283,3 +283,51 @@ def stop(self): if self.is_running: self.logger.error('Timeout exceeded while attempting to close for ' + str(self.timeout_interval) + ' ms.') + + +class ForwardingEventProcessor(BaseEventProcessor): + """ + ForwardingEventProcessor serves as the default EventProcessor. + + The ForwardingEventProcessor sends the LogEvent to EventDispatcher as soon as it is received. + """ + + def __init__(self, event_dispatcher, logger=None, notification_center=None): + """ ForwardingEventProcessor init method to configure event dispatching. + + Args: + event_dispatcher: Provides a dispatch_event method which if given a URL and params sends a request to it. + logger: Optional component which provides a log method to log messages. By default nothing would be logged. + notification_center: Optional instance of notification_center.NotificationCenter. + """ + self.event_dispatcher = event_dispatcher + self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) + self.notification_center = notification_center + + if not validator.is_notification_center_valid(self.notification_center): + self.logger.error(enums.Errors.INVALID_INPUT.format('notification_center')) + self.notification_center = _notification_center.NotificationCenter() + + def process(self, user_event): + """ Method to process the user_event by dispatching it. + Args: + user_event: UserEvent Instance. + """ + if not isinstance(user_event, UserEvent): + self.logger.error('Provided event is in an invalid format.') + return + + self.logger.debug('Received user_event: ' + str(user_event)) + + log_event = EventFactory.create_log_event(user_event, self.logger) + + if self.notification_center is not None: + self.notification_center.send_notifications( + enums.NotificationTypes.LOG_EVENT, + log_event + ) + + try: + self.event_dispatcher.dispatch_event(log_event) + except Exception as e: + self.logger.exception('Error dispatching event: ' + str(log_event) + ' ' + str(e)) diff --git a/optimizely/exceptions.py b/optimizely/exceptions.py index fe8c9124..1b027b1e 100644 --- a/optimizely/exceptions.py +++ b/optimizely/exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018, Optimizely +# Copyright 2016-2019, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -43,7 +43,7 @@ class InvalidGroupException(Exception): class InvalidInputException(Exception): - """ Raised when provided datafile, event dispatcher, logger or error handler is invalid. """ + """ Raised when provided datafile, event dispatcher, logger, event processor or error handler is invalid. """ pass diff --git a/optimizely/helpers/validator.py b/optimizely/helpers/validator.py index 4c38735b..441d868d 100644 --- a/optimizely/helpers/validator.py +++ b/optimizely/helpers/validator.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018, Optimizely +# Copyright 2016-2019, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -72,6 +72,19 @@ def is_config_manager_valid(config_manager): return _has_method(config_manager, 'get_config') +def is_event_processor_valid(event_processor): + """ Given an event_processor, determine if it is valid or not i.e. provides a process method. + + Args: + event_processor: Provides a process method to create user events and then send requests. + + Returns: + Boolean depending upon whether event_processor is valid or not. + """ + + return _has_method(event_processor, 'process') + + def is_error_handler_valid(error_handler): """ Given a error_handler determine if it is valid or not i.e. provides a handle_error method. diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 3e656994..fba5c5a6 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -17,12 +17,13 @@ from . import event_builder from . import exceptions from . import logger as _logging -from .config_manager import StaticConfigManager from .config_manager import PollingConfigManager +from .config_manager import StaticConfigManager 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_dispatcher import EventDispatcher as default_event_dispatcher -from .helpers import enums -from .helpers import validator +from .helpers import enums, validator from .notification_center import NotificationCenter @@ -38,7 +39,8 @@ def __init__(self, user_profile_service=None, sdk_key=None, config_manager=None, - notification_center=None): + notification_center=None, + event_processor=None): """ Optimizely init method for managing Custom projects. Args: @@ -56,6 +58,7 @@ def __init__(self, notification_center: Optional instance of notification_center.NotificationCenter. Useful when providing own config_manager.BaseConfigManager implementation which can be using the same NotificationCenter instance. + event_processor: Processes the given event(s) by creating LogEvent(s) and then dispatching it. """ self.logger_name = '.'.join([__name__, self.__class__.__name__]) self.is_valid = True @@ -64,6 +67,9 @@ def __init__(self, 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, + self.logger, + self.notification_center) try: self._validate_instantiation_options() @@ -114,6 +120,9 @@ def _validate_instantiation_options(self): if not validator.is_notification_center_valid(self.notification_center): raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('notification_center')) + if not validator.is_event_processor_valid(self.event_processor): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('event_processor')) + def _validate_user_inputs(self, attributes=None, event_tags=None): """ Helper method to validate user inputs. @@ -149,7 +158,7 @@ def _send_impression_event(self, project_config, experiment, variation, user_id, attributes: Dict representing user attributes and values which need to be recorded. """ - impression_event = self.event_builder.create_impression_event( + user_event = user_event_factory.UserEventFactory.create_impression_event( project_config, experiment, variation.id, @@ -157,18 +166,15 @@ def _send_impression_event(self, project_config, experiment, variation, user_id, attributes ) - self.logger.debug('Dispatching impression event to URL %s with params %s.' % ( - impression_event.url, - impression_event.params - )) - - try: - self.event_dispatcher.dispatch_event(impression_event) - except: - self.logger.exception('Unable to dispatch impression event!') + self.event_processor.process(user_event) - self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE, - experiment, user_id, attributes, variation, impression_event) + # Kept for backward compatibility. + # This notification is deprecated and new Decision notifications + # are sent via their respective method calls. + if len(self.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]) > 0: + log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) + self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE, experiment, + user_id, attributes, variation, log_event.__dict__) def _get_feature_variable_for_type(self, project_config, @@ -359,24 +365,21 @@ def track(self, event_key, user_id, attributes=None, event_tags=None): self.logger.info('Not tracking user "%s" for event "%s".' % (user_id, event_key)) return - conversion_event = self.event_builder.create_conversion_event( + user_event = user_event_factory.UserEventFactory.create_conversion_event( project_config, event_key, user_id, attributes, event_tags ) + + self.event_processor.process(user_event) self.logger.info('Tracking event "%s" for user "%s".' % (event_key, user_id)) - self.logger.debug('Dispatching conversion event to URL %s with params %s.' % ( - conversion_event.url, - conversion_event.params - )) - try: - self.event_dispatcher.dispatch_event(conversion_event) - except: - self.logger.exception('Unable to dispatch conversion event!') - self.notification_center.send_notifications(enums.NotificationTypes.TRACK, event_key, user_id, - attributes, event_tags, conversion_event) + + if len(self.notification_center.notification_listeners[enums.NotificationTypes.TRACK]) > 0: + log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) + self.notification_center.send_notifications(enums.NotificationTypes.TRACK, event_key, user_id, + attributes, event_tags, log_event.__dict__) def get_variation(self, experiment_key, user_id, attributes=None): """ Gets variation where user will be bucketed. diff --git a/tests/helpers_tests/test_validator.py b/tests/helpers_tests/test_validator.py index 302a32ce..8d390fdd 100644 --- a/tests/helpers_tests/test_validator.py +++ b/tests/helpers_tests/test_validator.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018, Optimizely +# Copyright 2016-2019, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -20,6 +20,7 @@ from optimizely import error_handler from optimizely import event_dispatcher from optimizely import logger +from optimizely.event import event_processor from optimizely.helpers import validator from tests import base @@ -42,6 +43,20 @@ def some_other_method(self): self.assertFalse(validator.is_config_manager_valid(CustomConfigManager())) + def test_is_event_processor_valid__returns_true(self): + """ Test that valid event_processor returns True. """ + + self.assertTrue(validator.is_event_processor_valid(event_processor.ForwardingEventProcessor)) + + def test_is_event_processor_valid__returns_false(self): + """ Test that invalid event_processor returns False. """ + + class CustomEventProcessor(object): + def some_other_method(self): + pass + + self.assertFalse(validator.is_event_processor_valid(CustomEventProcessor)) + def test_is_datafile_valid__returns_true(self): """ Test that valid datafile returns True. """ diff --git a/tests/test_event_processor.py b/tests/test_event_processor.py index 09a758b6..cbb3c98b 100644 --- a/tests/test_event_processor.py +++ b/tests/test_event_processor.py @@ -18,7 +18,8 @@ from . import base from optimizely.event.payload import Decision, Visitor -from optimizely.event.event_processor import BatchEventProcessor +from optimizely.event.event_processor import BatchEventProcessor, ForwardingEventProcessor +from optimizely.event.event_factory import EventFactory from optimizely.event.log_event import LogEvent from optimizely.event.user_event_factory import UserEventFactory from optimizely.helpers import enums @@ -401,3 +402,81 @@ def on_log_event(log_event): self.assertEqual(1, len(self.optimizely.notification_center.notification_listeners[ enums.NotificationTypes.LOG_EVENT ])) + + +class TestForwardingEventDispatcher(object): + + def __init__(self, is_updated=False): + self.is_updated = is_updated + + def dispatch_event(self, log_event): + if log_event.http_verb == 'POST' and log_event.url == EventFactory.EVENT_ENDPOINT: + self.is_updated = True + return self.is_updated + + +class ForwardingEventProcessorTest(base.BaseTest): + + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.test_user_id = 'test_user' + self.event_name = 'test_event' + self.optimizely.logger = SimpleLogger() + self.notification_center = self.optimizely.notification_center + self.event_dispatcher = TestForwardingEventDispatcher(is_updated=False) + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._event_processor = ForwardingEventProcessor(self.event_dispatcher, + mock_config_logging, + self.notification_center + ) + + def _build_conversion_event(self, event_name): + return UserEventFactory.create_conversion_event(self.project_config, + event_name, + self.test_user_id, + {}, + {} + ) + + def test_event_processor__dispatch_raises_exception(self): + """ Test that process logs dispatch failure gracefully. """ + + user_event = self._build_conversion_event(self.event_name) + log_event = EventFactory.create_log_event(user_event, self.optimizely.logger) + + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \ + mock.patch.object(self.event_dispatcher, 'dispatch_event', + side_effect=Exception('Failed to send.')): + + event_processor = ForwardingEventProcessor(self.event_dispatcher, mock_client_logging, self.notification_center) + event_processor.process(user_event) + + mock_client_logging.exception.assert_called_once_with( + 'Error dispatching event: ' + str(log_event) + ' Failed to send.' + ) + + def test_event_processor__with_test_event_dispatcher(self): + user_event = self._build_conversion_event(self.event_name) + self._event_processor.process(user_event) + self.assertStrictTrue(self.event_dispatcher.is_updated) + + def test_notification_center(self): + + callback_hit = [False] + + def on_log_event(log_event): + self.assertStrictTrue(isinstance(log_event, LogEvent)) + callback_hit[0] = True + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.LOG_EVENT, on_log_event + ) + + user_event = self._build_conversion_event(self.event_name) + self._event_processor.process(user_event) + + self.assertEqual(True, callback_hit[0]) + self.assertEqual(1, len(self.optimizely.notification_center.notification_listeners[ + enums.NotificationTypes.LOG_EVENT + ])) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 1a1f7689..64a76eb9 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -25,6 +25,7 @@ from optimizely import optimizely from optimizely import project_config from optimizely import version +from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums from . import base @@ -52,25 +53,29 @@ def isstr(self, s): def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): """ Helper method to validate properties of the event object. """ - self.assertEqual(expected_url, event_obj.url) + self.assertEqual(expected_url, event_obj.get('url')) + + event_params = event_obj.get('params') expected_params['visitors'][0]['attributes'] = \ sorted(expected_params['visitors'][0]['attributes'], key=itemgetter('key')) - event_obj.params['visitors'][0]['attributes'] = \ - sorted(event_obj.params['visitors'][0]['attributes'], key=itemgetter('key')) - self.assertEqual(expected_params, event_obj.params) - self.assertEqual(expected_verb, event_obj.http_verb) - self.assertEqual(expected_headers, event_obj.headers) + event_params['visitors'][0]['attributes'] = \ + sorted(event_params['visitors'][0]['attributes'], key=itemgetter('key')) + self.assertEqual(expected_params, event_params) + self.assertEqual(expected_verb, event_obj.get('http_verb')) + self.assertEqual(expected_headers, event_obj.get('headers')) def _validate_event_object_event_tags(self, event_obj, expected_event_metric_params, expected_event_features_params): """ Helper method to validate properties of the event object related to event tags. """ + event_params = event_obj.get('params') + # get event metrics from the created event object - event_metrics = event_obj.params['visitors'][0]['snapshots'][0]['events'][0]['tags'] + event_metrics = event_params['visitors'][0]['snapshots'][0]['events'][0]['tags'] self.assertEqual(expected_event_metric_params, event_metrics) # get event features from the created event object - event_features = event_obj.params['visitors'][0]['attributes'][0] + event_features = event_params['visitors'][0]['attributes'][0] self.assertEqual(expected_event_features_params, event_features) def test_init__invalid_datafile__logs_error(self): @@ -129,6 +134,19 @@ class InvalidDispatcher(object): mock_client_logger.exception.assert_called_once_with('Provided "event_dispatcher" is in an invalid format.') self.assertFalse(opt_obj.is_valid) + def test_init__invalid_event_processor__logs_error(self): + """ Test that invalid event_processor logs error on init. """ + + class InvalidProcessor(object): + pass + + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), event_processor=InvalidProcessor) + + mock_client_logger.exception.assert_called_once_with('Provided "event_processor" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) + def test_init__invalid_logger__logs_error(self): """ Test that invalid logger logs error on init. """ @@ -255,14 +273,14 @@ def test_invalid_json_raises_schema_validation_off(self): self.assertIsNone(opt_obj.config_manager.get_config()) def test_activate(self): - """ Test that activate calls dispatch_event with right params and returns expected variation. """ + """ Test that activate calls process with right params and returns expected variation. """ with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=self.project_config.get_variation_from_id('test_experiment', '111129')) 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) expected_params = { @@ -291,11 +309,16 @@ def test_activate(self): 'anonymize_ip': False, 'revision': '42' } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_decision.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None ) - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + self.assertEqual(1, mock_process.call_count) + + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_add_activate_remove_clear_listener(self): @@ -307,7 +330,7 @@ def on_activate(experiment, user_id, attributes, variation, event): if attributes is not None: self.assertTrue(isinstance(attributes, dict)) self.assertTrue(isinstance(variation, entities.Variation)) - self.assertTrue(isinstance(event, event_builder.Event)) + # self.assertTrue(isinstance(event, event_builder.Event)) print("Activated experiment {0}".format(experiment.key)) callbackhit[0] = True @@ -317,7 +340,7 @@ def on_activate(experiment, user_id, attributes, variation, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=self.project_config.get_variation_from_id('test_experiment', '111129')), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'): + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) self.assertEqual(True, callbackhit[0]) @@ -329,7 +352,7 @@ def on_activate(experiment, user_id, attributes, variation, event): len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE])) def test_add_track_remove_clear_listener(self): - """ Test adding a listener tract passes correctly and gets called""" + """ Test adding a listener track passes correctly and gets called""" callback_hit = [False] def on_track(event_key, user_id, attributes, event_tags, event): @@ -339,7 +362,9 @@ def on_track(event_key, user_id, attributes, event_tags, event): self.assertTrue(isinstance(attributes, dict)) if event_tags is not None: self.assertTrue(isinstance(event_tags, dict)) - self.assertTrue(isinstance(event, event_builder.Event)) + + # TODO: what should be done about passing dicts of class instances? + # self.assertTrue(isinstance(event, LogEvent)) print('Track event with event_key={0}'.format(event_key)) callback_hit[0] = True @@ -349,7 +374,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', '111129')), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'): + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.optimizely.track('test_event', 'test_user') self.assertEqual(True, callback_hit[0]) @@ -363,13 +388,21 @@ def on_track(event_key, user_id, attributes, event_tags, event): def test_activate_and_decision_listener(self): """ Test that activate calls broadcast activate and decision with proper parameters. """ + def on_activate(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, on_activate) + with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=self.project_config.get_variation_from_id('test_experiment', '111129')), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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')) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + self.assertEqual(mock_broadcast.call_count, 2) mock_broadcast.assert_has_calls([ @@ -388,21 +421,29 @@ def test_activate_and_decision_listener(self): self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None, self.project_config.get_variation_from_id('test_experiment', '111129'), - mock_dispatch.call_args[0][0] + log_event.__dict__ ) ]) def test_activate_and_decision_listener_with_attr(self): """ Test that activate calls broadcast activate and decision with proper parameters. """ + def on_activate(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, on_activate) + with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=self.project_config.get_variation_from_id('test_experiment', '111129')), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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', {'test_attribute': 'test_value'})) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + self.assertEqual(mock_broadcast.call_count, 2) mock_broadcast.assert_has_calls([ @@ -421,7 +462,7 @@ def test_activate_and_decision_listener_with_attr(self): self.project_config.get_experiment_from_key('test_experiment'), 'test_user', {'test_attribute': 'test_value'}, self.project_config.get_variation_from_id('test_experiment', '111129'), - mock_dispatch.call_args[0][0] + log_event.__dict__ ) ]) @@ -432,7 +473,7 @@ def test_decision_listener__user_not_in_experiment(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', return_value=None), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual(None, self.optimizely.activate('test_experiment', 'test_user')) @@ -450,52 +491,76 @@ def test_decision_listener__user_not_in_experiment(self): def test_track_listener(self): """ Test that track calls notification broadcaster. """ + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.TRACK, on_track) + 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_event_tracked: self.optimizely.track('test_event', 'test_user') + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", - 'test_user', None, None, mock_dispatch.call_args[0][0]) + 'test_user', None, None, log_event.__dict__) def test_track_listener_with_attr(self): """ Test that track calls notification broadcaster. """ + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.TRACK, on_track) + 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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'}) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", 'test_user', {'test_attribute': 'test_value'}, - None, mock_dispatch.call_args[0][0]) + None, log_event.__dict__) def test_track_listener_with_attr_with_event_tags(self): """ Test that track calls notification broadcaster. """ + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener( + enums.NotificationTypes.TRACK, on_track) + 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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'}, event_tags={'value': 1.234, 'non-revenue': 'abc'}) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", 'test_user', {'test_attribute': 'test_value'}, {'value': 1.234, 'non-revenue': 'abc'}, - mock_dispatch.call_args[0][0]) + log_event.__dict__) def test_is_feature_enabled__callback_listener(self): """ Test that the feature is enabled for the user if bucketed into variation of an experiment. - Also confirm that impression event is dispatched. """ + Also confirm that impression event is processed. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() @@ -517,9 +582,7 @@ def on_activate(experiment, user_id, attributes, variation, event): mock_variation, enums.DecisionSources.FEATURE_TEST )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) @@ -527,7 +590,7 @@ def on_activate(experiment, user_id, attributes, variation, event): def test_is_feature_enabled_rollout_callback_listener(self): """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is dispatched. """ + Also confirm that no impression event is processed. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() @@ -548,19 +611,17 @@ def on_activate(experiment, user_id, attributes, variation, event): mock_variation, enums.DecisionSources.ROLLOUT )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) mock_decision.assert_called_once_with(project_config, feature, 'test_user', None) # Check that impression event is not sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) self.assertEqual(False, access_callback[0]) def test_activate__with_attributes__audience_match(self): - """ Test that activate calls dispatch_event with right params and returns expected + """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met. """ with mock.patch( @@ -569,7 +630,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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'})) expected_params = { @@ -603,15 +664,19 @@ def test_activate__with_attributes__audience_match(self): 'anonymize_ip': False, 'revision': '42' } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_get_variation.assert_called_once_with(self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', {'test_attribute': 'test_value'}) - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_activate__with_attributes_of_different_types(self): - """ Test that activate calls dispatch_event with right params and returns expected + """ Test that activate calls process with right params and returns expected variation when different types of attributes are provided and audience conditions are met. """ with mock.patch( @@ -620,7 +685,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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: attributes = { 'test_attribute': 'test_value_1', @@ -678,19 +743,22 @@ def test_activate__with_attributes_of_different_types(self): 'revision': '42' } + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_bucket.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user' ) - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_activate__with_attributes__typed_audience_match(self): - """ Test that activate calls dispatch_event with right params and returns expected + """ Test that activate calls process with right params and returns expected 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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'})) @@ -702,12 +770,12 @@ def test_activate__with_attributes__typed_audience_match(self): } self.assertTrue( - expected_attr in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) - mock_dispatch_event.reset() + mock_process.reset() - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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})) @@ -719,25 +787,25 @@ def test_activate__with_attributes__typed_audience_match(self): } self.assertTrue( - expected_attr in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) def test_activate__with_attributes__typed_audience_mismatch(self): """ Test that activate returns None when typed audience conditions do not match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertIsNone(opt_obj.activate('typed_audience_experiment', 'test_user', {'house': 'Hufflepuff'})) - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_activate__with_attributes__complex_audience_match(self): - """ Test that activate calls dispatch_event with right params and returns expected + """ Test that activate calls process with right params and returns expected variation when attributes are provided and complex audience conditions are met. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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} @@ -758,32 +826,32 @@ def test_activate__with_attributes__complex_audience_match(self): } self.assertTrue( - expected_attr_1 in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) self.assertTrue( - expected_attr_2 in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) def test_activate__with_attributes__complex_audience_mismatch(self): """ Test that activate returns None when complex audience conditions do not match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: user_attr = {'house': 'Hufflepuff', 'lasers': 45.5} self.assertIsNone(opt_obj.activate('audience_combinations_experiment', 'test_user', user_attr)) - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_activate__with_attributes__audience_match__forced_bucketing(self): - """ Test that activate calls dispatch_event with right params and returns expected + """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met after a set_forced_variation is called. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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'})) @@ -820,12 +888,15 @@ def test_activate__with_attributes__audience_match__forced_bucketing(self): 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_activate__with_attributes__audience_match__bucketing_id_provided(self): - """ Test that activate calls dispatch_event with right params and returns expected variation + """ Test that activate calls process with right params and returns expected variation when attributes (including bucketing ID) are provided and audience conditions are met. """ with mock.patch( @@ -834,7 +905,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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'})) @@ -874,12 +945,16 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): 'anonymize_ip': False, 'revision': '42' } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + mock_get_variation.assert_called_once_with(self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', {'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'}) - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_activate__with_attributes__no_audience_match(self): @@ -894,30 +969,30 @@ def test_activate__with_attributes__no_audience_match(self): self.optimizely.logger) def test_activate__with_attributes__invalid_attributes(self): - """ Test that activate returns None and does not bucket or dispatch event when attributes are invalid. """ + """ Test that activate returns None and does not bucket or process event when attributes are invalid. """ with mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes='invalid')) self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_activate__experiment_not_running(self): - """ Test that activate returns None and does not dispatch event when experiment is not Running. """ + """ Test that activate returns None and does not process event when experiment is not Running. """ with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=False) as mock_is_experiment_running, \ mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'})) mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) self.assertEqual(0, mock_audience_check.call_count) self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_activate__whitelisting_overrides_audience_check(self): """ Test that during activate whitelist overrides audience check if user is in the whitelist. """ @@ -930,18 +1005,18 @@ def test_activate__whitelisting_overrides_audience_check(self): self.assertEqual(0, mock_audience_check.call_count) def test_activate__bucketer_returns_none(self): - """ Test that activate returns None and does not dispatch event when user is in no variation. """ + """ Test that activate returns None and does not process event when user is in no variation. """ with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True), \ mock.patch('optimizely.bucketer.Bucketer.bucket', return_value=None) as mock_bucket, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'})) mock_bucket.assert_called_once_with(self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user') - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_activate__invalid_object(self): """ Test that activate logs error if Optimizely instance is invalid. """ @@ -968,11 +1043,11 @@ def test_activate__invalid_config(self): 'Failing "activate".') def test_track__with_attributes(self): - """ Test that track calls dispatch_event with right params when attributes are provided. """ + """ Test that track calls process with right params when attributes are provided. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) expected_params = { @@ -1001,21 +1076,25 @@ def test_track__with_attributes(self): 'anonymize_ip': False, 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__with_attributes__typed_audience_match(self): - """ Test that track calls dispatch_event with right params when attributes are provided + """ Test that track calls process with right params when attributes are provided and it's a typed audience match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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!'}) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) expected_attr = { 'type': 'custom', @@ -1025,32 +1104,32 @@ def test_track__with_attributes__typed_audience_match(self): } self.assertTrue( - expected_attr in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) def test_track__with_attributes__typed_audience_mismatch(self): - """ Test that track calls dispatch_event even if audience conditions do not match. """ + """ Test that track calls process even if audience conditions do not match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Hufflepuff!'}) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) def test_track__with_attributes__complex_audience_match(self): - """ Test that track calls dispatch_event with right params when attributes are provided + """ Test that track calls process with right params when attributes are provided and it's a complex audience match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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} opt_obj.track('user_signed_up', 'test_user', user_attr) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) expected_attr_1 = { 'type': 'custom', @@ -1060,7 +1139,7 @@ def test_track__with_attributes__complex_audience_match(self): } self.assertTrue( - expected_attr_1 in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) expected_attr_2 = { @@ -1071,29 +1150,29 @@ def test_track__with_attributes__complex_audience_match(self): } self.assertTrue( - expected_attr_2 in mock_dispatch_event.call_args[0][0].params['visitors'][0]['attributes'] + expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] ) def test_track__with_attributes__complex_audience_mismatch(self): - """ Test that track calls dispatch_event even when complex audience conditions do not match. """ + """ Test that track calls process even when complex audience conditions do not match. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.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} opt_obj.track('user_signed_up', 'test_user', user_attr) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) def test_track__with_attributes__bucketing_id_provided(self): - """ Test that track calls dispatch_event with right params when + """ Test that track calls process with right params when attributes (including bucketing ID) are provided. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'}) @@ -1128,35 +1207,39 @@ def test_track__with_attributes__bucketing_id_provided(self): 'anonymize_ip': False, 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__with_attributes__no_audience_match(self): - """ Test that track calls dispatch_event even if audience conditions do not match. """ + """ Test that track calls process even if audience conditions do not match. """ with mock.patch('time.time', return_value=42), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'wrong_test_value'}) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) def test_track__with_attributes__invalid_attributes(self): - """ Test that track does not bucket or dispatch event if attributes are invalid. """ + """ Test that track does not bucket or process event if attributes are invalid. """ with mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes='invalid') self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_track__with_event_tags(self): - """ Test that track calls dispatch_event with right params when event tags are provided. """ + """ Test that track calls process with right params when event tags are provided. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) @@ -1193,17 +1276,20 @@ def test_track__with_event_tags(self): 'anonymize_ip': False, 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__with_event_tags_revenue(self): - """ Test that track calls dispatch_event with right params when only revenue + """ Test that track calls process with right params when only revenue event tags are provided only. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, event_tags={'revenue': 4200, 'non-revenue': 'abc'}) @@ -1238,15 +1324,19 @@ def test_track__with_event_tags_revenue(self): 'anonymize_ip': False, 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__with_event_tags_numeric_metric(self): - """ Test that track calls dispatch_event with right params when only numeric metric + """ Test that track calls process with right params when only numeric metric event tags are provided. """ - with mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, event_tags={'value': 1.234, 'non-revenue': 'abc'}) @@ -1261,18 +1351,22 @@ def test_track__with_event_tags_numeric_metric(self): 'value': 'test_value', 'key': 'test_attribute' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object_event_tags(mock_dispatch_event.call_args[0][0], + + self.assertEqual(1, mock_process.call_count) + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self._validate_event_object_event_tags(log_event.__dict__, expected_event_metrics_params, expected_event_features_params) def test_track__with_event_tags__forced_bucketing(self): - """ Test that track calls dispatch_event with right params when event_value information is provided + """ Test that track calls process with right params when event_value information is provided after a forced bucket. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) @@ -1311,17 +1405,19 @@ def test_track__with_event_tags__forced_bucketing(self): 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__with_invalid_event_tags(self): - """ Test that track calls dispatch_event with right params when invalid event tags are provided. """ + """ Test that track calls process with right params when invalid event tags are provided. """ with mock.patch('time.time', return_value=42), \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, event_tags={'revenue': '4200', 'value': True}) @@ -1355,31 +1451,35 @@ def test_track__with_invalid_event_tags(self): 'anonymize_ip': False, 'revision': '42' } - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], 'https://logx.optimizely.com/v1/events', + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, + 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_track__experiment_not_running(self): - """ Test that track calls dispatch_event even if experiment is not running. """ + """ Test that track calls process even if experiment is not running. """ 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user') # Assert that experiment is running is not performed self.assertEqual(0, mock_is_experiment_running.call_count) - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) def test_track_invalid_event_key(self): - """ Test that track does not call dispatch_event when event does not exist. """ - dispatch_event_patch = mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') - with dispatch_event_patch as mock_dispatch_event, \ + """ Test that track does not call process when event does not exist. """ + + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process,\ mock.patch.object(self.optimizely, 'logger') as mock_client_logging: self.optimizely.track('aabbcc_event', 'test_user') - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) mock_client_logging.info.assert_called_with( 'Not tracking user "test_user" for event "aabbcc_event".' ) @@ -1389,10 +1489,10 @@ 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_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'user_1') - self.assertEqual(1, mock_dispatch_event.call_count) + self.assertEqual(1, mock_process.call_count) def test_track__invalid_object(self): """ Test that track logs error if Optimizely instance is invalid. """ @@ -1618,17 +1718,17 @@ def test_is_feature_enabled__returns_false_for_invalid_feature(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature') as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertFalse(opt_obj.is_feature_enabled('invalid_feature', 'user1')) self.assertFalse(mock_decision.called) # Check that no event is sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enabled_for_variation(self): """ Test that the feature is enabled for the user if bucketed into variation of an experiment and - the variation's featureEnabled property is True. Also confirm that impression event is dispatched and + the variation's featureEnabled property is True. Also confirm that impression event is processed and decision listener is called with proper parameters """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1647,7 +1747,7 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab mock_variation, enums.DecisionSources.FEATURE_TEST )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ mock.patch('time.time', return_value=42): @@ -1701,15 +1801,18 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab 'anonymize_ip': False, 'revision': '1' } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + # Check that impression event is sent - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_disabled_for_variation(self): """ Test that the feature is disabled for the user if bucketed into variation of an experiment and - the variation's featureEnabled property is False. Also confirm that impression event is dispatched and + the variation's featureEnabled property is False. Also confirm that impression event is processed and decision is broadcasted with proper parameters """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1728,7 +1831,7 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis mock_variation, enums.DecisionSources.FEATURE_TEST )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ mock.patch('time.time', return_value=42): @@ -1783,15 +1886,17 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis 'anonymize_ip': False, 'revision': '1' } + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + # Check that impression event is sent - self.assertEqual(1, mock_dispatch_event.call_count) - self._validate_event_object(mock_dispatch_event.call_args[0][0], + self.assertEqual(1, mock_process.call_count) + self._validate_event_object(log_event.__dict__, 'https://logx.optimizely.com/v1/events', expected_params, 'POST', {'Content-Type': 'application/json'}) def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled(self): """ Test that the feature is enabled for the user if bucketed into variation of a rollout and - the variation's featureEnabled property is True. Also confirm that no impression event is dispatched and + the variation's featureEnabled property is True. Also confirm that no impression event is processed and decision is broadcasted with proper parameters """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1810,7 +1915,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled mock_variation, enums.DecisionSources.ROLLOUT )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ mock.patch('time.time', return_value=42): @@ -1832,11 +1937,11 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled ) # Check that impression event is not sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabled(self): """ Test that the feature is disabled for the user if bucketed into variation of a rollout and - the variation's featureEnabled property is False. Also confirm that no impression event is dispatched and + the variation's featureEnabled property is False. Also confirm that no impression event is processed and decision is broadcasted with proper parameters """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1855,7 +1960,7 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl mock_variation, enums.DecisionSources.ROLLOUT )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ mock.patch('time.time', return_value=42): @@ -1877,12 +1982,12 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl ) # Check that impression event is not sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_variation(self): """ Test that the feature is not enabled for the user if user is neither bucketed for Feature Experiment nor for Feature Rollout. - Also confirm that impression event is not dispatched. """ + Also confirm that impression event is not processed. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() @@ -1893,14 +1998,14 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va None, enums.DecisionSources.ROLLOUT )) as mock_decision, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event, \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ mock.patch('time.time', return_value=42): self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) # Check that impression event is not sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) @@ -1918,7 +2023,7 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va ) # Check that impression event is not sent - self.assertEqual(0, mock_dispatch_event.call_count) + self.assertEqual(0, mock_process.call_count) def test_is_feature_enabled__invalid_object(self): """ Test that is_feature_enabled returns False and logs error if Optimizely instance is invalid. """ @@ -3656,18 +3761,13 @@ def test_activate(self): return_value=self.project_config.get_variation_from_id( 'test_experiment', '111129')), \ mock.patch('time.time', return_value=42), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ mock.patch.object(self.optimizely, 'logger') as mock_client_logging: self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) mock_client_logging.info.assert_called_once_with( 'Activating user "test_user" in experiment "test_experiment".' ) - debug_message = mock_client_logging.debug.call_args_list[0][0][0] - self.assertRegexpMatches( - debug_message, - 'Dispatching impression event to URL https://logx.optimizely.com/v1/events with params' - ) def test_track(self): """ Test that expected log messages are logged during track. """ @@ -3676,20 +3776,14 @@ def test_track(self): event_key = 'test_event' mock_client_logger = mock.patch.object(self.optimizely, 'logger') - mock_conversion_event = event_builder.Event('logx.optimizely.com', {'event_key': event_key}) - with mock.patch('optimizely.event_builder.EventBuilder.create_conversion_event', - return_value=mock_conversion_event), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \ + event_builder.Event('logx.optimizely.com', {'event_key': event_key}) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ mock_client_logger as mock_client_logging: self.optimizely.track(event_key, user_id) mock_client_logging.info.assert_has_calls([ mock.call('Tracking event "%s" for user "%s".' % (event_key, user_id)), ]) - mock_client_logging.debug.assert_has_calls([ - mock.call('Dispatching conversion event to URL %s with params %s.' % ( - mock_conversion_event.url, mock_conversion_event.params)), - ]) def test_activate__experiment_not_running(self): """ Test that expected log messages are logged during activate when experiment is not running. """ @@ -3728,16 +3822,6 @@ def test_activate__no_audience_match(self): ) mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') - def test_activate__dispatch_raises_exception(self): - """ Test that activate logs dispatch failure gracefully. """ - - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event', - side_effect=Exception('Failed to send')): - self.assertEqual('control', self.optimizely.activate('test_experiment', 'user_1')) - - mock_client_logging.exception.assert_called_once_with('Unable to dispatch impression event!') - def test_track__invalid_attributes(self): """ Test that expected log messages are logged during track when attributes are in invalid format. """ @@ -3763,15 +3847,6 @@ def test_track__invalid_event_tag(self): 'Provided event tags are in an invalid format.' ) - def test_track__dispatch_raises_exception(self): - """ Test that track logs dispatch failure gracefully. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event', - side_effect=Exception('Failed to send')): - self.optimizely.track('test_event', 'user_1') - - mock_client_logging.exception.assert_called_once_with('Unable to dispatch conversion event!') - def test_get_variation__invalid_attributes(self): """ Test that expected log messages are logged during get variation when attributes are in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: @@ -3830,18 +3905,13 @@ def test_activate__empty_user_id(self): return_value=self.project_config.get_variation_from_id( 'test_experiment', '111129')), \ mock.patch('time.time', return_value=42), \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \ + mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ mock.patch.object(self.optimizely, 'logger') as mock_client_logging: self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) mock_client_logging.info.assert_called_once_with( 'Activating user "" in experiment "test_experiment".' ) - debug_message = mock_client_logging.debug.call_args_list[0][0][0] - self.assertRegexpMatches( - debug_message, - 'Dispatching impression event to URL https://logx.optimizely.com/v1/events with params' - ) def test_activate__invalid_attributes(self): """ Test that expected log messages are logged during activate when attributes are in invalid format. """