From a5accf0ee909b74fc6f7e60f6592e863ee66b44b Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 20 Jan 2021 18:12:57 +0500 Subject: [PATCH 01/20] WIP --- optimizely/decision/decision.py | 11 ++++++ optimizely/helpers/enums.py | 1 + optimizely/optimizely.py | 32 ++++++++++------ optimizely/user_context.py | 12 ++++-- tests/test_user_context.py | 67 +++++++++++++++++++++++++++------ 5 files changed, 98 insertions(+), 25 deletions(-) diff --git a/optimizely/decision/decision.py b/optimizely/decision/decision.py index 19ecb7b01..74d3bb08b 100644 --- a/optimizely/decision/decision.py +++ b/optimizely/decision/decision.py @@ -22,3 +22,14 @@ def __init__(self, variation_key=None, enabled=None, self.flag_key = flag_key self.user_context = user_context self.reasons = reasons or [] + + def as_json(self): + return { + 'variation_key': self.variation_key, + 'enabled': self.enabled, + 'variables': self.variables, + 'rule_key': self.rule_key, + 'flag_key': self.flag_key, + 'user_context': self.user_context.as_json(), + 'reasons': self.reasons + } diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 5685f9c87..10bc2d59f 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -86,6 +86,7 @@ class DecisionNotificationTypes(object): FEATURE_TEST = 'feature-test' FEATURE_VARIABLE = 'feature-variable' ALL_FEATURE_VARIABLES = 'all-feature-variables' + FLAG = 'flag' class DecisionSources(object): diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 6193fc2b7..531d2b500 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -979,7 +979,7 @@ def decide(self, user_context, key, decide_options=None): if not isinstance(key, string_types): self.logger.error('Key parameter is invalid') reasons.append(DecisionMessage.FLAG_KEY_INVALID.format(key)) - return Decision.new(flag_key=key, user_context=user_context, reasons=reasons) + return Decision(flag_key=key, user_context=user_context, reasons=reasons) # validate that key maps to a feature flag config = self.config_manager.get_config() @@ -1041,12 +1041,14 @@ def get_reasons(self): experiment = None decision_source = DecisionSources.ROLLOUT source_info = {} + decision_event_dispatched = False decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_context.user_id, user_context.user_attributes, DecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options) + # Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.) if decision.experiment is not None: experiment = decision.experiment @@ -1058,14 +1060,16 @@ def get_reasons(self): feature_enabled = variation.featureEnabled decision_source = decision.source source_info["variation"] = variation - + # # Send impression event if Decision came from a feature # test and decide options doesn't include disableDecisionEvent if DecideOption.DISABLE_DECISION_EVENT not in decide_options: if decision_source == DecisionSources.FEATURE_TEST or config.send_flag_decisions: self._send_impression_event(config, experiment, variation, flag_key, rule_key or '', - feature_enabled, decision_source, + decision_source, feature_enabled, user_id, attributes) + decision_event_dispatched = True + # Generate all variables map if decide options doesn't include excludeVariables if DecideOption.EXCLUDE_VARIABLES not in decide_options: @@ -1078,19 +1082,27 @@ def get_reasons(self): decide_options ) - # Send notification self.notification_center.send_notifications( enums.NotificationTypes.DECISION, - enums.DecisionNotificationTypes.FEATURE, + enums.DecisionNotificationTypes.FLAG, user_id, attributes or {}, { - 'feature_key': key, - 'feature_enabled': feature_enabled, - 'source': decision.source, - 'source_info': source_info, + # 'feature_key': key, + # 'feature_enabled': feature_enabled, + # 'source': decision.source, + # 'source_info': source_info, + 'flag_key' : flag_key, + 'enabled' : feature_enabled, + 'variables': all_variables , + 'variation_key' : variation_key, + 'rule_key' : rule_key, + 'reasons' : reasons, + 'decision_event_dispatched': decision_event_dispatched + }, ) + # Send notification include_reasons = [] if DecideOption.INCLUDE_REASONS in decide_options: @@ -1133,7 +1145,6 @@ def decide_all(self, user_context, decide_options=None): keys = [] for f in config.feature_flags: keys.append(f['key']) - return self.decide_for_keys(user_context, keys, decide_options) def decide_for_keys(self, user_context, keys, decide_options=None): @@ -1166,5 +1177,4 @@ def decide_for_keys(self, user_context, keys, decide_options=None): if enabled_flags_only and not decision.enabled: continue decisions[key] = decision - return decisions diff --git a/optimizely/user_context.py b/optimizely/user_context.py index 98b40a277..b4bf87455 100644 --- a/optimizely/user_context.py +++ b/optimizely/user_context.py @@ -82,7 +82,7 @@ def decide_for_keys(self, keys, options=None): self.logger.error("Optimizely Client invalid") return None - self.client.decide_for_keys(self, keys, options) + return self.client.decide_for_keys(self, keys, options) def decide_all(self, options=None): """ @@ -97,7 +97,13 @@ def decide_all(self, options=None): self.logger.error("Optimizely Client invalid") return None - self.client.decide_all(self, options) + return self.client.decide_all(self, options) def track_event(self, event_key, event_tags=None): - self.client.track(event_key, self.user_id, self.user_attributes, event_tags) + return self.client.track(event_key, self.user_id, self.user_attributes, event_tags) + + def as_json(self): + return { + 'user_id': self.user_id, + 'attributes': self.user_attributes, + } diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 4312b1cb6..b9b13a76c 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -15,13 +15,13 @@ import mock +from optimizely import logger, optimizely, decision_service from optimizely.decision.decide_option import DecideOption from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums +from optimizely.user_context import UserContext from optimizely.user_profile import UserProfileService, UserProfile from . import base -from optimizely import logger, optimizely, decision_service -from optimizely.user_context import UserContext class UserContextTests(base.BaseTest): @@ -47,9 +47,11 @@ def test_decide_feature_test(self): mock_experiment = project_config.get_experiment_from_key('test_experiment') mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), ): user_context = opt_obj.create_user_context('test_user') decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) @@ -122,7 +124,7 @@ def test_track(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: user_context = opt_obj.create_user_context('test_user') user_context.track_event('test_event') @@ -145,8 +147,8 @@ def test_decide_sendEvent(self): self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -190,8 +192,8 @@ def test_decide_doNotSendEvent_withOption(self): self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -250,7 +252,7 @@ def save(self, user_profile): self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', + 'optimizely.bucketer.Bucketer.bucket', return_value=mock_variation, ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' @@ -314,7 +316,7 @@ def save(self, user_profile): self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', + 'optimizely.bucketer.Bucketer.bucket', return_value=mock_variation, ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' @@ -356,3 +358,46 @@ def save(self, user_profile): # Check that impression event is NOT sent for rollout and send_flag_decisions = True # with disable decision event decision option self.assertEqual(0, mock_process.call_count) + + "Add more test cases for user_context scenario" + + def test_optimizely_user_context_created_with_expected_values(self): + user_id = 'test_user' + attributes = {"browser": "chrome"} + uc = UserContext(self.optimizely, user_id, attributes) + self.assertEquals("test_user", uc.user_id) + self.assertEqual(attributes, uc.user_attributes) + self.assertIs(self.optimizely, uc.client) + + def test_set_attributes(self): + user_id = 'test_user' + attributes = {"browser": "chrome"} + uc = UserContext(self.optimizely, user_id, attributes) + self.assertEqual(attributes, uc.user_attributes) + uc.set_attribute('color', 'red') + self.assertEquals({ + "browser": "chrome", + "color": "red"}, uc.user_attributes) + + def test_set_attributes_overrides_value_of_existing_key(self): + user_id = 'test_user' + attributes = {"browser": "chrome"} + uc = UserContext(self.optimizely, user_id, attributes) + self.assertEquals(attributes, uc.user_attributes) + uc.set_attribute('browser', 'firefox') + self.assertEquals({"browser": "firefox"}, uc.user_attributes) + + def test_set_attribute_when_no_attributes_provided_in_constructor(self): + user_id = 'test_user' + uc = UserContext(self.optimizely, user_id) + self.assertEqual({}, uc.user_attributes) + uc.set_attribute('browser', 'firefox') + self.assertEqual({'browser': 'firefox'}, uc.user_attributes) + + def test_attribute_when_no_update_on_caller_copy_update(self): + user_id = 'test_user' + attributes = {"browser": "chrome"} + uc = UserContext(self.optimizely, user_id, attributes) + self.assertEqual(attributes, uc.user_attributes) + attributes['new_key'] = 'test_value' + self.assertNotEqual(attributes, uc.user_attributes) From c3d4e338b20eb1b840e74b768cf505e4f8b3d8cd Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 22 Jan 2021 17:53:25 +0500 Subject: [PATCH 02/20] WIP --- optimizely/decision/decide_option.py | 2 +- optimizely/decision/decision.py | 2 +- optimizely/optimizely.py | 34 ++++++++++++++++------------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/optimizely/decision/decide_option.py b/optimizely/decision/decide_option.py index adc1eb26a..051dde457 100644 --- a/optimizely/decision/decide_option.py +++ b/optimizely/decision/decide_option.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2021, 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 diff --git a/optimizely/decision/decision.py b/optimizely/decision/decision.py index 74d3bb08b..33d53f526 100644 --- a/optimizely/decision/decision.py +++ b/optimizely/decision/decision.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2021, 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 diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 531d2b500..a95500ed3 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1060,7 +1060,7 @@ def get_reasons(self): feature_enabled = variation.featureEnabled decision_source = decision.source source_info["variation"] = variation - # + # Send impression event if Decision came from a feature # test and decide options doesn't include disableDecisionEvent if DecideOption.DISABLE_DECISION_EVENT not in decide_options: @@ -1073,14 +1073,23 @@ def get_reasons(self): # Generate all variables map if decide options doesn't include excludeVariables if DecideOption.EXCLUDE_VARIABLES not in decide_options: - project_config = self.config_manager.get_config() - for v_key in feature_flag.variables: - v = feature_flag.variables[v_key] - all_variables[v.key] = self._get_feature_variable_for_type(project_config, feature_flag.key, - v.key, v.type, user_id, attributes, - DecideOption.IGNORE_USER_PROFILE_SERVICE in - decide_options - ) + for variable_key in feature_flag.variables: + variable = config.get_variable_for_feature(flag_key, variable_key) + variable_value = variable.defaultValue + if feature_enabled: + variable_value = config.get_variable_value_for_variation(variable, decision.variation) + self.logger.debug( + 'Got variable value "%s" for variable "%s" of feature flag "%s".' + % (variable_value, variable_key, flag_key) + ) + + try: + actual_value = config.get_typecast_value(variable_value, variable.type) + except: + self.logger.error('Unable to cast value. Returning None.') + actual_value = None + + all_variables[variable_key] = actual_value self.notification_center.send_notifications( enums.NotificationTypes.DECISION, @@ -1088,10 +1097,6 @@ def get_reasons(self): user_id, attributes or {}, { - # 'feature_key': key, - # 'feature_enabled': feature_enabled, - # 'source': decision.source, - # 'source_info': source_info, 'flag_key' : flag_key, 'enabled' : feature_enabled, 'variables': all_variables , @@ -1102,8 +1107,8 @@ def get_reasons(self): }, ) - # Send notification + # Send notification include_reasons = [] if DecideOption.INCLUDE_REASONS in decide_options: handler.flush() @@ -1116,6 +1121,7 @@ def get_reasons(self): rule_key=rule_key, flag_key=flag_key, user_context=user_context, reasons=include_reasons) + def decide_all(self, user_context, decide_options=None): """ decide_all will return a decision for every feature key in the current config From ac856c777831d8cab4522123e1143993e791997c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 22 Jan 2021 18:33:08 +0500 Subject: [PATCH 03/20] fix: Passes All FSC --- .travis.yml | 3 - optimizely/decision/__init__.py | 2 +- optimizely/decision/decision_message.py | 2 +- optimizely/decision_service.py | 2 +- optimizely/helpers/enums.py | 4 +- optimizely/optimizely.py | 75 +++++++------------------ optimizely/user_context.py | 20 +------ tests/test_optimizely.py | 2 +- tests/test_user_context.py | 2 +- 9 files changed, 29 insertions(+), 83 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1d420664..ce7e0e51d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,6 @@ python: # - "3.8" is handled in 'Test' job using xenial as Python 3.8 is not available for trusty. - "pypy" - "pypy3" -before_install: - - sudo apt-get --auto-remove --yes remove python-openssl - - sudo pip install pyOpenSSL install: "pip install -r requirements/core.txt;pip install -r requirements/test.txt" script: "pytest --cov=optimizely" after_success: diff --git a/optimizely/decision/__init__.py b/optimizely/decision/__init__.py index 8f0a0bcef..016c35cd9 100644 --- a/optimizely/decision/__init__.py +++ b/optimizely/decision/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2021, 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 diff --git a/optimizely/decision/decision_message.py b/optimizely/decision/decision_message.py index ea3c48d39..a7e1db91a 100644 --- a/optimizely/decision/decision_message.py +++ b/optimizely/decision/decision_message.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2021, 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 diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 1b767e11a..5d9c8e5bd 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -1,4 +1,4 @@ -# Copyright 2017-2020, Optimizely +# Copyright 2017-2021, 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 diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 10bc2d59f..8339eee68 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 @@ -82,10 +82,10 @@ class DatafileVersions(object): class DecisionNotificationTypes(object): AB_TEST = 'ab-test' + ALL_FEATURE_VARIABLES = 'all-feature-variables' FEATURE = 'feature' FEATURE_TEST = 'feature-test' FEATURE_VARIABLE = 'feature-variable' - ALL_FEATURE_VARIABLES = 'all-feature-variables' FLAG = 'flag' diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index a95500ed3..a2d449b7e 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -10,8 +10,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging -import threading from six import string_types @@ -53,7 +51,7 @@ def __init__( notification_center=None, event_processor=None, datafile_access_token=None, - default_decisions=None + default_decide_options=None ): """ Optimizely init method for managing Custom projects. @@ -77,7 +75,7 @@ def __init__( which simply forwards events to the event dispatcher. To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor. datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment. - default_decisions: Optional list of decide options used with the decide APIs. + default_decide_options: Optional list of decide options used with the decide APIs. """ self.logger_name = '.'.join([__name__, self.__class__.__name__]) self.is_valid = True @@ -89,7 +87,13 @@ def __init__( self.event_processor = event_processor or ForwardingEventProcessor( self.event_dispatcher, logger=self.logger, notification_center=self.notification_center, ) - self.default_decisions = default_decisions or [] + + if default_decide_options is None: + self.default_decide_options = [] + + if not isinstance(self.default_decide_options, list): + self.logger.debug('Provided default decide options is not a list.') + self.default_decide_options = [] try: self._validate_instantiation_options() @@ -203,8 +207,7 @@ def _send_impression_event(self, project_config, experiment, variation, flag_key ) def _get_feature_variable_for_type( - self, project_config, feature_key, variable_key, variable_type, user_id, attributes, - ignore_user_profile=False + self, project_config, feature_key, variable_key, variable_type, user_id, attributes ): """ Helper method to determine value for a certain variable attached to a feature flag based on type of variable. @@ -215,7 +218,6 @@ def _get_feature_variable_for_type( variable_type: Type of variable which could be one of boolean/double/integer/string. user_id: ID for user. attributes: Dict representing user attributes. - ignore_user_profile: if true don't use the user profile service Returns: Value of the variable. None if: @@ -259,7 +261,7 @@ def _get_feature_variable_for_type( source_info = {} variable_value = variable.defaultValue decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, - attributes, ignore_user_profile) + attributes) if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -310,7 +312,7 @@ def _get_feature_variable_for_type( return actual_value def _get_all_feature_variables_for_type( - self, project_config, feature_key, user_id, attributes, + self, project_config, feature_key, user_id, attributes, ): """ Helper method to determine value for all variables attached to a feature flag. @@ -948,8 +950,8 @@ def create_user_context(self, user_id, attributes=None): self.logger.error(enums.Errors.INVALID_INPUT.format('attributes')) return None - user_context = UserContext(self, user_id, attributes) - return user_context + return UserContext(self, user_id, attributes) + def decide(self, user_context, key, decide_options=None): """ @@ -971,7 +973,7 @@ def decide(self, user_context, key, decide_options=None): # check if SDK is ready if not self.is_valid: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide')) + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('decide')) reasons.append(DecisionMessage.SDK_NOT_READY) return Decision(flag_key=key, user_context=user_context, reasons=reasons) @@ -996,38 +998,11 @@ def decide(self, user_context, key, decide_options=None): # merge decide_options and default_decide_options if isinstance(decide_options, list): - decide_options += self.default_decisions + decide_options += self.default_decide_options else: self.logger.debug('Provided decide options is not an array. Using default decide options.') - decide_options = self.default_decisions - - class ReasonLogHandler(logging.StreamHandler): - def __init__(self): - super(ReasonLogHandler, self).__init__() - self._name = "ReasonLogHandler" - self.reasons = {threading.current_thread().ident: []} - # setting to info level since we don't put debug in reasons. - self.level = logging.INFO - formatter = logging.Formatter('%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s') - self.setFormatter(formatter) - self.createLock() - - def handle(self, record): - msg = self.format(record) - self.reasons[threading.current_thread().ident].append(msg) + decide_options = self.default_decide_options - def emit(self, record): - pass - - def get_reasons(self): - return self.reasons[threading.current_thread().ident] - - handler = None - - if DecideOption.INCLUDE_REASONS in decide_options: - handler = ReasonLogHandler() - self.decision_service.logger.addHandler(handler) - config.logger.addHandler(handler) # Create Optimizely Decision Result. user_id = user_context.user_id @@ -1091,6 +1066,7 @@ def get_reasons(self): all_variables[variable_key] = actual_value + # Send notification self.notification_center.send_notifications( enums.NotificationTypes.DECISION, enums.DecisionNotificationTypes.FLAG, @@ -1108,18 +1084,9 @@ def get_reasons(self): }, ) - # Send notification - include_reasons = [] - if DecideOption.INCLUDE_REASONS in decide_options: - handler.flush() - include_reasons = reasons - include_reasons += handler.get_reasons() - self.decision_service.logger.removeHandler(handler) - config.logger.removeHandler(handler) - return Decision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables, rule_key=rule_key, - flag_key=flag_key, user_context=user_context, reasons=include_reasons) + flag_key=flag_key, user_context=user_context, reasons=reasons) def decide_all(self, user_context, decide_options=None): @@ -1138,7 +1105,7 @@ def decide_all(self, user_context, decide_options=None): # check if SDK is ready if not self.is_valid: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide_all')) + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('decide_all')) return {} config = self.config_manager.get_config() @@ -1170,7 +1137,7 @@ def decide_for_keys(self, user_context, keys, decide_options=None): # check if SDK is ready if not self.is_valid: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide_for_keys')) + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('decide_for_keys')) return {} enabled_flags_only = False diff --git a/optimizely/user_context.py b/optimizely/user_context.py index b4bf87455..29829bc3e 100644 --- a/optimizely/user_context.py +++ b/optimizely/user_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely and contributors +# Copyright 2021, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from . import logger as _logging - class UserContext(object): """ @@ -36,10 +34,6 @@ def __init__(self, optimizely_client, user_id, user_attributes=None): self.user_id = user_id self.user_attributes = user_attributes.copy() if user_attributes else {} - self.logger_name = '.'.join([__name__, self.__class__.__name__]) - - self.logger = _logging.reset_logger(self.logger_name) - def set_attribute(self, attribute_key, attribute_value): """ sets a attribute by key for this user context. @@ -62,10 +56,6 @@ def decide(self, key, options=None): Returns: Decision object """ - if not self.client: - self.logger.error("Optimizely Client invalid") - return None - return self.client.decide(self, key, options) def decide_for_keys(self, keys, options=None): @@ -78,10 +68,6 @@ def decide_for_keys(self, keys, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - if not self.client: - self.logger.error("Optimizely Client invalid") - return None - return self.client.decide_for_keys(self, keys, options) def decide_all(self, options=None): @@ -93,10 +79,6 @@ def decide_all(self, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - if not self.client: - self.logger.error("Optimizely Client invalid") - return None - return self.client.decide_all(self, options) def track_event(self, event_key, event_tags=None): diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index e88c308c7..fc3d8c74a 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 diff --git a/tests/test_user_context.py b/tests/test_user_context.py index b9b13a76c..d3ed3002a 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2021, 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 From aa483cc022b1b97d4c68d5057d23f9ab0e9f336d Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 22 Jan 2021 18:41:00 +0500 Subject: [PATCH 04/20] fix: unit tests and cleanup --- optimizely/optimizely.py | 17 +++----- optimizely/user_context.py | 1 + tests/test_decision_service.py | 3 ++ tests/test_user_context.py | 76 ++++++++++++++++------------------ 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index a2d449b7e..971ba4b3e 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -952,7 +952,6 @@ def create_user_context(self, user_id, attributes=None): return UserContext(self, user_id, attributes) - def decide(self, user_context, key, decide_options=None): """ decide calls optimizely decide with feature key provided @@ -1003,7 +1002,6 @@ def decide(self, user_context, key, decide_options=None): self.logger.debug('Provided decide options is not an array. Using default decide options.') decide_options = self.default_decide_options - # Create Optimizely Decision Result. user_id = user_context.user_id attributes = user_context.user_attributes @@ -1023,7 +1021,6 @@ def decide(self, user_context, key, decide_options=None): DecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options) - # Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.) if decision.experiment is not None: experiment = decision.experiment @@ -1045,7 +1042,6 @@ def decide(self, user_context, key, decide_options=None): user_id, attributes) decision_event_dispatched = True - # Generate all variables map if decide options doesn't include excludeVariables if DecideOption.EXCLUDE_VARIABLES not in decide_options: for variable_key in feature_flag.variables: @@ -1073,12 +1069,12 @@ def decide(self, user_context, key, decide_options=None): user_id, attributes or {}, { - 'flag_key' : flag_key, - 'enabled' : feature_enabled, - 'variables': all_variables , - 'variation_key' : variation_key, - 'rule_key' : rule_key, - 'reasons' : reasons, + 'flag_key': flag_key, + 'enabled': feature_enabled, + 'variables': all_variables, + 'variation_key': variation_key, + 'rule_key': rule_key, + 'reasons': reasons, 'decision_event_dispatched': decision_event_dispatched }, @@ -1088,7 +1084,6 @@ def decide(self, user_context, key, decide_options=None): rule_key=rule_key, flag_key=flag_key, user_context=user_context, reasons=reasons) - def decide_all(self, user_context, decide_options=None): """ decide_all will return a decision for every feature key in the current config diff --git a/optimizely/user_context.py b/optimizely/user_context.py index 29829bc3e..0ee4e249e 100644 --- a/optimizely/user_context.py +++ b/optimizely/user_context.py @@ -13,6 +13,7 @@ # limitations under the License. # + class UserContext(object): """ Representation of an Optimizely User Context using which APIs are to be called. diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 6875a1c06..91240e326 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1183,6 +1183,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( self.project_config.get_experiment_from_key("test_experiment"), "test_user", None, + False ) def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(self): @@ -1302,6 +1303,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) self.project_config.get_experiment_from_key("group_exp_1"), "test_user", None, + False ) def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): @@ -1349,6 +1351,7 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self self.project_config.get_experiment_from_key("test_experiment"), "test_user", None, + False ) def test_get_variation_for_feature__returns_none_for_invalid_group_id(self): diff --git a/tests/test_user_context.py b/tests/test_user_context.py index d3ed3002a..960baa1b8 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -12,7 +12,6 @@ # limitations under the License. import json import logging - import mock from optimizely import logger, optimizely, decision_service @@ -164,20 +163,19 @@ def test_decide_sendEvent(self): mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, - 'feature', + 'flag', 'test_user', {}, { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'rollout', - 'source_info': { - 'experiment': mock_experiment, - 'variation': mock_variation, - }, + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': True, + 'variables': decision.variables, }, ) - # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) @@ -209,17 +207,18 @@ def test_decide_doNotSendEvent_withOption(self): mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, - 'feature', + 'flag', 'test_user', {}, { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'rollout', - 'source_info': { - 'experiment': mock_experiment, - 'variation': mock_variation, - }, + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + }, ) @@ -245,7 +244,6 @@ def save(self, user_profile): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) project_config = opt_obj.config_manager.get_config() - mock_experiment = project_config.get_experiment_from_key('test_experiment') mock_variation = project_config.get_variation_from_id('test_experiment', '111129') # Assert that featureEnabled property is True @@ -271,17 +269,18 @@ def save(self, user_profile): mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, - 'feature', + 'flag', 'test_user', {}, { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'source_info': { - 'experiment': mock_experiment, - 'variation': mock_variation, - }, + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + }, ) @@ -309,7 +308,6 @@ def save(self, user_profile): user_profile_service=ups) project_config = opt_obj.config_manager.get_config() - mock_experiment = project_config.get_experiment_from_key('test_experiment') mock_variation = project_config.get_variation_from_id('test_experiment', '111129') # Assert that featureEnabled property is True @@ -336,25 +334,21 @@ def save(self, user_profile): mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, - 'feature', + 'flag', 'test_user', {}, { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'source_info': { - 'experiment': mock_experiment, - 'variation': mock_variation, - }, + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + }, ) - self.assertIsNotNone(decision.reasons) - self.assertTrue(decision.reasons[0].find( - 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.') is not -1) - self.assertTrue(decision.reasons[1].find( - 'User "test_user" is in variation "variation" of experiment test_experiment.') is not -1) # Check that impression event is NOT sent for rollout and send_flag_decisions = True # with disable decision event decision option self.assertEqual(0, mock_process.call_count) From 30da8fc68912a4c951c16027e124cf48f13f4689 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 25 Jan 2021 18:22:35 +0500 Subject: [PATCH 05/20] refact: rename decide classes --- ..._option.py => optimizely_decide_option.py} | 2 +- .../{decision.py => optimizely_decision.py} | 2 +- ...sage.py => optimizely_decision_message.py} | 2 +- optimizely/optimizely.py | 53 ++++++++++--------- ..._context.py => optimizely_user_context.py} | 2 +- tests/test_user_context.py | 14 ++--- 6 files changed, 38 insertions(+), 37 deletions(-) rename optimizely/decision/{decide_option.py => optimizely_decide_option.py} (95%) rename optimizely/decision/{decision.py => optimizely_decision.py} (97%) rename optimizely/decision/{decision_message.py => optimizely_decision_message.py} (95%) rename optimizely/{user_context.py => optimizely_user_context.py} (98%) diff --git a/optimizely/decision/decide_option.py b/optimizely/decision/optimizely_decide_option.py similarity index 95% rename from optimizely/decision/decide_option.py rename to optimizely/decision/optimizely_decide_option.py index 051dde457..4eb8e7e55 100644 --- a/optimizely/decision/decide_option.py +++ b/optimizely/decision/optimizely_decide_option.py @@ -12,7 +12,7 @@ # limitations under the License. -class DecideOption(object): +class OptimizelyDecideOption(object): DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT' ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY' IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE' diff --git a/optimizely/decision/decision.py b/optimizely/decision/optimizely_decision.py similarity index 97% rename from optimizely/decision/decision.py rename to optimizely/decision/optimizely_decision.py index 33d53f526..781ab2bba 100644 --- a/optimizely/decision/decision.py +++ b/optimizely/decision/optimizely_decision.py @@ -12,7 +12,7 @@ # limitations under the License. -class Decision(object): +class OptimizelyDecision(object): def __init__(self, variation_key=None, enabled=None, variables=None, rule_key=None, flag_key=None, user_context=None, reasons=None): self.variation_key = variation_key diff --git a/optimizely/decision/decision_message.py b/optimizely/decision/optimizely_decision_message.py similarity index 95% rename from optimizely/decision/decision_message.py rename to optimizely/decision/optimizely_decision_message.py index a7e1db91a..f3875a7cd 100644 --- a/optimizely/decision/decision_message.py +++ b/optimizely/decision/optimizely_decision_message.py @@ -12,7 +12,7 @@ # limitations under the License. -class DecisionMessage(object): +class OptimizelyDecisionMessage(object): SDK_NOT_READY = 'Optimizely SDK not configured properly yet.' FLAG_KEY_INVALID = 'No flag was found for key "%s".' VARIABLE_VALUE_INVALID = 'Variable value for key "%s" is invalid or wrong type.' diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 971ba4b3e..f719e7cb3 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -21,9 +21,9 @@ from .config_manager import AuthDatafilePollingConfigManager from .config_manager import PollingConfigManager from .config_manager import StaticConfigManager -from .decision.decide_option import DecideOption -from .decision.decision import Decision -from .decision.decision_message import DecisionMessage +from .decision.optimizely_decide_option import OptimizelyDecideOption +from .decision.optimizely_decision import OptimizelyDecision +from .decision.optimizely_decision_message import OptimizelyDecisionMessage from .error_handler import NoOpErrorHandler as noop_error_handler from .event import event_factory, user_event_factory from .event.event_processor import ForwardingEventProcessor @@ -32,7 +32,7 @@ from .helpers.enums import DecisionSources from .notification_center import NotificationCenter from .optimizely_config import OptimizelyConfigService -from .user_context import UserContext +from .optimizely_user_context import OptimizelyUserContext class Optimizely(object): @@ -950,7 +950,7 @@ def create_user_context(self, user_id, attributes=None): self.logger.error(enums.Errors.INVALID_INPUT.format('attributes')) return None - return UserContext(self, user_id, attributes) + return OptimizelyUserContext(self, user_id, attributes) def decide(self, user_context, key, decide_options=None): """ @@ -958,14 +958,14 @@ def decide(self, user_context, key, decide_options=None): Args: user_context: UserContent with userid and attributes key: feature key - decide_options: list of DecideOption + decide_options: list of OptimizelyDecideOption Returns: Decision object """ # raising on user context as it is internal and not provided directly by the user. - if not isinstance(user_context, UserContext): + if not isinstance(user_context, OptimizelyUserContext): raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('user_context')) reasons = [] @@ -973,27 +973,27 @@ def decide(self, user_context, key, decide_options=None): # check if SDK is ready if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('decide')) - reasons.append(DecisionMessage.SDK_NOT_READY) - return Decision(flag_key=key, user_context=user_context, reasons=reasons) + reasons.append(OptimizelyDecisionMessage.SDK_NOT_READY) + return OptimizelyDecision(flag_key=key, user_context=user_context, reasons=reasons) # validate that key is a string if not isinstance(key, string_types): self.logger.error('Key parameter is invalid') - reasons.append(DecisionMessage.FLAG_KEY_INVALID.format(key)) - return Decision(flag_key=key, user_context=user_context, reasons=reasons) + reasons.append(OptimizelyDecisionMessage.FLAG_KEY_INVALID.format(key)) + return OptimizelyDecision(flag_key=key, user_context=user_context, reasons=reasons) # validate that key maps to a feature flag config = self.config_manager.get_config() if not config: self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide')) - reasons.append(DecisionMessage.SDK_NOT_READY) - return Decision(flag_key=key, user_context=user_context, reasons=reasons) + reasons.append(OptimizelyDecisionMessage.SDK_NOT_READY) + return OptimizelyDecision(flag_key=key, user_context=user_context, reasons=reasons) feature_flag = config.get_feature_from_key(key) if feature_flag is None: self.logger.error("No feature flag was found for key '#{key}'.") - reasons.append(DecisionMessage.FLAG_KEY_INVALID.format(key)) - return Decision(flag_key=key, user_context=user_context, reasons=reasons) + reasons.append(OptimizelyDecisionMessage.FLAG_KEY_INVALID.format(key)) + return OptimizelyDecision(flag_key=key, user_context=user_context, reasons=reasons) # merge decide_options and default_decide_options if isinstance(decide_options, list): @@ -1018,7 +1018,7 @@ def decide(self, user_context, key, decide_options=None): decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_context.user_id, user_context.user_attributes, - DecideOption.IGNORE_USER_PROFILE_SERVICE in + OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options) # Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.) @@ -1035,7 +1035,7 @@ def decide(self, user_context, key, decide_options=None): # Send impression event if Decision came from a feature # test and decide options doesn't include disableDecisionEvent - if DecideOption.DISABLE_DECISION_EVENT not in decide_options: + if OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options: if decision_source == DecisionSources.FEATURE_TEST or config.send_flag_decisions: self._send_impression_event(config, experiment, variation, flag_key, rule_key or '', decision_source, feature_enabled, @@ -1043,7 +1043,7 @@ def decide(self, user_context, key, decide_options=None): decision_event_dispatched = True # Generate all variables map if decide options doesn't include excludeVariables - if DecideOption.EXCLUDE_VARIABLES not in decide_options: + if OptimizelyDecideOption.EXCLUDE_VARIABLES not in decide_options: for variable_key in feature_flag.variables: variable = config.get_variable_for_feature(flag_key, variable_key) variable_value = variable.defaultValue @@ -1080,9 +1080,10 @@ def decide(self, user_context, key, decide_options=None): }, ) - return Decision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables, - rule_key=rule_key, - flag_key=flag_key, user_context=user_context, reasons=reasons) + return OptimizelyDecision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables, + rule_key=rule_key, flag_key=flag_key, + user_context=user_context, reasons=reasons + ) def decide_all(self, user_context, decide_options=None): """ @@ -1095,7 +1096,7 @@ def decide_all(self, user_context, decide_options=None): A dictionary of feature key to Decision """ # raising on user context as it is internal and not provided directly by the user. - if not isinstance(user_context, UserContext): + if not isinstance(user_context, OptimizelyUserContext): raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('user_context')) # check if SDK is ready @@ -1107,8 +1108,8 @@ def decide_all(self, user_context, decide_options=None): reasons = [] if not config: self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide')) - reasons.append(DecisionMessage.SDK_NOT_READY) - return Decision(user_context=user_context, reasons=reasons) + reasons.append(OptimizelyDecisionMessage.SDK_NOT_READY) + return OptimizelyDecision(user_context=user_context, reasons=reasons) keys = [] for f in config.feature_flags: @@ -1127,7 +1128,7 @@ def decide_for_keys(self, user_context, keys, decide_options=None): An dictionary of feature key to Decision """ # raising on user context as it is internal and not provided directly by the user. - if not isinstance(user_context, UserContext): + if not isinstance(user_context, OptimizelyUserContext): raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('user_context')) # check if SDK is ready @@ -1137,7 +1138,7 @@ def decide_for_keys(self, user_context, keys, decide_options=None): enabled_flags_only = False if decide_options is not None: - enabled_flags_only = DecideOption.ENABLED_FLAGS_ONLY in decide_options + enabled_flags_only = OptimizelyDecideOption.ENABLED_FLAGS_ONLY in decide_options decisions = {} for key in keys: diff --git a/optimizely/user_context.py b/optimizely/optimizely_user_context.py similarity index 98% rename from optimizely/user_context.py rename to optimizely/optimizely_user_context.py index 0ee4e249e..1ba43217c 100644 --- a/optimizely/user_context.py +++ b/optimizely/optimizely_user_context.py @@ -14,7 +14,7 @@ # -class UserContext(object): +class OptimizelyUserContext(object): """ Representation of an Optimizely User Context using which APIs are to be called. """ diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 960baa1b8..b7ae6a67c 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -18,7 +18,7 @@ from optimizely.decision.decide_option import DecideOption from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums -from optimizely.user_context import UserContext +from optimizely.optimizely_user_context import OptimizelyUserContext from optimizely.user_profile import UserProfileService, UserProfile from . import base @@ -32,7 +32,7 @@ def test_user_context(self): """ tests user context creating and attributes """ - uc = UserContext(self.optimizely, "test_user") + uc = OptimizelyUserContext(self.optimizely, "test_user") self.assertEqual(uc.user_attributes, {}, "should have created default empty") self.assertEqual(uc.user_id, "test_user", "should have same user id") uc.set_attribute("key", "value") @@ -358,7 +358,7 @@ def save(self, user_profile): def test_optimizely_user_context_created_with_expected_values(self): user_id = 'test_user' attributes = {"browser": "chrome"} - uc = UserContext(self.optimizely, user_id, attributes) + uc = OptimizelyUserContext(self.optimizely, user_id, attributes) self.assertEquals("test_user", uc.user_id) self.assertEqual(attributes, uc.user_attributes) self.assertIs(self.optimizely, uc.client) @@ -366,7 +366,7 @@ def test_optimizely_user_context_created_with_expected_values(self): def test_set_attributes(self): user_id = 'test_user' attributes = {"browser": "chrome"} - uc = UserContext(self.optimizely, user_id, attributes) + uc = OptimizelyUserContext(self.optimizely, user_id, attributes) self.assertEqual(attributes, uc.user_attributes) uc.set_attribute('color', 'red') self.assertEquals({ @@ -376,14 +376,14 @@ def test_set_attributes(self): def test_set_attributes_overrides_value_of_existing_key(self): user_id = 'test_user' attributes = {"browser": "chrome"} - uc = UserContext(self.optimizely, user_id, attributes) + uc = OptimizelyUserContext(self.optimizely, user_id, attributes) self.assertEquals(attributes, uc.user_attributes) uc.set_attribute('browser', 'firefox') self.assertEquals({"browser": "firefox"}, uc.user_attributes) def test_set_attribute_when_no_attributes_provided_in_constructor(self): user_id = 'test_user' - uc = UserContext(self.optimizely, user_id) + uc = OptimizelyUserContext(self.optimizely, user_id) self.assertEqual({}, uc.user_attributes) uc.set_attribute('browser', 'firefox') self.assertEqual({'browser': 'firefox'}, uc.user_attributes) @@ -391,7 +391,7 @@ def test_set_attribute_when_no_attributes_provided_in_constructor(self): def test_attribute_when_no_update_on_caller_copy_update(self): user_id = 'test_user' attributes = {"browser": "chrome"} - uc = UserContext(self.optimizely, user_id, attributes) + uc = OptimizelyUserContext(self.optimizely, user_id, attributes) self.assertEqual(attributes, uc.user_attributes) attributes['new_key'] = 'test_value' self.assertNotEqual(attributes, uc.user_attributes) From cc8dfda78b02f9bc0f99c24362dc8a2dc45ae4d6 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 25 Jan 2021 18:32:08 +0500 Subject: [PATCH 06/20] fix: merge default decide options in decide for keys --- optimizely/optimizely.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index f719e7cb3..320f86f48 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1136,6 +1136,13 @@ def decide_for_keys(self, user_context, keys, decide_options=None): self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('decide_for_keys')) return {} + # merge decide_options and default_decide_options + if isinstance(decide_options, list): + decide_options += self.default_decide_options + else: + self.logger.debug('Provided decide options is not an array. Using default decide options.') + decide_options = self.default_decide_options + enabled_flags_only = False if decide_options is not None: enabled_flags_only = OptimizelyDecideOption.ENABLED_FLAGS_ONLY in decide_options From 98e99ffce605e8e164d801ad2fd4191e0b9a2cb0 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 25 Jan 2021 18:40:07 +0500 Subject: [PATCH 07/20] prefix decide methods with _ --- optimizely/optimizely.py | 10 +++++----- optimizely/optimizely_user_context.py | 6 +++--- tests/test_optimizely.py | 2 +- tests/test_user_context.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 320f86f48..d353c7a32 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -952,7 +952,7 @@ def create_user_context(self, user_id, attributes=None): return OptimizelyUserContext(self, user_id, attributes) - def decide(self, user_context, key, decide_options=None): + def _decide(self, user_context, key, decide_options=None): """ decide calls optimizely decide with feature key provided Args: @@ -1085,7 +1085,7 @@ def decide(self, user_context, key, decide_options=None): user_context=user_context, reasons=reasons ) - def decide_all(self, user_context, decide_options=None): + def _decide_all(self, user_context, decide_options=None): """ decide_all will return a decision for every feature key in the current config Args: @@ -1114,9 +1114,9 @@ def decide_all(self, user_context, decide_options=None): keys = [] for f in config.feature_flags: keys.append(f['key']) - return self.decide_for_keys(user_context, keys, decide_options) + return self._decide_for_keys(user_context, keys, decide_options) - def decide_for_keys(self, user_context, keys, decide_options=None): + def _decide_for_keys(self, user_context, keys, decide_options=None): """ Args: @@ -1149,7 +1149,7 @@ def decide_for_keys(self, user_context, keys, decide_options=None): decisions = {} for key in keys: - decision = self.decide(user_context, key, decide_options) + decision = self._decide(user_context, key, decide_options) if enabled_flags_only and not decision.enabled: continue decisions[key] = decision diff --git a/optimizely/optimizely_user_context.py b/optimizely/optimizely_user_context.py index 1ba43217c..0bb8f1fb4 100644 --- a/optimizely/optimizely_user_context.py +++ b/optimizely/optimizely_user_context.py @@ -57,7 +57,7 @@ def decide(self, key, options=None): Returns: Decision object """ - return self.client.decide(self, key, options) + return self.client._decide(self, key, options) def decide_for_keys(self, keys, options=None): """ @@ -69,7 +69,7 @@ def decide_for_keys(self, keys, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - return self.client.decide_for_keys(self, keys, options) + return self.client._decide_for_keys(self, keys, options) def decide_all(self, options=None): """ @@ -80,7 +80,7 @@ def decide_all(self, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - return self.client.decide_all(self, options) + return self.client._decide_all(self, options) def track_event(self, event_key, event_tags=None): return self.client.track(event_key, self.user_id, self.user_attributes, event_tags) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index fc3d8c74a..7278ccc13 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -688,7 +688,7 @@ def test_decide_experiment(self): return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), ): user_context = opt_obj.create_user_context('test_user') - decision = opt_obj.decide(user_context, 'test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) self.assertTrue(decision.enabled, "decision should be enabled") def test_activate__with_attributes__audience_match(self): diff --git a/tests/test_user_context.py b/tests/test_user_context.py index b7ae6a67c..5372ae4c0 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -63,7 +63,7 @@ def test_decide_rollout(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) user_context = opt_obj.create_user_context('test_user') - decision = opt_obj.decide(user_context, 'test_feature_in_rollout') + decision = user_context.decide('test_feature_in_rollout') self.assertFalse(decision.enabled) self.assertEqual(decision.flag_key, 'test_feature_in_rollout') @@ -74,7 +74,7 @@ def test_decide_for_keys(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) user_context = opt_obj.create_user_context('test_user') - decisions = opt_obj.decide_for_keys(user_context, ['test_feature_in_rollout', 'test_feature_in_experiment']) + decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) self.assertTrue(len(decisions) == 2) self.assertFalse(decisions['test_feature_in_rollout'].enabled) @@ -90,7 +90,7 @@ def test_decide_all(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) user_context = opt_obj.create_user_context('test_user') - decisions = opt_obj.decide_all(user_context) + decisions = user_context.decide_all() self.assertTrue(len(decisions) == 4) self.assertFalse(decisions['test_feature_in_rollout'].enabled) @@ -113,7 +113,7 @@ def test_decide_all_enabled_only(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) user_context = opt_obj.create_user_context('test_user') - decisions = opt_obj.decide_all(user_context, [DecideOption.ENABLED_FLAGS_ONLY]) + decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) self.assertTrue(len(decisions) == 0) def test_track(self): From 7b5aa3a570a46c684c0ca308c81323e22df81c6e Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 25 Jan 2021 19:00:57 +0500 Subject: [PATCH 08/20] fix: decide option import --- tests/test_user_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 5372ae4c0..fe19c3bf7 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -15,7 +15,7 @@ import mock from optimizely import logger, optimizely, decision_service -from optimizely.decision.decide_option import DecideOption +from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums from optimizely.optimizely_user_context import OptimizelyUserContext From 6c29ee8cf562591f59f6a6421ab7719c8e9baf41 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 25 Jan 2021 19:45:00 +0500 Subject: [PATCH 09/20] mutex locks --- optimizely/optimizely.py | 12 ++++++--- optimizely/optimizely_user_context.py | 38 ++++++++++++++++++++++----- tests/test_user_context.py | 37 +++++++++++++++++--------- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index d353c7a32..d675311bf 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -90,8 +90,12 @@ def __init__( if default_decide_options is None: self.default_decide_options = [] + else: + self.default_decide_options = default_decide_options - if not isinstance(self.default_decide_options, list): + if isinstance(self.default_decide_options, list): + self.default_decide_options = self.default_decide_options[:] + else: self.logger.debug('Provided default decide options is not a list.') self.default_decide_options = [] @@ -1004,7 +1008,7 @@ def _decide(self, user_context, key, decide_options=None): # Create Optimizely Decision Result. user_id = user_context.user_id - attributes = user_context.user_attributes + attributes = user_context.get_user_attributes() variation_key = None variation = None feature_enabled = False @@ -1016,8 +1020,8 @@ def _decide(self, user_context, key, decide_options=None): source_info = {} decision_event_dispatched = False - decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_context.user_id, - user_context.user_attributes, + decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_id, + attributes, OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options) diff --git a/optimizely/optimizely_user_context.py b/optimizely/optimizely_user_context.py index 0bb8f1fb4..386cefaad 100644 --- a/optimizely/optimizely_user_context.py +++ b/optimizely/optimizely_user_context.py @@ -13,6 +13,8 @@ # limitations under the License. # +import threading + class OptimizelyUserContext(object): """ @@ -33,7 +35,19 @@ def __init__(self, optimizely_client, user_id, user_attributes=None): self.client = optimizely_client self.user_id = user_id - self.user_attributes = user_attributes.copy() if user_attributes else {} + + if not isinstance(user_attributes, dict): + user_attributes = {} + + self._user_attributes = user_attributes.copy() if user_attributes else {} + self.lock = threading.Lock() + + def _clone(self): + return OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes) + + def get_user_attributes(self): + with self.lock: + return self._user_attributes.copy() def set_attribute(self, attribute_key, attribute_value): """ @@ -45,7 +59,8 @@ def set_attribute(self, attribute_key, attribute_value): Returns: None """ - self.user_attributes[attribute_key] = attribute_value + with self.lock: + self._user_attributes[attribute_key] = attribute_value def decide(self, key, options=None): """ @@ -57,7 +72,10 @@ def decide(self, key, options=None): Returns: Decision object """ - return self.client._decide(self, key, options) + if isinstance(options, list): + options = options[:] + + return self.client._decide(self._clone(), key, options) def decide_for_keys(self, keys, options=None): """ @@ -69,7 +87,10 @@ def decide_for_keys(self, keys, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - return self.client._decide_for_keys(self, keys, options) + if isinstance(options, list): + options = options[:] + + return self.client._decide_for_keys(self._clone(), keys, options) def decide_all(self, options=None): """ @@ -80,13 +101,16 @@ def decide_all(self, options=None): Returns: Dictionary with feature_key keys and Decision object values """ - return self.client._decide_all(self, options) + if isinstance(options, list): + options = options[:] + + return self.client._decide_all(self._clone(), options) def track_event(self, event_key, event_tags=None): - return self.client.track(event_key, self.user_id, self.user_attributes, event_tags) + return self.client.track(event_key, self.user_id, self.get_user_attributes(), event_tags) def as_json(self): return { 'user_id': self.user_id, - 'attributes': self.user_attributes, + 'attributes': self.get_user_attributes(), } diff --git a/tests/test_user_context.py b/tests/test_user_context.py index fe19c3bf7..5da4abfdf 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -23,7 +23,7 @@ from . import base -class UserContextTests(base.BaseTest): +class UserContextTest(base.BaseTest): def setUp(self): base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') self.logger = logger.NoOpLogger() @@ -33,12 +33,23 @@ def test_user_context(self): tests user context creating and attributes """ uc = OptimizelyUserContext(self.optimizely, "test_user") - self.assertEqual(uc.user_attributes, {}, "should have created default empty") + self.assertEqual(uc.get_user_attributes(), {}, "should have created default empty") self.assertEqual(uc.user_id, "test_user", "should have same user id") uc.set_attribute("key", "value") - self.assertEqual(uc.user_attributes["key"], "value", "should have added attribute") + self.assertEqual(uc.get_user_attributes()["key"], "value", "should have added attribute") uc.set_attribute("key", "value2") - self.assertEqual(uc.user_attributes["key"], "value2", "should have new attribute") + self.assertEqual(uc.get_user_attributes()["key"], "value2", "should have new attribute") + + def test_decide_user_context(self): + """ Test that the user context in decide response is not the same object on which + the decide was called """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + decision = user_context.decide('test_feature_in_rollout') + user_context.set_attribute("test_key", "test_value") + self.assertNotEqual(user_context.get_user_attributes(), decision.user_context.get_user_attributes()) def test_decide_feature_test(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -360,38 +371,38 @@ def test_optimizely_user_context_created_with_expected_values(self): attributes = {"browser": "chrome"} uc = OptimizelyUserContext(self.optimizely, user_id, attributes) self.assertEquals("test_user", uc.user_id) - self.assertEqual(attributes, uc.user_attributes) + self.assertEqual(attributes, uc.get_user_attributes()) self.assertIs(self.optimizely, uc.client) def test_set_attributes(self): user_id = 'test_user' attributes = {"browser": "chrome"} uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEqual(attributes, uc.user_attributes) + self.assertEqual(attributes, uc.get_user_attributes()) uc.set_attribute('color', 'red') self.assertEquals({ "browser": "chrome", - "color": "red"}, uc.user_attributes) + "color": "red"}, uc.get_user_attributes()) def test_set_attributes_overrides_value_of_existing_key(self): user_id = 'test_user' attributes = {"browser": "chrome"} uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEquals(attributes, uc.user_attributes) + self.assertEquals(attributes, uc.get_user_attributes()) uc.set_attribute('browser', 'firefox') - self.assertEquals({"browser": "firefox"}, uc.user_attributes) + self.assertEquals({"browser": "firefox"}, uc.get_user_attributes()) def test_set_attribute_when_no_attributes_provided_in_constructor(self): user_id = 'test_user' uc = OptimizelyUserContext(self.optimizely, user_id) - self.assertEqual({}, uc.user_attributes) + self.assertEqual({}, uc.get_user_attributes()) uc.set_attribute('browser', 'firefox') - self.assertEqual({'browser': 'firefox'}, uc.user_attributes) + self.assertEqual({'browser': 'firefox'}, uc.get_user_attributes()) def test_attribute_when_no_update_on_caller_copy_update(self): user_id = 'test_user' attributes = {"browser": "chrome"} uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEqual(attributes, uc.user_attributes) + self.assertEqual(attributes, uc.get_user_attributes()) attributes['new_key'] = 'test_value' - self.assertNotEqual(attributes, uc.user_attributes) + self.assertNotEqual(attributes, uc.get_user_attributes()) From c9a0e640ed3ca3e35bc61f9e731a8294276adb48 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 26 Jan 2021 19:21:06 +0500 Subject: [PATCH 10/20] Apply suggestions from code review Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --- optimizely/optimizely.py | 2 +- optimizely/optimizely_user_context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index d675311bf..bdff68ce4 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1113,7 +1113,7 @@ def _decide_all(self, user_context, decide_options=None): if not config: self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide')) reasons.append(OptimizelyDecisionMessage.SDK_NOT_READY) - return OptimizelyDecision(user_context=user_context, reasons=reasons) + return {} keys = [] for f in config.feature_flags: diff --git a/optimizely/optimizely_user_context.py b/optimizely/optimizely_user_context.py index 386cefaad..9416f65d8 100644 --- a/optimizely/optimizely_user_context.py +++ b/optimizely/optimizely_user_context.py @@ -43,7 +43,7 @@ def __init__(self, optimizely_client, user_id, user_attributes=None): self.lock = threading.Lock() def _clone(self): - return OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes) + return OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes()) def get_user_attributes(self): with self.lock: From 0462e72057e6255f71c0d4710c0b4e4f9e85499c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 26 Jan 2021 20:41:52 +0500 Subject: [PATCH 11/20] tests: user context tests --- tests/test_optimizely.py | 313 ++++++++++++++++++++++++++++ tests/test_user_context.py | 414 +++++++------------------------------ 2 files changed, 382 insertions(+), 345 deletions(-) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 7278ccc13..20b248e3a 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -4993,3 +4993,316 @@ def test_user_context_invalid_user_id(self): for u in user_ids: uc = self.optimizely.create_user_context(u) self.assertIsNone(uc, "invalid user id should return none") + + # def test_decide_feature_test(self): + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + # project_config = opt_obj.config_manager.get_config() + + # mock_experiment = project_config.get_experiment_from_key('test_experiment') + # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # with mock.patch( + # 'optimizely.decision_service.DecisionService.get_variation_for_feature', + # return_value=decision_service.Decision(mock_experiment, mock_variation, + # enums.DecisionSources.FEATURE_TEST), + # ): + # user_context = opt_obj.create_user_context('test_user') + # decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + # self.assertTrue(decision.enabled, "decision should be enabled") + + # def test_decide_rollout(self): + # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + # Also confirm that no impression event is processed. """ + + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # user_context = opt_obj.create_user_context('test_user') + # decision = user_context.decide('test_feature_in_rollout') + # self.assertFalse(decision.enabled) + # self.assertEqual(decision.flag_key, 'test_feature_in_rollout') + + # def test_decide_for_keys(self): + # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + # Also confirm that no impression event is processed. """ + + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # user_context = opt_obj.create_user_context('test_user') + # decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) + # self.assertTrue(len(decisions) == 2) + + # self.assertFalse(decisions['test_feature_in_rollout'].enabled) + # self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') + + # self.assertFalse(decisions['test_feature_in_experiment'].enabled) + # self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + + # def test_decide_all(self): + # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + # Also confirm that no impression event is processed. """ + + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # user_context = opt_obj.create_user_context('test_user') + # decisions = user_context.decide_all() + # self.assertTrue(len(decisions) == 4) + + # self.assertFalse(decisions['test_feature_in_rollout'].enabled) + # self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') + + # self.assertFalse(decisions['test_feature_in_experiment'].enabled) + # self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + + # self.assertFalse(decisions['test_feature_in_group'].enabled) + # self.assertEqual(decisions['test_feature_in_group'].flag_key, 'test_feature_in_group') + + # self.assertFalse(decisions['test_feature_in_experiment_and_rollout'].enabled) + # self.assertEqual(decisions['test_feature_in_experiment_and_rollout'].flag_key, + # 'test_feature_in_experiment_and_rollout') + + # def test_decide_all_enabled_only(self): + # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + # Also confirm that no impression event is processed. """ + + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # user_context = opt_obj.create_user_context('test_user') + # decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) + # self.assertTrue(len(decisions) == 0) + + # def test_track(self): + # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + # Also confirm that no impression event is processed. """ + + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # 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: + # user_context = opt_obj.create_user_context('test_user') + # user_context.track_event('test_event') + + # log_event = EventFactory.create_log_event(mock_process.call_args[0][0], opt_obj.logger) + # self.assertEqual(log_event.params['visitors'][0]['visitor_id'], 'test_user') + # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['timestamp'], 42000) + # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['uuid'], + # 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c') + # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['key'], 'test_event') + + # def test_decide_sendEvent(self): + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + # project_config = opt_obj.config_manager.get_config() + + # mock_experiment = project_config.get_experiment_from_key('test_experiment') + # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # # Assert that featureEnabled property is True + # self.assertTrue(mock_variation.featureEnabled) + + # with mock.patch( + # 'optimizely.decision_service.DecisionService.get_variation_for_feature', + # return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + # ), 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 + # ): + # context = opt_obj.create_user_context('test_user') + # decision = context.decide('test_feature_in_experiment') + # self.assertTrue(decision.enabled) + + # mock_broadcast_decision.assert_called_with( + # enums.NotificationTypes.DECISION, + # 'flag', + # 'test_user', + # {}, + # { + # 'flag_key': 'test_feature_in_experiment', + # 'enabled': True, + # 'variation_key': decision.variation_key, + # 'rule_key': decision.rule_key, + # 'reasons': decision.reasons, + # 'decision_event_dispatched': True, + # 'variables': decision.variables, + # }, + # ) + # # Check that impression event is sent for rollout and send_flag_decisions = True + # self.assertEqual(1, mock_process.call_count) + + # def test_decide_doNotSendEvent_withOption(self): + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + # project_config = opt_obj.config_manager.get_config() + + # mock_experiment = project_config.get_experiment_from_key('test_experiment') + # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # # Assert that featureEnabled property is True + # self.assertTrue(mock_variation.featureEnabled) + + # with mock.patch( + # 'optimizely.decision_service.DecisionService.get_variation_for_feature', + # return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + # ), 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 + # ): + # context = opt_obj.create_user_context('test_user') + # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + # self.assertTrue(decision.enabled) + + # mock_broadcast_decision.assert_called_with( + # enums.NotificationTypes.DECISION, + # 'flag', + # 'test_user', + # {}, + # { + # 'flag_key': 'test_feature_in_experiment', + # 'enabled': True, + # 'variation_key': decision.variation_key, + # 'rule_key': decision.rule_key, + # 'reasons': decision.reasons, + # 'decision_event_dispatched': False, + # 'variables': decision.variables, + + # }, + # ) + + # # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # # with disable decision event decision option + # self.assertEqual(0, mock_process.call_count) + + # def test_decide_options_bypass_UPS(self): + # user_id = 'test_user' + # experiment_bucket_map = {'111127': {'variation_id': '111128'}} + + # profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) + + # class Ups(UserProfileService): + + # def lookup(self, user_id): + # return profile + + # def save(self, user_profile): + # super(Ups, self).save(user_profile) + + # ups = Ups() + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) + # project_config = opt_obj.config_manager.get_config() + + # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # # Assert that featureEnabled property is True + # self.assertTrue(mock_variation.featureEnabled) + + # with mock.patch( + # 'optimizely.bucketer.Bucketer.bucket', + # return_value=mock_variation, + # ), 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 + # ): + # context = opt_obj.create_user_context(user_id) + # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, + # DecideOption.IGNORE_USER_PROFILE_SERVICE, + # DecideOption.EXCLUDE_VARIABLES]) + # self.assertTrue(decision.enabled) + + # mock_broadcast_decision.assert_called_with( + # enums.NotificationTypes.DECISION, + # 'flag', + # 'test_user', + # {}, + # { + # 'flag_key': 'test_feature_in_experiment', + # 'enabled': True, + # 'variation_key': decision.variation_key, + # 'rule_key': decision.rule_key, + # 'reasons': decision.reasons, + # 'decision_event_dispatched': False, + # 'variables': decision.variables, + + # }, + # ) + + # # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # # with disable decision event decision option + # self.assertEqual(0, mock_process.call_count) + + # def test_decide_options_reasons(self): + # user_id = 'test_user' + # experiment_bucket_map = {'111127': {'variation_id': '111128'}} + + # profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) + + # class Ups(UserProfileService): + + # def lookup(self, user_id): + # return profile + + # def save(self, user_profile): + # super(Ups, self).save(user_profile) + + # ups = Ups() + # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), + # logger=logger.SimpleLogger(min_level=logging.DEBUG), + # user_profile_service=ups) + # project_config = opt_obj.config_manager.get_config() + + # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # # Assert that featureEnabled property is True + # self.assertTrue(mock_variation.featureEnabled) + + # with mock.patch( + # 'optimizely.bucketer.Bucketer.bucket', + # return_value=mock_variation, + # ), 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 + # ): + # context = opt_obj.create_user_context(user_id) + # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, + # DecideOption.IGNORE_USER_PROFILE_SERVICE, + # DecideOption.EXCLUDE_VARIABLES, + # DecideOption.INCLUDE_REASONS]) + # self.assertTrue(decision.enabled) + + # mock_broadcast_decision.assert_called_with( + # enums.NotificationTypes.DECISION, + # 'flag', + # 'test_user', + # {}, + # { + # 'flag_key': 'test_feature_in_experiment', + # 'enabled': True, + # 'variation_key': decision.variation_key, + # 'rule_key': decision.rule_key, + # 'reasons': decision.reasons, + # 'decision_event_dispatched': False, + # 'variables': decision.variables, + + # }, + # ) + + # # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # # with disable decision event decision option + # self.assertEqual(0, mock_process.call_count) \ No newline at end of file diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 5da4abfdf..7fe78e38b 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -11,398 +11,122 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -import logging import mock -from optimizely import logger, optimizely, decision_service -from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption -from optimizely.event.event_factory import EventFactory -from optimizely.helpers import enums +from optimizely import optimizely from optimizely.optimizely_user_context import OptimizelyUserContext -from optimizely.user_profile import UserProfileService, UserProfile from . import base class UserContextTest(base.BaseTest): def setUp(self): base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.logger = logger.NoOpLogger() def test_user_context(self): """ - tests user context creating and attributes + tests user context creating and setting attributes """ uc = OptimizelyUserContext(self.optimizely, "test_user") - self.assertEqual(uc.get_user_attributes(), {}, "should have created default empty") - self.assertEqual(uc.user_id, "test_user", "should have same user id") - uc.set_attribute("key", "value") - self.assertEqual(uc.get_user_attributes()["key"], "value", "should have added attribute") - uc.set_attribute("key", "value2") - self.assertEqual(uc.get_user_attributes()["key"], "value2", "should have new attribute") + # user attribute should be empty dict + self.assertEqual({}, uc.get_user_attributes()) - def test_decide_user_context(self): - """ Test that the user context in decide response is not the same object on which - the decide was called """ + # user id should be as provided in constructor + self.assertEqual("test_user", uc.user_id) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + # set attribute + uc.set_attribute("browser", "chrome") + self.assertEqual("chrome", uc.get_user_attributes()["browser"], ) - user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_rollout') - user_context.set_attribute("test_key", "test_value") - self.assertNotEqual(user_context.get_user_attributes(), decision.user_context.get_user_attributes()) + # set another attribute + uc.set_attribute("color", "red") + self.assertEqual("chrome", uc.get_user_attributes()["browser"]) + self.assertEqual("red", uc.get_user_attributes()["color"]) - def test_decide_feature_test(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() + # override existing attribute + uc.set_attribute("browser", "firefox") + self.assertEqual("firefox", uc.get_user_attributes()["browser"]) + self.assertEqual("red", uc.get_user_attributes()["color"]) - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + def test_attributes_are_cloned_when_passed_to_user_context(self): + user_id = 'test_user' + attributes = {"browser": "chrome"} + uc = OptimizelyUserContext(self.optimizely, user_id, attributes) + self.assertEqual(attributes, uc.get_user_attributes()) + attributes['new_key'] = 'test_value' + self.assertNotEqual(attributes, uc.get_user_attributes()) - with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST), - ): - user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - self.assertTrue(decision.enabled, "decision should be enabled") + def test_attributes_default_to_dict_when_passes_as_non_dict(self): + uc = OptimizelyUserContext(self.optimizely, "test_user", True) + # user attribute should be empty dict + self.assertEqual({}, uc.get_user_attributes()) - def test_decide_rollout(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + uc = OptimizelyUserContext(self.optimizely, "test_user", 10) + # user attribute should be empty dict + self.assertEqual({}, uc.get_user_attributes()) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + uc = OptimizelyUserContext(self.optimizely, "test_user", 'helloworld') + # user attribute should be empty dict + self.assertEqual({}, uc.get_user_attributes()) - user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_rollout') - self.assertFalse(decision.enabled) - self.assertEqual(decision.flag_key, 'test_feature_in_rollout') + uc = OptimizelyUserContext(self.optimizely, "test_user", []) + # user attribute should be empty dict + self.assertEqual({}, uc.get_user_attributes()) - def test_decide_for_keys(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + def test_user_context_is_cloned_when_passed_to_optimizely_APIs(self): + """ Test that the user context in decide response is not the same object on which + the decide was called """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - user_context = opt_obj.create_user_context('test_user') - decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) - self.assertTrue(len(decisions) == 2) - self.assertFalse(decisions['test_feature_in_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') - - self.assertFalse(decisions['test_feature_in_experiment'].enabled) - self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') - - def test_decide_all(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + # decide + decision = user_context.decide('test_feature_in_rollout') + self.assertNotEqual(user_context, decision.user_context) - user_context = opt_obj.create_user_context('test_user') + # decide_all decisions = user_context.decide_all() - self.assertTrue(len(decisions) == 4) + self.assertNotEqual(user_context, decisions['test_feature_in_rollout'].user_context) - self.assertFalse(decisions['test_feature_in_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') - - self.assertFalse(decisions['test_feature_in_experiment'].enabled) - self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') - - self.assertFalse(decisions['test_feature_in_group'].enabled) - self.assertEqual(decisions['test_feature_in_group'].flag_key, 'test_feature_in_group') - - self.assertFalse(decisions['test_feature_in_experiment_and_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_experiment_and_rollout'].flag_key, - 'test_feature_in_experiment_and_rollout') - - def test_decide_all_enabled_only(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + # decide_for_keys + decisions = user_context.decide_for_keys(['test_feature_in_rollout']) + self.assertNotEqual(user_context, decisions['test_feature_in_rollout'].user_context) + def test_user_context_calls_optimizely_API_and_returns_response(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - user_context = opt_obj.create_user_context('test_user') - decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) - self.assertTrue(len(decisions) == 0) - - def test_track(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - 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: - user_context = opt_obj.create_user_context('test_user') - user_context.track_event('test_event') - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], opt_obj.logger) - self.assertEqual(log_event.params['visitors'][0]['visitor_id'], 'test_user') - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['timestamp'], 42000) - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['uuid'], - 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c') - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['key'], 'test_event') - - def test_decide_sendEvent(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) + # decide with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), - ), 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 + 'optimizely.optimizely.Optimizely._decide', + return_value='I am response from decide API' ): - context = opt_obj.create_user_context('test_user') - decision = context.decide('test_feature_in_experiment') - self.assertTrue(decision.enabled) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'flag', - 'test_user', - {}, - { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': True, - 'variables': decision.variables, - }, - ) - # Check that impression event is sent for rollout and send_flag_decisions = True - self.assertEqual(1, mock_process.call_count) - - def test_decide_doNotSendEvent_withOption(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + response = user_context.decide('test_feature_in_rollout') - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) + self.assertEquals('I am response from decide API', response) + # decide_all with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), - ), 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 + 'optimizely.optimizely.Optimizely._decide_all', + return_value='I am response from decide All API' ): - context = opt_obj.create_user_context('test_user') - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - self.assertTrue(decision.enabled) + response = user_context.decide_all() - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'flag', - 'test_user', - {}, - { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': False, - 'variables': decision.variables, - - }, - ) - - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) - - def test_decide_options_bypass_UPS(self): - user_id = 'test_user' - experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - class Ups(UserProfileService): - - def lookup(self, user_id): - return profile - - def save(self, user_profile): - super(Ups, self).save(user_profile) - - ups = Ups() - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) - project_config = opt_obj.config_manager.get_config() - - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) + self.assertEquals('I am response from decide All API', response) + # decide_for_keys with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', - return_value=mock_variation, - ), 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 + 'optimizely.optimizely.Optimizely._decide_for_keys', + return_value='I am response from decide for keys API' ): - context = opt_obj.create_user_context(user_id) - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - DecideOption.IGNORE_USER_PROFILE_SERVICE, - DecideOption.EXCLUDE_VARIABLES]) - self.assertTrue(decision.enabled) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'flag', - 'test_user', - {}, - { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': False, - 'variables': decision.variables, - - }, - ) + response = user_context.decide_for_keys(['test_feature_in_rollout']) - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) - - def test_decide_options_reasons(self): - user_id = 'test_user' - experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - class Ups(UserProfileService): - - def lookup(self, user_id): - return profile - - def save(self, user_profile): - super(Ups, self).save(user_profile) - - ups = Ups() - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), - logger=logger.SimpleLogger(min_level=logging.DEBUG), - user_profile_service=ups) - project_config = opt_obj.config_manager.get_config() - - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) + self.assertEquals('I am response from decide for keys API', response) + # track event with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', - return_value=mock_variation, - ), 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 + 'optimizely.optimizely.Optimizely.track', + return_value='I am response from track API' ): - context = opt_obj.create_user_context(user_id) - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - DecideOption.IGNORE_USER_PROFILE_SERVICE, - DecideOption.EXCLUDE_VARIABLES, - DecideOption.INCLUDE_REASONS]) - self.assertTrue(decision.enabled) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'flag', - 'test_user', - {}, - { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': False, - 'variables': decision.variables, - - }, - ) - - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) + response = user_context.track_event('some_event') - "Add more test cases for user_context scenario" - - def test_optimizely_user_context_created_with_expected_values(self): - user_id = 'test_user' - attributes = {"browser": "chrome"} - uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEquals("test_user", uc.user_id) - self.assertEqual(attributes, uc.get_user_attributes()) - self.assertIs(self.optimizely, uc.client) - - def test_set_attributes(self): - user_id = 'test_user' - attributes = {"browser": "chrome"} - uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEqual(attributes, uc.get_user_attributes()) - uc.set_attribute('color', 'red') - self.assertEquals({ - "browser": "chrome", - "color": "red"}, uc.get_user_attributes()) - - def test_set_attributes_overrides_value_of_existing_key(self): - user_id = 'test_user' - attributes = {"browser": "chrome"} - uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEquals(attributes, uc.get_user_attributes()) - uc.set_attribute('browser', 'firefox') - self.assertEquals({"browser": "firefox"}, uc.get_user_attributes()) - - def test_set_attribute_when_no_attributes_provided_in_constructor(self): - user_id = 'test_user' - uc = OptimizelyUserContext(self.optimizely, user_id) - self.assertEqual({}, uc.get_user_attributes()) - uc.set_attribute('browser', 'firefox') - self.assertEqual({'browser': 'firefox'}, uc.get_user_attributes()) - - def test_attribute_when_no_update_on_caller_copy_update(self): - user_id = 'test_user' - attributes = {"browser": "chrome"} - uc = OptimizelyUserContext(self.optimizely, user_id, attributes) - self.assertEqual(attributes, uc.get_user_attributes()) - attributes['new_key'] = 'test_value' - self.assertNotEqual(attributes, uc.get_user_attributes()) + self.assertEquals('I am response from track API', response) From fb5cef93aa3cd280d8a155225201273139a22316 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 27 Jan 2021 20:52:13 +0500 Subject: [PATCH 12/20] tests: WIP --- tests/test_optimizely.py | 313 ----------------------------------- tests/test_user_context.py | 325 ++++++++++++++++++++++++++++++++++--- 2 files changed, 302 insertions(+), 336 deletions(-) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 20b248e3a..7278ccc13 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -4993,316 +4993,3 @@ def test_user_context_invalid_user_id(self): for u in user_ids: uc = self.optimizely.create_user_context(u) self.assertIsNone(uc, "invalid user id should return none") - - # def test_decide_feature_test(self): - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - # project_config = opt_obj.config_manager.get_config() - - # mock_experiment = project_config.get_experiment_from_key('test_experiment') - # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # with mock.patch( - # 'optimizely.decision_service.DecisionService.get_variation_for_feature', - # return_value=decision_service.Decision(mock_experiment, mock_variation, - # enums.DecisionSources.FEATURE_TEST), - # ): - # user_context = opt_obj.create_user_context('test_user') - # decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - # self.assertTrue(decision.enabled, "decision should be enabled") - - # def test_decide_rollout(self): - # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - # Also confirm that no impression event is processed. """ - - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # user_context = opt_obj.create_user_context('test_user') - # decision = user_context.decide('test_feature_in_rollout') - # self.assertFalse(decision.enabled) - # self.assertEqual(decision.flag_key, 'test_feature_in_rollout') - - # def test_decide_for_keys(self): - # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - # Also confirm that no impression event is processed. """ - - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # user_context = opt_obj.create_user_context('test_user') - # decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) - # self.assertTrue(len(decisions) == 2) - - # self.assertFalse(decisions['test_feature_in_rollout'].enabled) - # self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') - - # self.assertFalse(decisions['test_feature_in_experiment'].enabled) - # self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') - - # def test_decide_all(self): - # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - # Also confirm that no impression event is processed. """ - - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # user_context = opt_obj.create_user_context('test_user') - # decisions = user_context.decide_all() - # self.assertTrue(len(decisions) == 4) - - # self.assertFalse(decisions['test_feature_in_rollout'].enabled) - # self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') - - # self.assertFalse(decisions['test_feature_in_experiment'].enabled) - # self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') - - # self.assertFalse(decisions['test_feature_in_group'].enabled) - # self.assertEqual(decisions['test_feature_in_group'].flag_key, 'test_feature_in_group') - - # self.assertFalse(decisions['test_feature_in_experiment_and_rollout'].enabled) - # self.assertEqual(decisions['test_feature_in_experiment_and_rollout'].flag_key, - # 'test_feature_in_experiment_and_rollout') - - # def test_decide_all_enabled_only(self): - # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - # Also confirm that no impression event is processed. """ - - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # user_context = opt_obj.create_user_context('test_user') - # decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) - # self.assertTrue(len(decisions) == 0) - - # def test_track(self): - # """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - # Also confirm that no impression event is processed. """ - - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # 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: - # user_context = opt_obj.create_user_context('test_user') - # user_context.track_event('test_event') - - # log_event = EventFactory.create_log_event(mock_process.call_args[0][0], opt_obj.logger) - # self.assertEqual(log_event.params['visitors'][0]['visitor_id'], 'test_user') - # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['timestamp'], 42000) - # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['uuid'], - # 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c') - # self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['key'], 'test_event') - - # def test_decide_sendEvent(self): - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - # project_config = opt_obj.config_manager.get_config() - - # mock_experiment = project_config.get_experiment_from_key('test_experiment') - # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # # Assert that featureEnabled property is True - # self.assertTrue(mock_variation.featureEnabled) - - # with mock.patch( - # 'optimizely.decision_service.DecisionService.get_variation_for_feature', - # return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), - # ), 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 - # ): - # context = opt_obj.create_user_context('test_user') - # decision = context.decide('test_feature_in_experiment') - # self.assertTrue(decision.enabled) - - # mock_broadcast_decision.assert_called_with( - # enums.NotificationTypes.DECISION, - # 'flag', - # 'test_user', - # {}, - # { - # 'flag_key': 'test_feature_in_experiment', - # 'enabled': True, - # 'variation_key': decision.variation_key, - # 'rule_key': decision.rule_key, - # 'reasons': decision.reasons, - # 'decision_event_dispatched': True, - # 'variables': decision.variables, - # }, - # ) - # # Check that impression event is sent for rollout and send_flag_decisions = True - # self.assertEqual(1, mock_process.call_count) - - # def test_decide_doNotSendEvent_withOption(self): - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - # project_config = opt_obj.config_manager.get_config() - - # mock_experiment = project_config.get_experiment_from_key('test_experiment') - # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # # Assert that featureEnabled property is True - # self.assertTrue(mock_variation.featureEnabled) - - # with mock.patch( - # 'optimizely.decision_service.DecisionService.get_variation_for_feature', - # return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), - # ), 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 - # ): - # context = opt_obj.create_user_context('test_user') - # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - # self.assertTrue(decision.enabled) - - # mock_broadcast_decision.assert_called_with( - # enums.NotificationTypes.DECISION, - # 'flag', - # 'test_user', - # {}, - # { - # 'flag_key': 'test_feature_in_experiment', - # 'enabled': True, - # 'variation_key': decision.variation_key, - # 'rule_key': decision.rule_key, - # 'reasons': decision.reasons, - # 'decision_event_dispatched': False, - # 'variables': decision.variables, - - # }, - # ) - - # # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # # with disable decision event decision option - # self.assertEqual(0, mock_process.call_count) - - # def test_decide_options_bypass_UPS(self): - # user_id = 'test_user' - # experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - # profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - # class Ups(UserProfileService): - - # def lookup(self, user_id): - # return profile - - # def save(self, user_profile): - # super(Ups, self).save(user_profile) - - # ups = Ups() - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) - # project_config = opt_obj.config_manager.get_config() - - # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # # Assert that featureEnabled property is True - # self.assertTrue(mock_variation.featureEnabled) - - # with mock.patch( - # 'optimizely.bucketer.Bucketer.bucket', - # return_value=mock_variation, - # ), 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 - # ): - # context = opt_obj.create_user_context(user_id) - # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - # DecideOption.IGNORE_USER_PROFILE_SERVICE, - # DecideOption.EXCLUDE_VARIABLES]) - # self.assertTrue(decision.enabled) - - # mock_broadcast_decision.assert_called_with( - # enums.NotificationTypes.DECISION, - # 'flag', - # 'test_user', - # {}, - # { - # 'flag_key': 'test_feature_in_experiment', - # 'enabled': True, - # 'variation_key': decision.variation_key, - # 'rule_key': decision.rule_key, - # 'reasons': decision.reasons, - # 'decision_event_dispatched': False, - # 'variables': decision.variables, - - # }, - # ) - - # # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # # with disable decision event decision option - # self.assertEqual(0, mock_process.call_count) - - # def test_decide_options_reasons(self): - # user_id = 'test_user' - # experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - # profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - # class Ups(UserProfileService): - - # def lookup(self, user_id): - # return profile - - # def save(self, user_profile): - # super(Ups, self).save(user_profile) - - # ups = Ups() - # opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), - # logger=logger.SimpleLogger(min_level=logging.DEBUG), - # user_profile_service=ups) - # project_config = opt_obj.config_manager.get_config() - - # mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # # Assert that featureEnabled property is True - # self.assertTrue(mock_variation.featureEnabled) - - # with mock.patch( - # 'optimizely.bucketer.Bucketer.bucket', - # return_value=mock_variation, - # ), 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 - # ): - # context = opt_obj.create_user_context(user_id) - # decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - # DecideOption.IGNORE_USER_PROFILE_SERVICE, - # DecideOption.EXCLUDE_VARIABLES, - # DecideOption.INCLUDE_REASONS]) - # self.assertTrue(decision.enabled) - - # mock_broadcast_decision.assert_called_with( - # enums.NotificationTypes.DECISION, - # 'flag', - # 'test_user', - # {}, - # { - # 'flag_key': 'test_feature_in_experiment', - # 'enabled': True, - # 'variation_key': decision.variation_key, - # 'rule_key': decision.rule_key, - # 'reasons': decision.reasons, - # 'decision_event_dispatched': False, - # 'variables': decision.variables, - - # }, - # ) - - # # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # # with disable decision event decision option - # self.assertEqual(0, mock_process.call_count) \ No newline at end of file diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 7fe78e38b..07ee74c76 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -11,11 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. import json +import logging + import mock -from optimizely import optimizely -from optimizely.optimizely_user_context import OptimizelyUserContext +from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption +from optimizely.event.event_factory import EventFactory +from optimizely.helpers import enums +from optimizely.user_profile import UserProfileService, UserProfile from . import base +from optimizely import logger, optimizely, decision_service +from optimizely.optimizely_user_context import OptimizelyUserContext class UserContextTest(base.BaseTest): @@ -91,42 +97,315 @@ def test_user_context_is_cloned_when_passed_to_optimizely_APIs(self): decisions = user_context.decide_for_keys(['test_feature_in_rollout']) self.assertNotEqual(user_context, decisions['test_feature_in_rollout'].user_context) - def test_user_context_calls_optimizely_API_and_returns_response(self): + def test_decide_feature_test(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), + ): + user_context = opt_obj.create_user_context('test_user') + decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + self.assertTrue(decision.enabled, "decision should be enabled") + + def test_decide_rollout(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context('test_user') + decision = user_context.decide('test_feature_in_rollout') + self.assertFalse(decision.enabled) + self.assertEqual(decision.flag_key, 'test_feature_in_rollout') + + def test_decide_for_keys(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) + self.assertTrue(len(decisions) == 2) + + self.assertFalse(decisions['test_feature_in_rollout'].enabled) + self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') + + self.assertFalse(decisions['test_feature_in_experiment'].enabled) + self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + + def test_decide_all(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + decisions = user_context.decide_all() + self.assertTrue(len(decisions) == 4) + + self.assertFalse(decisions['test_feature_in_rollout'].enabled) + self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') + + self.assertFalse(decisions['test_feature_in_experiment'].enabled) + self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + + self.assertFalse(decisions['test_feature_in_group'].enabled) + self.assertEqual(decisions['test_feature_in_group'].flag_key, 'test_feature_in_group') + + self.assertFalse(decisions['test_feature_in_experiment_and_rollout'].enabled) + self.assertEqual(decisions['test_feature_in_experiment_and_rollout'].flag_key, + 'test_feature_in_experiment_and_rollout') + + def test_decide_all_enabled_only(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) + self.assertTrue(len(decisions) == 0) + + def test_track(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + 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: + user_context = opt_obj.create_user_context('test_user') + user_context.track_event('test_event') + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], opt_obj.logger) + self.assertEqual(log_event.params['visitors'][0]['visitor_id'], 'test_user') + self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['timestamp'], 42000) + self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['uuid'], + 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c') + self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['key'], 'test_event') + + def test_decide_sendEvent(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + self.assertTrue(mock_variation.featureEnabled) - # decide with mock.patch( - 'optimizely.optimizely.Optimizely._decide', - return_value='I am response from decide API' + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + ), 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 ): - response = user_context.decide('test_feature_in_rollout') + context = opt_obj.create_user_context('test_user') + decision = context.decide('test_feature_in_experiment') + self.assertTrue(decision.enabled) - self.assertEquals('I am response from decide API', response) + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {}, + { + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': True, + 'variables': decision.variables, + }, + ) + # Check that impression event is sent for rollout and send_flag_decisions = True + self.assertEqual(1, mock_process.call_count) + + def test_decide_doNotSendEvent_withOption(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + self.assertTrue(mock_variation.featureEnabled) - # decide_all with mock.patch( - 'optimizely.optimizely.Optimizely._decide_all', - return_value='I am response from decide All API' + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + ), 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 ): - response = user_context.decide_all() + context = opt_obj.create_user_context('test_user') + decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + self.assertTrue(decision.enabled) - self.assertEquals('I am response from decide All API', response) + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {}, + { + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + + }, + ) + + # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # with disable decision event decision option + self.assertEqual(0, mock_process.call_count) + + def test_decide_options_bypass_UPS(self): + user_id = 'test_user' + experiment_bucket_map = {'111127': {'variation_id': '111128'}} + + profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) + + class Ups(UserProfileService): + + def lookup(self, user_id): + return profile + + def save(self, user_profile): + super(Ups, self).save(user_profile) + + ups = Ups() + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) + project_config = opt_obj.config_manager.get_config() + + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + self.assertTrue(mock_variation.featureEnabled) - # decide_for_keys with mock.patch( - 'optimizely.optimizely.Optimizely._decide_for_keys', - return_value='I am response from decide for keys API' + 'optimizely.bucketer.Bucketer.bucket', + return_value=mock_variation, + ), 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 ): - response = user_context.decide_for_keys(['test_feature_in_rollout']) + context = opt_obj.create_user_context(user_id) + decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, + DecideOption.IGNORE_USER_PROFILE_SERVICE, + DecideOption.EXCLUDE_VARIABLES]) + self.assertTrue(decision.enabled) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {}, + { + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + + }, + ) + + # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # with disable decision event decision option + self.assertEqual(0, mock_process.call_count) + + def test_decide_options_reasons(self): + user_id = 'test_user' + experiment_bucket_map = {'111127': {'variation_id': '111128'}} + + profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) + + class Ups(UserProfileService): + + def lookup(self, user_id): + return profile + + def save(self, user_profile): + super(Ups, self).save(user_profile) - self.assertEquals('I am response from decide for keys API', response) + ups = Ups() + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), + logger=logger.SimpleLogger(min_level=logging.DEBUG), + user_profile_service=ups) + project_config = opt_obj.config_manager.get_config() + + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + self.assertTrue(mock_variation.featureEnabled) - # track event with mock.patch( - 'optimizely.optimizely.Optimizely.track', - return_value='I am response from track API' + 'optimizely.bucketer.Bucketer.bucket', + return_value=mock_variation, + ), 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 ): - response = user_context.track_event('some_event') + context = opt_obj.create_user_context(user_id) + decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, + DecideOption.IGNORE_USER_PROFILE_SERVICE, + DecideOption.EXCLUDE_VARIABLES, + DecideOption.INCLUDE_REASONS]) + self.assertTrue(decision.enabled) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {}, + { + 'flag_key': 'test_feature_in_experiment', + 'enabled': True, + 'variation_key': decision.variation_key, + 'rule_key': decision.rule_key, + 'reasons': decision.reasons, + 'decision_event_dispatched': False, + 'variables': decision.variables, + + }, + ) - self.assertEquals('I am response from track API', response) + # Check that impression event is NOT sent for rollout and send_flag_decisions = True + # with disable decision event decision option + self.assertEqual(0, mock_process.call_count) From fa3f6d9caf25b8e1b2fafc05412b5da97abfc1d2 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 28 Jan 2021 18:55:58 +0500 Subject: [PATCH 13/20] feat: reasons work --- optimizely/bucketer.py | 58 ++-- optimizely/decision_service.py | 272 +++++++++------ optimizely/helpers/audience.py | 31 +- optimizely/optimizely.py | 32 +- tests/helpers_tests/test_audience.py | 109 +++--- tests/test_bucketing.py | 241 +++++++------- tests/test_decision_service.py | 473 +++++++++++++++------------ tests/test_optimizely.py | 254 ++++++++------ tests/test_user_context.py | 14 +- 9 files changed, 861 insertions(+), 623 deletions(-) diff --git a/optimizely/bucketer.py b/optimizely/bucketer.py index 940a95497..aba43874e 100644 --- a/optimizely/bucketer.py +++ b/optimizely/bucketer.py @@ -1,4 +1,4 @@ -# Copyright 2016-2017, 2019-2020 Optimizely +# Copyright 2016-2017, 2019-2021 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 @@ -71,21 +71,24 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations. Returns: - Entity ID which may represent experiment or variation. + Entity ID which may represent experiment or variation and + array of log messages representing decision making. """ - + decide_reasons = [] bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) bucketing_number = self._generate_bucket_value(bucketing_key) + message = 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id) project_config.logger.debug( - 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id) + message ) + decide_reasons.append(message) for traffic_allocation in traffic_allocations: current_end_of_range = traffic_allocation.get('endOfRange') if bucketing_number < current_end_of_range: - return traffic_allocation.get('entityId') + return traffic_allocation.get('entityId'), decide_reasons - return None + return None, decide_reasons def bucket(self, project_config, experiment, user_id, bucketing_id): """ For a given experiment and bucketing ID determines variation to be shown to user. @@ -97,11 +100,13 @@ def bucket(self, project_config, experiment, user_id, bucketing_id): bucketing_id: ID to be used for bucketing the user. Returns: - Variation in which user with ID user_id will be put in. None if no variation. + Variation in which user with ID user_id will be put in. None if no variation + and array of log messages representing decision making. + */. """ - + decide_reasons = [] if not experiment: - return None + return None, decide_reasons # Determine if experiment is in a mutually exclusive group. # This will not affect evaluation of rollout rules. @@ -109,29 +114,44 @@ def bucket(self, project_config, experiment, user_id, bucketing_id): group = project_config.get_group(experiment.groupId) if not group: - return None + return None, decide_reasons - user_experiment_id = self.find_bucket( + user_experiment_id, find_bucket_reasons = self.find_bucket( project_config, bucketing_id, experiment.groupId, group.trafficAllocation, ) + decide_reasons += find_bucket_reasons if not user_experiment_id: - project_config.logger.info('User "%s" is in no experiment.' % user_id) - return None + message = 'User "%s" is in no experiment.' % user_id + project_config.logger.info(message) + decide_reasons.append(message) + return None, decide_reasons if user_experiment_id != experiment.id: + message = 'User "%s" is not in experiment "%s" of group %s.' \ + % (user_id, experiment.key, experiment.groupId) project_config.logger.info( - 'User "%s" is not in experiment "%s" of group %s.' % (user_id, experiment.key, experiment.groupId) + message ) - return None + decide_reasons.append(message) + return None, decide_reasons + message = 'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId) project_config.logger.info( - 'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId) + message ) + decide_reasons.append(message) # Bucket user if not in white-list and in group (if any) - variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation) + variation_id, find_bucket_reasons = self.find_bucket(project_config, bucketing_id, + experiment.id, experiment.trafficAllocation) + decide_reasons += find_bucket_reasons if variation_id: variation = project_config.get_variation_from_id(experiment.key, variation_id) - return variation + return variation, decide_reasons + + else: + message = 'Bucketed into an empty traffic range. Returning nil.' + project_config.logger.info(message) + decide_reasons.append(message) - return None + return None, decide_reasons diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 5d9c8e5bd..1a835c98f 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -47,19 +47,21 @@ def _get_bucketing_id(self, user_id, attributes): attributes: Dict representing user attributes. May consist of bucketing ID to be used. Returns: - String representing bucketing ID if it is a String type in attributes else return user ID. + String representing bucketing ID if it is a String type in attributes else return user ID + array of log messages representing decision making. """ - + decide_reasons = [] attributes = attributes or {} bucketing_id = attributes.get(enums.ControlAttributes.BUCKETING_ID) if bucketing_id is not None: if isinstance(bucketing_id, string_types): - return bucketing_id - - self.logger.warning('Bucketing ID attribute is not a string. Defaulted to user_id.') + return bucketing_id, decide_reasons + message = 'Bucketing ID attribute is not a string. Defaulted to user_id.' + self.logger.warning(message) + decide_reasons.append(message) - return user_id + return user_id, decide_reasons def set_forced_variation(self, project_config, experiment_key, user_id, variation_key): """ Sets users to a map of experiments to forced variations. @@ -128,38 +130,46 @@ def get_forced_variation(self, project_config, experiment_key, user_id): user_id: The user ID. Returns: - The variation which the given user and experiment should be forced into. + The variation which the given user and experiment should be forced into and + array of log messages representing decision making. """ - + decide_reasons = [] if user_id not in self.forced_variation_map: - self.logger.debug('User "%s" is not in the forced variation map.' % user_id) - return None + message = 'User "%s" is not in the forced variation map.' % user_id + self.logger.debug(message) + decide_reasons.append(message) + return None, decide_reasons experiment = project_config.get_experiment_from_key(experiment_key) if not experiment: # The invalid experiment key will be logged inside this call. - return None + return None, decide_reasons experiment_to_variation_map = self.forced_variation_map.get(user_id) if not experiment_to_variation_map: + message = 'No experiment "%s" mapped to user "%s" in the forced variation map.' % (experiment_key, user_id) self.logger.debug( - 'No experiment "%s" mapped to user "%s" in the forced variation map.' % (experiment_key, user_id) + message ) - return None + decide_reasons.append(message) + return None, decide_reasons variation_id = experiment_to_variation_map.get(experiment.id) if variation_id is None: - self.logger.debug('No variation mapped to experiment "%s" in the forced variation map.' % experiment_key) - return None + message = 'No variation mapped to experiment "%s" in the forced variation map.' % experiment_key + self.logger.debug(message) + decide_reasons.append(message) + return None, decide_reasons variation = project_config.get_variation_from_id(experiment_key, variation_id) - + message = 'Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map' \ + % (variation.key, experiment_key, user_id) self.logger.debug( - 'Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map' - % (variation.key, experiment_key, user_id) + message ) - return variation + decide_reasons.append(message) + return variation, decide_reasons def get_whitelisted_variation(self, project_config, experiment, user_id): """ Determine if a user is forced into a variation (through whitelisting) @@ -171,18 +181,21 @@ def get_whitelisted_variation(self, project_config, experiment, user_id): user_id: ID for the user. Returns: - Variation in which the user with ID user_id is forced into. None if no variation. + Variation in which the user with ID user_id is forced into. None if no variation and + array of log messages representing decision making. """ - + decide_reasons = [] forced_variations = experiment.forcedVariations if forced_variations and user_id in forced_variations: variation_key = forced_variations.get(user_id) variation = project_config.get_variation_from_key(experiment.key, variation_key) if variation: - self.logger.info('User "%s" is forced in variation "%s".' % (user_id, variation_key)) - return variation + message = 'User "%s" is forced in variation "%s".' % (user_id, variation_key) + self.logger.info(message) + decide_reasons.append(message) + return variation, decide_reasons - return None + return None, decide_reasons def get_stored_variation(self, project_config, experiment, user_profile): """ Determine if the user has a stored variation available for the given experiment and return that. @@ -193,24 +206,28 @@ def get_stored_variation(self, project_config, experiment, user_profile): user_profile: UserProfile object representing the user's profile. Returns: - Variation if available. None otherwise. + Variation if available. None otherwise And an array of log messages representing decision making. """ - + decide_reasons = [] user_id = user_profile.user_id variation_id = user_profile.get_variation_for_experiment(experiment.id) if variation_id: variation = project_config.get_variation_from_id(experiment.key, variation_id) if variation: + message = 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".'\ + % (user_id, variation.key, experiment.key) self.logger.info( - 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' - % (user_id, variation.key, experiment.key) + message ) - return variation + decide_reasons.append(message) + return variation, decide_reasons - return None + return None, decide_reasons - def get_variation(self, project_config, experiment, user_id, attributes, ignore_user_profile=False): + def get_variation( + self, project_config, experiment, user_id, attributes, ignore_user_profile=False, decide_options=[] + ): """ Top-level function to help determine variation user should be put in. First, check if experiment is running. @@ -225,25 +242,31 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_ user_id: ID for user. attributes: Dict representing user attributes. ignore_user_profile: True to ignore the user profile lookup. Defaults to False. + decideOptions: Options to customize evaluation. Returns: - Variation user should see. None if user is not in experiment or experiment is not running. + Variation user should see. None if user is not in experiment or experiment is not running + And an array of log messages representing decision making. """ - + decide_reasons = [] # Check if experiment is running if not experiment_helper.is_experiment_running(experiment): - self.logger.info('Experiment "%s" is not running.' % experiment.key) - return None + message = 'Experiment "%s" is not running.' % experiment.key + self.logger.info(message) + decide_reasons.append(message) + return None, decide_reasons # Check if the user is forced into a variation - variation = self.get_forced_variation(project_config, experiment.key, user_id) + variation, reasons_received = self.get_forced_variation(project_config, experiment.key, user_id) + decide_reasons += reasons_received if variation: - return variation + return variation, decide_reasons # Check to see if user is white-listed for a certain variation - variation = self.get_whitelisted_variation(project_config, experiment, user_id) + variation, reasons_received = self.get_whitelisted_variation(project_config, experiment, user_id) + decide_reasons += reasons_received if variation: - return variation + return variation, decide_reasons # Check to see if user has a decision available for the given experiment user_profile = UserProfile(user_id) @@ -256,30 +279,44 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_ if validator.is_user_profile_valid(retrieved_profile): user_profile = UserProfile(**retrieved_profile) - variation = self.get_stored_variation(project_config, experiment, user_profile) + variation, reasons_received = self.get_stored_variation(project_config, experiment, user_profile) + decide_reasons += reasons_received if variation: - return variation + message = 'Returning previously activated variation ID "{}" of experiment ' \ + '"{}" for user "{}" from user profile.'.format(variation, experiment, user_id) + self.logger.info(message) + decide_reasons.append(message) + return variation, decide_reasons else: self.logger.warning('User profile has invalid format.') # Bucket user and store the new decision audience_conditions = experiment.get_audience_conditions_or_ids() - if not audience_helper.does_user_meet_audience_conditions(project_config, audience_conditions, - enums.ExperimentAudienceEvaluationLogs, - experiment.key, - attributes, self.logger): + user_meets_audience_conditions, reasons_received = audience_helper.does_user_meet_audience_conditions( + project_config, audience_conditions, + enums.ExperimentAudienceEvaluationLogs, + experiment.key, + attributes, self.logger) + decide_reasons += reasons_received + if not user_meets_audience_conditions: + message = 'User "{}" does not meet conditions to be in experiment "{}".'.format(user_id, experiment.key) self.logger.info( - 'User "{}" does not meet conditions to be in experiment "{}".'.format(user_id, experiment.key)) - return None + message + ) + decide_reasons.append(message) + return None, decide_reasons # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) - + bucketing_id, bucketing_id_reasons = self._get_bucketing_id(user_id, attributes) + decide_reasons += bucketing_id_reasons + variation, bucket_reasons = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) + decide_reasons += bucket_reasons if variation: + message = 'User "%s" is in variation "%s" of experiment %s.' % (user_id, variation.key, experiment.key) self.logger.info( - 'User "%s" is in variation "%s" of experiment %s.' % (user_id, variation.key, experiment.key) + message ) + decide_reasons.append(message) # Store this new decision and return the variation for the user if not ignore_user_profile and self.user_profile_service: try: @@ -287,26 +324,27 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_ self.user_profile_service.save(user_profile.__dict__) except: self.logger.exception('Unable to save user profile for user "{}".'.format(user_id)) - return variation - - self.logger.info('User "%s" is in no variation.' % user_id) - return None + return variation, decide_reasons + message = 'User "%s" is in no variation.' % user_id + self.logger.info(message) + decide_reasons.append(message) + return None, decide_reasons def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None): """ Determine which experiment/variation the user is in for a given rollout. - Returns the variation of the first experiment the user qualifies for. + Returns the variation of the first experiment the user qualifies for. Args: project_config: Instance of ProjectConfig. rollout: Rollout for which we are getting the variation. user_id: ID for user. attributes: Dict representing user attributes. - ignore_user_profile: True if we should bypass the user profile service Returns: - Decision namedtuple consisting of experiment and variation for the user. + Decision namedtuple consisting of experiment and variation for the user and + array of log messages representing decision making. """ - + decide_reasons = [] # Go through each experiment in order and try to get the variation for the user if rollout and len(rollout.experiments) > 0: for idx in range(len(rollout.experiments) - 1): @@ -315,53 +353,72 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes # Check if user meets audience conditions for targeting rule audience_conditions = rollout_rule.get_audience_conditions_or_ids() - if not audience_helper.does_user_meet_audience_conditions(project_config, - audience_conditions, - enums.RolloutRuleAudienceEvaluationLogs, - logging_key, - attributes, - self.logger): + user_meets_audience_conditions, reasons_received = audience_helper.does_user_meet_audience_conditions( + project_config, + audience_conditions, + enums.RolloutRuleAudienceEvaluationLogs, + logging_key, + attributes, + self.logger) + decide_reasons += reasons_received + if not user_meets_audience_conditions: + message = 'User "{}" does not meet conditions for targeting rule {}.'.format(user_id, logging_key) self.logger.debug( - 'User "{}" does not meet conditions for targeting rule {}.'.format(user_id, logging_key)) + message + ) + decide_reasons.append(message) continue - - self.logger.debug( - 'User "{}" meets audience conditions for targeting rule {}.'.format(user_id, idx + 1)) + message = 'User "{}" meets audience conditions for targeting rule {}.'.format(user_id, idx + 1) + self.logger.debug(message) + decide_reasons.append(message) # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, rollout_rule, user_id, bucketing_id) + bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes) + decide_reasons += bucket_reasons + variation, reasons = self.bucketer.bucket(project_config, rollout_rule, user_id, bucketing_id) + decide_reasons += reasons if variation: + message = 'User "{}" is in the traffic group of targeting rule {}.'.format(user_id, logging_key) self.logger.debug( - 'User "{}" is in the traffic group of targeting rule {}.'.format(user_id, logging_key) + message ) - return Decision(rollout_rule, variation, enums.DecisionSources.ROLLOUT) + decide_reasons.append(message) + return Decision(rollout_rule, variation, enums.DecisionSources.ROLLOUT), decide_reasons else: + message = 'User "{}" is not in the traffic group for targeting rule {}. ' \ + 'Checking "Everyone Else" rule now.'.format(user_id, logging_key) # Evaluate no further rules self.logger.debug( - 'User "{}" is not in the traffic group for targeting rule {}. ' - 'Checking "Everyone Else" rule now.'.format(user_id, logging_key) + message ) + decide_reasons.append(message) break # Evaluate last rule i.e. "Everyone Else" rule everyone_else_rule = project_config.get_experiment_from_key(rollout.experiments[-1].get('key')) audience_conditions = everyone_else_rule.get_audience_conditions_or_ids() - if audience_helper.does_user_meet_audience_conditions( + audience_eval, audience_reasons = audience_helper.does_user_meet_audience_conditions( project_config, audience_conditions, enums.RolloutRuleAudienceEvaluationLogs, 'Everyone Else', attributes, self.logger - ): + ) + decide_reasons += audience_reasons + if audience_eval: # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, everyone_else_rule, user_id, bucketing_id) + bucketing_id, bucket_id_reasons = self._get_bucketing_id(user_id, attributes) + decide_reasons += bucket_id_reasons + variation, bucket_reasons = self.bucketer.bucket( + project_config, everyone_else_rule, user_id, bucketing_id) + decide_reasons += bucket_reasons if variation: - self.logger.debug('User "{}" meets conditions for targeting rule "Everyone Else".'.format(user_id)) - return Decision(everyone_else_rule, variation, enums.DecisionSources.ROLLOUT,) + message = 'User "{}" meets conditions for targeting rule "Everyone Else".'.format(user_id) + self.logger.debug(message) + decide_reasons.append(message) + return Decision(everyone_else_rule, variation, enums.DecisionSources.ROLLOUT,), decide_reasons - return Decision(None, None, enums.DecisionSources.ROLLOUT) + return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons def get_experiment_in_group(self, project_config, group, bucketing_id): """ Determine which experiment in the group the user is bucketed into. @@ -372,24 +429,30 @@ def get_experiment_in_group(self, project_config, group, bucketing_id): bucketing_id: ID to be used for bucketing the user. Returns: - Experiment if the user is bucketed into an experiment in the specified group. None otherwise. + Experiment if the user is bucketed into an experiment in the specified group. None otherwise + and array of log messages representing decision making. """ - - experiment_id = self.bucketer.find_bucket(project_config, bucketing_id, group.id, group.trafficAllocation) + decide_reasons = [] + experiment_id, reasons = self.bucketer.find_bucket( + project_config, bucketing_id, group.id, group.trafficAllocation) + decide_reasons += reasons if experiment_id: experiment = project_config.get_experiment_from_id(experiment_id) if experiment: + message = 'User with bucketing ID "%s" is in experiment %s of group %s.' % \ + (bucketing_id, experiment.key, group.id) self.logger.info( - 'User with bucketing ID "%s" is in experiment %s of group %s.' - % (bucketing_id, experiment.key, group.id) + message ) - return experiment - + decide_reasons.append(message) + return experiment, decide_reasons + message = 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id) self.logger.info( - 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id) + message ) + decide_reasons.append(message) - return None + return None, decide_reasons def get_variation_for_feature(self, project_config, feature, user_id, attributes=None, ignore_user_profile=False): """ Returns the experiment/variation the user is bucketed in for the given feature. @@ -399,24 +462,26 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes feature: Feature for which we are determining if it is enabled or not for the given user. user_id: ID for user. attributes: Dict representing user attributes. - ignore_user_profile: True if you want to bypass the user profile service + ignore_user_profile: True if we should bypass the user profile service Returns: Decision namedtuple consisting of experiment and variation for the user. """ - - bucketing_id = self._get_bucketing_id(user_id, attributes) - + decide_reasons = [] + bucketing_id, reasons = self._get_bucketing_id(user_id, attributes) + decide_reasons += reasons # First check if the feature is in a mutex group if feature.groupId: group = project_config.get_group(feature.groupId) if group: - experiment = self.get_experiment_in_group(project_config, group, bucketing_id) + experiment, reasons = self.get_experiment_in_group(project_config, group, bucketing_id) + decide_reasons += reasons if experiment and experiment.id in feature.experimentIds: - variation = self.get_variation(project_config, experiment, user_id, attributes, ignore_user_profile) - + variation, variation_reasons = self.get_variation( + project_config, experiment, user_id, attributes, ignore_user_profile) + decide_reasons += variation_reasons if variation: - return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) + return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons else: self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature')) @@ -425,14 +490,15 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes # If an experiment is not in a group, then the feature can only be associated with one experiment experiment = project_config.get_experiment_from_id(feature.experimentIds[0]) if experiment: - variation = self.get_variation(project_config, experiment, user_id, attributes, ignore_user_profile) - + variation, variation_reasons = self.get_variation( + project_config, experiment, user_id, attributes, ignore_user_profile) + decide_reasons += variation_reasons if variation: - return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) + return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons # Next check if user is part of a rollout if feature.rolloutId: rollout = project_config.get_rollout_from_id(feature.rolloutId) return self.get_variation_for_rollout(project_config, rollout, user_id, attributes) else: - return Decision(None, None, enums.DecisionSources.ROLLOUT) + return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons diff --git a/optimizely/helpers/audience.py b/optimizely/helpers/audience.py index 857d20efc..6f7882fd0 100644 --- a/optimizely/helpers/audience.py +++ b/optimizely/helpers/audience.py @@ -1,4 +1,4 @@ -# Copyright 2016, 2018-2020, Optimizely +# Copyright 2016, 2018-2021, 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 @@ -35,15 +35,21 @@ def does_user_meet_audience_conditions(config, logger: Provides a logger to send log messages to. Returns: - Boolean representing if user satisfies audience conditions for any of the audiences or not. + Boolean representing if user satisfies audience conditions for any of the audiences or not + And an array of log messages representing decision making. """ - logger.debug(audience_logs.EVALUATING_AUDIENCES_COMBINED.format(logging_key, json.dumps(audience_conditions))) + decide_reasons = [] + message = audience_logs.EVALUATING_AUDIENCES_COMBINED.format(logging_key, json.dumps(audience_conditions)) + logger.debug(message) + # decide_reasons.append(message) # Return True in case there are no audiences if audience_conditions is None or audience_conditions == []: - logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, 'TRUE')) + message = audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, 'TRUE') + logger.info(message) + decide_reasons.append(message) - return True + return True, decide_reasons if attributes is None: attributes = {} @@ -61,19 +67,24 @@ def evaluate_audience(audience_id): if audience is None: return None - - logger.debug(audience_logs.EVALUATING_AUDIENCE.format(audience_id, audience.conditions)) + _message = audience_logs.EVALUATING_AUDIENCE.format(audience_id, audience.conditions) + logger.debug(_message) + decide_reasons.append(_message) result = condition_tree_evaluator.evaluate( audience.conditionStructure, lambda index: evaluate_custom_attr(audience_id, index), ) result_str = str(result).upper() if result is not None else 'UNKNOWN' - logger.debug(audience_logs.AUDIENCE_EVALUATION_RESULT.format(audience_id, result_str)) + _message = audience_logs.AUDIENCE_EVALUATION_RESULT.format(audience_id, result_str) + logger.debug(_message) + decide_reasons.append(_message) return result eval_result = condition_tree_evaluator.evaluate(audience_conditions, evaluate_audience) eval_result = eval_result or False - logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, str(eval_result).upper())) - return eval_result + message = audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, str(eval_result).upper()) + logger.info(message) + decide_reasons.append(message) + return eval_result, decide_reasons diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index bdff68ce4..cac37e800 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 @@ -264,8 +264,8 @@ def _get_feature_variable_for_type( feature_enabled = False source_info = {} variable_value = variable.defaultValue - decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, - attributes) + (decision, _) = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, + attributes) if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -348,7 +348,8 @@ def _get_all_feature_variables_for_type( feature_enabled = False source_info = {} - decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes) + (decision, _) = self.decision_service.get_variation_for_feature( + project_config, feature_flag, user_id, attributes) if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -540,7 +541,7 @@ def get_variation(self, experiment_key, user_id, attributes=None): if not self._validate_user_inputs(attributes): return None - variation = self.decision_service.get_variation(project_config, experiment, user_id, attributes) + (variation, _) = self.decision_service.get_variation(project_config, experiment, user_id, attributes) if variation: variation_key = variation.key @@ -597,7 +598,7 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None): feature_enabled = False source_info = {} - decision = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) + (decision, _) = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT @@ -909,7 +910,7 @@ def get_forced_variation(self, experiment_key, user_id): self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_forced_variation')) return None - forced_variation = self.decision_service.get_forced_variation(project_config, experiment_key, user_id) + forced_variation, _ = self.decision_service.get_forced_variation(project_config, experiment_key, user_id) return forced_variation.key if forced_variation else None def get_optimizely_config(self): @@ -1019,11 +1020,12 @@ def _decide(self, user_context, key, decide_options=None): decision_source = DecisionSources.ROLLOUT source_info = {} decision_event_dispatched = False + ignore_ups = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options - decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_id, - attributes, - OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in - decide_options) + decision, decision_reasons = self.decision_service.get_variation_for_feature(config, feature_flag, user_id, + attributes, ignore_ups) + + reasons += decision_reasons # Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.) if decision.experiment is not None: @@ -1066,6 +1068,10 @@ def _decide(self, user_context, key, decide_options=None): all_variables[variable_key] = actual_value + should_include_reasons = False + if OptimizelyDecideOption.INCLUDE_REASONS in decide_options: + should_include_reasons = True + # Send notification self.notification_center.send_notifications( enums.NotificationTypes.DECISION, @@ -1078,7 +1084,7 @@ def _decide(self, user_context, key, decide_options=None): 'variables': all_variables, 'variation_key': variation_key, 'rule_key': rule_key, - 'reasons': reasons, + 'reasons': reasons if should_include_reasons else [], 'decision_event_dispatched': decision_event_dispatched }, @@ -1086,7 +1092,7 @@ def _decide(self, user_context, key, decide_options=None): return OptimizelyDecision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables, rule_key=rule_key, flag_key=flag_key, - user_context=user_context, reasons=reasons + user_context=user_context, reasons=reasons if should_include_reasons else [] ) def _decide_all(self, user_context, decide_options=None): diff --git a/tests/helpers_tests/test_audience.py b/tests/helpers_tests/test_audience.py index 953118872..719705d6d 100644 --- a/tests/helpers_tests/test_audience.py +++ b/tests/helpers_tests/test_audience.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 @@ -34,47 +34,48 @@ def test_does_user_meet_audience_conditions__no_audience(self): experiment = self.project_config.get_experiment_from_key('test_experiment') experiment.audienceIds = [] experiment.audienceConditions = [] + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictTrue( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) + user_meets_audience_conditions ) # Audience Ids exist but Audience Conditions is Empty experiment = self.project_config.get_experiment_from_key('test_experiment') experiment.audienceIds = ['11154'] experiment.audienceConditions = [] + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictTrue( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) - + user_meets_audience_conditions ) # Audience Ids is Empty and Audience Conditions is None experiment = self.project_config.get_experiment_from_key('test_experiment') experiment.audienceIds = [] experiment.audienceConditions = None + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictTrue( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) - + user_meets_audience_conditions ) def test_does_user_meet_audience_conditions__with_audience(self): @@ -160,16 +161,16 @@ def test_does_user_meet_audience_conditions__returns_true__when_condition_tree_e user_attributes = {'test_attribute': 'test_value_1'} experiment = self.project_config.get_experiment_from_key('test_experiment') with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=True): - + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictTrue( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) + user_meets_audience_conditions ) def test_does_user_meet_audience_conditions_returns_false_when_condition_tree_evaluator_returns_none_or_false(self): @@ -179,29 +180,29 @@ def test_does_user_meet_audience_conditions_returns_false_when_condition_tree_ev user_attributes = {'test_attribute': 'test_value_1'} experiment = self.project_config.get_experiment_from_key('test_experiment') with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=None): - + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictFalse( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) + user_meets_audience_conditions ) with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=False): - + user_meets_audience_conditions, _ = audience.does_user_meet_audience_conditions( + self.project_config, + experiment.get_audience_conditions_or_ids(), + enums.ExperimentAudienceEvaluationLogs, + 'test_experiment', + user_attributes, + self.mock_client_logger + ) self.assertStrictFalse( - audience.does_user_meet_audience_conditions( - self.project_config, - experiment.get_audience_conditions_or_ids(), - enums.ExperimentAudienceEvaluationLogs, - 'test_experiment', - user_attributes, - self.mock_client_logger - ) + user_meets_audience_conditions ) def test_does_user_meet_audience_conditions__evaluates_audience_ids(self): diff --git a/tests/test_bucketing.py b/tests/test_bucketing.py index f0268b665..fb71ba131 100644 --- a/tests/test_bucketing.py +++ b/tests/test_bucketing.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 @@ -37,14 +37,15 @@ def test_bucket(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42 ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('111128', 'control'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ), + variation, ) mock_generate_bucket_value.assert_called_once_with('test_user111127') @@ -52,13 +53,14 @@ def test_bucket(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242 ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ) + variation ) mock_generate_bucket_value.assert_called_once_with('test_user111127') @@ -66,14 +68,15 @@ def test_bucket(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042 ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('111129', 'variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ), + variation, ) mock_generate_bucket_value.assert_called_once_with('test_user111127') @@ -81,26 +84,27 @@ def test_bucket(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=424242 ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ) + variation ) mock_generate_bucket_value.assert_called_once_with('test_user111127') def test_bucket__invalid_experiment(self): """ Test that bucket returns None for unknown experiment. """ - + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('invalid_experiment'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('invalid_experiment'), - 'test_user', - 'test_user', - ) + variation ) def test_bucket__invalid_group(self): @@ -110,8 +114,8 @@ def test_bucket__invalid_group(self): experiment = project_config.get_experiment_from_key('group_exp_1') # Set invalid group ID for the experiment experiment.groupId = 'invalid_group_id' - - self.assertIsNone(self.bucketer.bucket(self.project_config, experiment, 'test_user', 'test_user')) + variation, _ = self.bucketer.bucket(self.project_config, experiment, 'test_user', 'test_user') + self.assertIsNone(variation) def test_bucket__experiment_in_group(self): """ Test that for provided bucket values correct variation ID is returned. """ @@ -120,14 +124,15 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('28902', 'group_exp_1_variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ), + variation, ) self.assertEqual( @@ -138,13 +143,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 9500], ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ) + variation ) self.assertEqual( [mock.call('test_user19228'), mock.call('test_user32222')], mock_generate_bucket_value.call_args_list, @@ -154,13 +160,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_2'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_2'), - 'test_user', - 'test_user', - ) + variation ) mock_generate_bucket_value.assert_called_once_with('test_user19228') @@ -168,13 +175,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 424242], ) as mock_generate_bucket_value: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ) + variation ) self.assertEqual( [mock.call('test_user19228'), mock.call('test_user32222')], mock_generate_bucket_value.call_args_list, @@ -223,14 +231,15 @@ def test_bucket(self): with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42), mock.patch.object( self.project_config, 'logger' ) as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('111128', 'control'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ), + variation, ) mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') @@ -239,13 +248,14 @@ def test_bucket(self): with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242), mock.patch.object( self.project_config, 'logger' ) as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_called_once_with('Assigned bucket 4242 to user with bucketing ID "test_user".') @@ -254,14 +264,15 @@ def test_bucket(self): with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042), mock.patch.object( self.project_config, 'logger' ) as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('111129', 'variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ), + variation, ) mock_config_logging.debug.assert_called_once_with('Assigned bucket 5042 to user with bucketing ID "test_user".') @@ -270,13 +281,14 @@ def test_bucket(self): with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=424242), mock.patch.object( self.project_config, 'logger' ) as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_called_once_with( @@ -290,14 +302,15 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertEqual( entities.Variation('28902', 'group_exp_1_variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ), + variation, ) mock_config_logging.debug.assert_has_calls( [ @@ -315,13 +328,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[8400, 9500], ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_called_once_with('Assigned bucket 8400 to user with bucketing ID "test_user".') mock_config_logging.info.assert_called_once_with('User "test_user" is in no experiment.') @@ -330,13 +344,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 9500], ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_has_calls( [ @@ -354,13 +369,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_2'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_2'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') mock_config_logging.info.assert_called_once_with( @@ -371,13 +387,14 @@ def test_bucket__experiment_in_group(self): with mock.patch( 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 424242], ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation, _ = self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) self.assertIsNone( - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user', - ) + variation ) mock_config_logging.debug.assert_has_calls( diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 91240e326..92c7d4ceb 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1,4 +1,4 @@ -# Copyright 2017-2020, Optimizely +# Copyright 2017-2021, 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 @@ -33,16 +33,19 @@ def test_get_bucketing_id__no_bucketing_id_attribute(self): """ Test that _get_bucketing_id returns correct bucketing ID when there is no bucketing ID attribute. """ # No attributes + bucketing_id, _ = self.decision_service._get_bucketing_id("test_user", None) self.assertEqual( - "test_user", self.decision_service._get_bucketing_id("test_user", None) + "test_user", + bucketing_id ) # With attributes, but no bucketing ID + bucketing_id, _ = self.decision_service._get_bucketing_id( + "test_user", {"random_key": "random_value"} + ) self.assertEqual( "test_user", - self.decision_service._get_bucketing_id( - "test_user", {"random_key": "random_value"} - ), + bucketing_id, ) def test_get_bucketing_id__bucketing_id_attribute(self): @@ -50,11 +53,12 @@ def test_get_bucketing_id__bucketing_id_attribute(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + bucketing_id, _ = self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": "user_bucket_value"} + ) self.assertEqual( "user_bucket_value", - self.decision_service._get_bucketing_id( - "test_user", {"$opt_bucketing_id": "user_bucket_value"} - ), + bucketing_id, ) mock_decision_service_logging.debug.assert_not_called() @@ -63,33 +67,35 @@ def test_get_bucketing_id__bucketing_id_attribute_not_a_string(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + bucketing_id, _ = self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": True} + ) self.assertEqual( "test_user", - self.decision_service._get_bucketing_id( - "test_user", {"$opt_bucketing_id": True} - ), + bucketing_id, ) mock_decision_service_logging.warning.assert_called_once_with( "Bucketing ID attribute is not a string. Defaulted to user_id." ) mock_decision_service_logging.reset_mock() + bucketing_id, _ = self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": 5.9} + ) self.assertEqual( "test_user", - self.decision_service._get_bucketing_id( - "test_user", {"$opt_bucketing_id": 5.9} - ), + bucketing_id, ) mock_decision_service_logging.warning.assert_called_once_with( "Bucketing ID attribute is not a string. Defaulted to user_id." ) mock_decision_service_logging.reset_mock() - + bucketing_id, _ = self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": 5} + ) self.assertEqual( "test_user", - self.decision_service._get_bucketing_id( - "test_user", {"$opt_bucketing_id": 5} - ), + bucketing_id, ) mock_decision_service_logging.warning.assert_called_once_with( "Bucketing ID attribute is not a string. Defaulted to user_id." @@ -154,10 +160,11 @@ def test_set_forced_variation__multiple_sets(self): self.project_config, "test_experiment", "test_user_1", "variation" ) ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user_1" - ).key, + variation.key, "variation", ) # same user, same experiment, different variation @@ -166,10 +173,11 @@ def test_set_forced_variation__multiple_sets(self): self.project_config, "test_experiment", "test_user_1", "control" ) ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user_1" - ).key, + variation.key, "control", ) # same user, different experiment @@ -178,10 +186,11 @@ def test_set_forced_variation__multiple_sets(self): self.project_config, "group_exp_1", "test_user_1", "group_exp_1_control" ) ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_1" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "group_exp_1", "test_user_1" - ).key, + variation.key, "group_exp_1_control", ) @@ -191,10 +200,11 @@ def test_set_forced_variation__multiple_sets(self): self.project_config, "test_experiment", "test_user_2", "variation" ) ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_2" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user_2" - ).key, + variation.key, "variation", ) # different user, different experiment @@ -203,24 +213,27 @@ def test_set_forced_variation__multiple_sets(self): self.project_config, "group_exp_1", "test_user_2", "group_exp_1_control" ) ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_2" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "group_exp_1", "test_user_2" - ).key, + variation.key, "group_exp_1_control", ) # make sure the first user forced variations are still valid + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user_1" - ).key, + variation.key, "control", ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_1" + ) self.assertEqual( - self.decision_service.get_forced_variation( - self.project_config, "group_exp_1", "test_user_1" - ).key, + variation.key, "group_exp_1_control", ) @@ -269,15 +282,17 @@ def test_get_forced_variation__invalid_user_id(self): "test_experiment" ] = "test_variation" + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", None + ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", None - ) + variation + ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "" ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "" - ) + variation ) def test_get_forced_variation__invalid_experiment_key(self): @@ -286,21 +301,23 @@ def test_get_forced_variation__invalid_experiment_key(self): self.decision_service.forced_variation_map["test_user"][ "test_experiment" ] = "test_variation" - + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment_not_in_datafile", "test_user" + ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment_not_in_datafile", "test_user" - ) + variation + ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, None, "test_user" ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, None, "test_user" - ) + variation + ) + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "", "test_user" ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "", "test_user" - ) + variation ) def test_get_forced_variation_with_none_set_for_user(self): @@ -311,10 +328,11 @@ def test_get_forced_variation_with_none_set_for_user(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user" + ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user" - ) + variation ) mock_decision_service_logging.debug.assert_called_once_with( 'No experiment "test_experiment" mapped to user "test_user" in the forced variation map.' @@ -331,10 +349,11 @@ def test_get_forced_variation_missing_variation_mapped_to_experiment(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + variation, _ = self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user" + ) self.assertIsNone( - self.decision_service.get_forced_variation( - self.project_config, "test_experiment", "test_user" - ) + variation ) mock_decision_service_logging.debug.assert_called_once_with( @@ -348,11 +367,12 @@ def test_get_whitelisted_variation__user_in_forced_variation(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + variation, _ = self.decision_service.get_whitelisted_variation( + self.project_config, experiment, "user_1" + ) self.assertEqual( entities.Variation("111128", "control"), - self.decision_service.get_whitelisted_variation( - self.project_config, experiment, "user_1" - ), + variation, ) mock_decision_service_logging.info.assert_called_once_with( @@ -367,10 +387,11 @@ def test_get_whitelisted_variation__user_in_invalid_variation(self): "optimizely.project_config.ProjectConfig.get_variation_from_key", return_value=None, ) as mock_get_variation_id: + variation, _ = self.decision_service.get_whitelisted_variation( + self.project_config, experiment, "user_1" + ) self.assertIsNone( - self.decision_service.get_whitelisted_variation( - self.project_config, experiment, "user_1" - ) + variation ) mock_get_variation_id.assert_called_once_with("test_experiment", "control") @@ -385,11 +406,12 @@ def test_get_stored_variation__stored_decision_available(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: + variation, _ = self.decision_service.get_stored_variation( + self.project_config, experiment, profile + ) self.assertEqual( entities.Variation("111128", "control"), - self.decision_service.get_stored_variation( - self.project_config, experiment, profile - ), + variation, ) mock_decision_service_logging.info.assert_called_once_with( @@ -401,10 +423,11 @@ def test_get_stored_variation__no_stored_decision_available(self): experiment = self.project_config.get_experiment_from_key("test_experiment") profile = user_profile.UserProfile("test_user") + variation, _ = self.decision_service.get_stored_variation( + self.project_config, experiment, profile + ) self.assertIsNone( - self.decision_service.get_stored_variation( - self.project_config, experiment, profile - ) + variation ) def test_get_variation__experiment_not_running(self): @@ -428,10 +451,11 @@ def test_get_variation__experiment_not_running(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertIsNone( - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ) + variation ) mock_decision_service_logging.info.assert_called_once_with( @@ -451,16 +475,17 @@ def test_get_variation__bucketing_id_provided(self): experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( "optimizely.decision_service.DecisionService.get_forced_variation", - return_value=None, + return_value=[None, []], ), mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=None, + return_value=[None, []], ), mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), mock.patch( - "optimizely.bucketer.Bucketer.bucket" + "optimizely.bucketer.Bucketer.bucket", + return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: - self.decision_service.get_variation( + variation, _ = self.decision_service.get_variation( self.project_config, experiment, "test_user", @@ -481,7 +506,7 @@ def test_get_variation__user_whitelisted_for_variation(self): experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=entities.Variation("111128", "control"), + return_value=[entities.Variation("111128", "control"), []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( @@ -493,11 +518,12 @@ def test_get_variation__user_whitelisted_for_variation(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111128", "control"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that forced variation is returned and stored decision or bucketing service are not involved @@ -516,10 +542,10 @@ def test_get_variation__user_has_stored_decision(self): experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=entities.Variation("111128", "control"), + return_value=[entities.Variation("111128", "control"), []], ) as mock_get_stored_variation, mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions" ) as mock_audience_check, mock.patch( @@ -533,11 +559,12 @@ def test_get_variation__user_has_stored_decision(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111128", "control"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that stored variation is returned and bucketing service is not involved @@ -567,26 +594,27 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=None, + return_value=[None, []], ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup", return_value={"user_id": "test_user", "experiment_bucket_map": {}}, ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that user is bucketed and new decision is stored @@ -627,24 +655,25 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_n self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup" ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that user is bucketed and new decision is not stored as user profile service is not available @@ -674,12 +703,12 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=None, + return_value=[None, []], ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=False + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[False, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket" ) as mock_bucket, mock.patch( @@ -688,10 +717,11 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertIsNone( - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ) + variation ) # Assert that user is bucketed and new decision is stored @@ -721,25 +751,26 @@ def test_get_variation__user_profile_in_invalid_format(self): self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup", return_value="invalid_profile", ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that user is bucketed and new decision is stored @@ -778,25 +809,26 @@ def test_get_variation__user_profile_lookup_fails(self): self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup", side_effect=Exception("major problem"), ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that user is bucketed and new decision is stored @@ -835,25 +867,26 @@ def test_get_variation__user_profile_save_fails(self): self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup", return_value=None ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save", side_effect=Exception("major problem"), ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, experiment, "test_user", None - ), + variation, ) # Assert that user is bucketed and new decision is stored @@ -891,26 +924,27 @@ def test_get_variation__ignore_user_profile_when_specified(self): self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=None, + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=entities.Variation("111129", "variation"), + return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.lookup" ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: + variation, _ = self.decision_service.get_variation( + self.project_config, + experiment, + "test_user", + None, + ignore_user_profile=True, + ) self.assertEqual( entities.Variation("111129", "variation"), - self.decision_service.get_variation( - self.project_config, - experiment, - "test_user", - None, - ignore_user_profile=True, - ), + variation, ) # Assert that user is bucketed and new decision is NOT stored @@ -946,11 +980,12 @@ def test_get_variation_for_rollout__returns_none_if_no_experiments(self): with self.mock_config_logger as mock_logging: no_experiment_rollout = self.project_config.get_rollout_from_id("201111") + variation_received, _ = self.decision_service.get_variation_for_rollout( + self.project_config, no_experiment_rollout, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout( - self.project_config, no_experiment_rollout, "test_user" - ), + variation_received, ) # Assert no log messages were generated @@ -963,20 +998,21 @@ def test_get_variation_for_rollout__returns_decision_if_user_in_rollout(self): rollout = self.project_config.get_rollout_from_id("211111") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=self.project_config.get_variation_from_id("211127", "211129"), + return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: + variation_received, _ = self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ) self.assertEqual( decision_service.Decision( self.project_config.get_experiment_from_id("211127"), self.project_config.get_variation_from_id("211127", "211129"), enums.DecisionSources.ROLLOUT, ), - self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" - ), + variation_received, ) # Check all log messages @@ -998,23 +1034,24 @@ def test_get_variation_for_rollout__calls_bucket_with_bucketing_id(self): rollout = self.project_config.get_rollout_from_id("211111") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", - return_value=self.project_config.get_variation_from_id("211127", "211129"), + return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: + variation_received, _ = self.decision_service.get_variation_for_rollout( + self.project_config, + rollout, + "test_user", + {"$opt_bucketing_id": "user_bucket_value"}, + ) self.assertEqual( decision_service.Decision( self.project_config.get_experiment_from_id("211127"), self.project_config.get_variation_from_id("211127", "211129"), enums.DecisionSources.ROLLOUT, ), - self.decision_service.get_variation_for_rollout( - self.project_config, - rollout, - "test_user", - {"$opt_bucketing_id": "user_bucket_value"}, - ), + variation_received, ) # Check all log messages @@ -1040,17 +1077,18 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): ) with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=True + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging, mock.patch( - "optimizely.bucketer.Bucketer.bucket", side_effect=[None, variation_to_mock] + "optimizely.bucketer.Bucketer.bucket", side_effect=[[None, []], [variation_to_mock, []]] ): + variation_received, _ = self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ) self.assertEqual( decision_service.Decision( everyone_else_exp, variation_to_mock, enums.DecisionSources.ROLLOUT ), - self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" - ), + variation_received, ) # Check that after first experiment, it skips to the last experiment to check @@ -1096,13 +1134,14 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): rollout = self.project_config.get_rollout_from_id("211111") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=False + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[False, []] ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging: + variation_received, _ = self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" - ), + variation_received, ) # Check that all experiments in rollout layer were checked @@ -1164,18 +1203,19 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( ) decision_patch = mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=expected_variation, + return_value=[expected_variation, []], ) with decision_patch as mock_decision, self.mock_decision_logger: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision( expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, ), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_decision.assert_called_once_with( @@ -1188,7 +1228,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(self): """ Test that get_variation_for_feature returns the variation of - the experiment in the rollout that the user is bucketed into. """ + the experiment in the rollout that the user is bucketed into. """ feature = self.project_config.get_feature_from_key("test_feature_in_rollout") @@ -1197,16 +1237,16 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(sel ) get_variation_for_rollout_patch = mock.patch( "optimizely.decision_service.DecisionService.get_variation_for_rollout", - return_value=expected_variation, + return_value=[expected_variation, None], ) - with \ - get_variation_for_rollout_patch as mock_get_variation_for_rollout, \ + with get_variation_for_rollout_patch as mock_get_variation_for_rollout, \ self.mock_decision_logger as mock_decision_service_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( expected_variation, - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) expected_rollout = self.project_config.get_rollout_from_id("211111") @@ -1222,7 +1262,7 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ self, ): """ Test that get_variation_for_feature returns the variation of the experiment in the - feature's rollout even if the user is not bucketed into the feature's experiment. """ + feature's rollout even if the user is not bucketed into the feature's experiment. """ feature = self.project_config.get_feature_from_key( "test_feature_in_experiment_and_rollout" @@ -1234,19 +1274,20 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ ) with mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions", - side_effect=[False, True], + side_effect=[[False, []], [True, []]], ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging, mock.patch( - "optimizely.bucketer.Bucketer.bucket", return_value=expected_variation - ): + "optimizely.bucketer.Bucketer.bucket", return_value=[expected_variation, []]): + + decision, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision( expected_experiment, expected_variation, enums.DecisionSources.ROLLOUT, ), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + decision, ) self.assertEqual(2, mock_audience_check.call_count) @@ -1258,6 +1299,7 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ None, mock_decision_service_logging, ) + mock_audience_check.assert_any_call( self.project_config, self.project_config.get_experiment_from_key("211127").get_audience_conditions_or_ids(), @@ -1279,25 +1321,26 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) ) with mock.patch( "optimizely.decision_service.DecisionService.get_experiment_in_group", - return_value=self.project_config.get_experiment_from_key("group_exp_1"), + return_value=(self.project_config.get_experiment_from_key("group_exp_1"), []), ) as mock_get_experiment_in_group, mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=expected_variation, + return_value=(expected_variation, []), ) as mock_decision: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision( expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, ), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group("19228"), "test_user" - ) + self.project_config, self.project_config.get_group("19228"), 'test_user') + mock_decision.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key("group_exp_1"), @@ -1314,20 +1357,21 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): with mock.patch( "optimizely.decision_service.DecisionService.get_experiment_in_group", - return_value=None, + return_value=[None, []], ) as mock_get_experiment_in_group, mock.patch( "optimizely.decision_service.DecisionService.get_variation" ) as mock_decision: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group("19228"), "test_user" - ) + self.project_config, self.project_config.get_group("19228"), "test_user") + self.assertFalse(mock_decision.called) def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self): @@ -1337,13 +1381,14 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self with mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=None, + return_value=[None, []], ) as mock_decision: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_decision.assert_called_once_with( @@ -1361,11 +1406,12 @@ def test_get_variation_for_feature__returns_none_for_invalid_group_id(self): feature.groupId = "aabbccdd" with self.mock_decision_logger as mock_decision_service_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_decision_service_logging.error.assert_called_once_with( enums.Errors.INVALID_GROUP_ID.format("_get_variation_for_feature") @@ -1381,13 +1427,14 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no with mock.patch( "optimizely.decision_service.DecisionService.get_experiment_in_group", - return_value=self.project_config.get_experiment_from_key("group_exp_2"), + return_value=[self.project_config.get_experiment_from_key("group_exp_2"), []], ) as mock_decision: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" - ), + variation_received, ) mock_decision.assert_called_once_with( @@ -1400,13 +1447,14 @@ def test_get_experiment_in_group(self): group = self.project_config.get_group("19228") experiment = self.project_config.get_experiment_from_id("32222") with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value="32222" + "optimizely.bucketer.Bucketer.find_bucket", return_value=["32222", []] ), self.mock_decision_logger as mock_decision_service_logging: + variation_received, _ = self.decision_service.get_experiment_in_group( + self.project_config, group, "test_user" + ) self.assertEqual( experiment, - self.decision_service.get_experiment_in_group( - self.project_config, group, "test_user" - ), + variation_received, ) mock_decision_service_logging.info.assert_called_once_with( @@ -1418,12 +1466,13 @@ def test_get_experiment_in_group__returns_none_if_user_not_in_group(self): group = self.project_config.get_group("19228") with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value=None + "optimizely.bucketer.Bucketer.find_bucket", return_value=[None, []] ), self.mock_decision_logger as mock_decision_service_logging: + variation_received, _ = self.decision_service.get_experiment_in_group( + self.project_config, group, "test_user" + ) self.assertIsNone( - self.decision_service.get_experiment_in_group( - self.project_config, group, "test_user" - ) + variation_received ) mock_decision_service_logging.info.assert_called_once_with( diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 7278ccc13..16920ffef 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -304,7 +304,7 @@ def test_activate(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + 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( @@ -383,7 +383,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'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) @@ -416,7 +416,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'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.optimizely.track('test_event', 'test_user') @@ -444,7 +444,7 @@ def on_activate(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'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -484,7 +484,7 @@ def on_activate(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'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -520,7 +520,8 @@ def test_decision_listener__user_not_in_experiment(self): """ Test that activate calls broadcast decision with variation_key 'None' \ when user not in experiment. """ - with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None,), mock.patch( + with mock.patch('optimizely.decision_service.DecisionService.get_variation', + return_value=(None, []),), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' @@ -545,7 +546,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'), + 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( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -567,7 +568,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'), + 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( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -594,7 +595,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'), + 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( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -636,7 +637,8 @@ def on_activate(experiment, user_id, attributes, variation, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=( + decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) @@ -662,7 +664,8 @@ def on_activate(experiment, user_id, attributes, variation, event): mock_variation = project_config.get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: @@ -685,7 +688,8 @@ def test_decide_experiment(self): mock_variation = project_config.get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): user_context = opt_obj.create_user_context('test_user') decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) @@ -697,7 +701,7 @@ def test_activate__with_attributes__audience_match(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -768,7 +772,7 @@ def test_activate__with_attributes_of_different_types(self): with mock.patch( 'optimizely.bucketer.Bucketer.bucket', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_bucket, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -1042,7 +1046,7 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -1122,7 +1126,7 @@ def test_activate__with_attributes__no_audience_match(self): """ Test that activate returns None when audience conditions do not match. """ with mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', - return_value=False) as mock_audience_check: + return_value=(False, [])) as mock_audience_check: self.assertIsNone( self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'},) ) @@ -1189,9 +1193,9 @@ def test_activate__bucketer_returns_none(self): with mock.patch( 'optimizely.helpers.audience.does_user_meet_audience_conditions', - return_value=True), mock.patch( + return_value=(True, [])), mock.patch( 'optimizely.bucketer.Bucketer.bucket', - return_value=None) as mock_bucket, mock.patch( + return_value=(None, [])) as mock_bucket, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.assertIsNone( @@ -1780,7 +1784,7 @@ def test_get_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: self.assertEqual( 'variation', self.optimizely.get_variation('test_experiment', 'test_user'), @@ -1805,7 +1809,7 @@ def test_get_variation_with_experiment_in_feature(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: self.assertEqual('variation', opt_obj.get_variation('test_experiment', 'test_user')) @@ -1822,7 +1826,8 @@ def test_get_variation_with_experiment_in_feature(self): def test_get_variation__returns_none(self): """ Test that get_variation returns no variation and broadcasts decision with proper parameters. """ - with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None,), mock.patch( + with mock.patch('optimizely.decision_service.DecisionService.get_variation', + return_value=(None, []),), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: self.assertEqual( @@ -1980,7 +1985,8 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2078,7 +2084,8 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2176,7 +2183,8 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2224,7 +2232,8 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2324,7 +2333,8 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2364,7 +2374,7 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va feature = project_config.get_feature_from_key('test_feature_in_experiment') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2406,7 +2416,7 @@ def test_is_feature_enabled__returns_false_when_variation_is_nil(self,): feature = project_config.get_feature_from_key('test_feature_in_experiment_and_rollout') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2509,14 +2519,19 @@ def test_get_enabled_features__broadcasts_decision_for_each_feature(self): def side_effect(*args, **kwargs): feature = args[1] + response = None if feature.key == 'test_feature_in_experiment': - return decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST) + response = decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST) elif feature.key == 'test_feature_in_rollout': - return decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT) + response = decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT) elif feature.key == 'test_feature_in_experiment_and_rollout': - return decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST,) + response = decision_service.Decision( + mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST,) else: - return decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT) + response = decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT) + + return (response, []) with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', side_effect=side_effect, @@ -2640,7 +2655,8 @@ def test_get_feature_variable_boolean(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2677,7 +2693,8 @@ def test_get_feature_variable_double(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2714,7 +2731,8 @@ def test_get_feature_variable_integer(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2751,7 +2769,8 @@ def test_get_feature_variable_string(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2789,7 +2808,8 @@ def test_get_feature_variable_json(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2835,7 +2855,8 @@ def test_get_all_feature_variables(self): 'variable_without_usage': 45} with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2891,7 +2912,8 @@ def test_get_feature_variable(self): # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2919,7 +2941,8 @@ def test_get_feature_variable(self): # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2949,7 +2972,8 @@ def test_get_feature_variable(self): # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2979,7 +3003,8 @@ def test_get_feature_variable(self): # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3009,7 +3034,8 @@ def test_get_feature_variable(self): # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3048,7 +3074,8 @@ def test_get_feature_variable_boolean_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3089,7 +3116,8 @@ def test_get_feature_variable_double_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3130,7 +3158,8 @@ def test_get_feature_variable_integer_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3171,7 +3200,8 @@ def test_get_feature_variable_string_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3212,7 +3242,8 @@ def test_get_feature_variable_json_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3253,7 +3284,8 @@ def test_get_all_feature_variables_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3306,7 +3338,8 @@ def test_get_feature_variable_for_feature_in_rollout(self): # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3338,7 +3371,8 @@ def test_get_feature_variable_for_feature_in_rollout(self): # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3370,7 +3404,8 @@ def test_get_feature_variable_for_feature_in_rollout(self): # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3402,7 +3437,8 @@ def test_get_feature_variable_for_feature_in_rollout(self): # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3435,7 +3471,8 @@ def test_get_feature_variable_for_feature_in_rollout(self): # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3478,7 +3515,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertTrue( opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') @@ -3487,7 +3525,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -3496,7 +3535,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -3505,7 +3545,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -3514,7 +3555,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -3523,13 +3565,15 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Non-typed with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), @@ -3537,7 +3581,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), @@ -3545,7 +3590,8 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -3560,7 +3606,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3594,7 +3640,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3628,7 +3674,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3662,7 +3708,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3696,7 +3742,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3730,7 +3776,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Non-typed with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3761,7 +3807,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3794,7 +3840,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3827,7 +3873,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -4140,7 +4186,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertTrue( @@ -4155,7 +4202,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -4169,7 +4217,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -4183,7 +4232,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -4197,7 +4247,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -4211,7 +4262,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Non-typed with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) @@ -4223,7 +4275,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), @@ -4236,7 +4289,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), @@ -4249,7 +4303,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -4270,7 +4325,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable_boolean('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4282,7 +4338,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable_double('test_feature_in_rollout', 'price', 'test_user'), @@ -4296,7 +4353,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_rollout', 'count', 'test_user'), @@ -4310,7 +4368,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable_string('test_feature_in_rollout', 'message', 'test_user'), @@ -4323,7 +4382,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"field": 1}, opt_obj.get_feature_variable_json('test_feature_in_rollout', 'object', 'test_user'), @@ -4336,7 +4396,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Non-typed with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4347,7 +4408,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user'), @@ -4360,7 +4422,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user'), @@ -4373,7 +4436,8 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user'), @@ -4391,7 +4455,8 @@ def test_get_feature_variable__returns_none_if_type_mismatch(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: # "is_working" is boolean variable and we are using double method on it. self.assertIsNone( @@ -4411,7 +4476,8 @@ def test_get_feature_variable__returns_none_if_unable_to_cast(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch( 'optimizely.project_config.ProjectConfig.get_typecast_value', side_effect=ValueError(), ), mock.patch.object( @@ -4628,7 +4694,7 @@ def test_activate(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( @@ -4769,7 +4835,7 @@ def test_activate__empty_user_id(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 07ee74c76..9ff21aac5 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -106,8 +106,8 @@ def test_decide_feature_test(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST), + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), ): user_context = opt_obj.create_user_context('test_user') decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) @@ -204,7 +204,8 @@ def test_decide_sendEvent(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []) ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -248,7 +249,8 @@ def test_decide_doNotSendEvent_withOption(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -308,7 +310,7 @@ def save(self, user_profile): with mock.patch( 'optimizely.bucketer.Bucketer.bucket', - return_value=mock_variation, + return_value=(mock_variation, []), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -372,7 +374,7 @@ def save(self, user_profile): with mock.patch( 'optimizely.bucketer.Bucketer.bucket', - return_value=mock_variation, + return_value=(mock_variation, []), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( From 3732d84c96d52085b6453404401dc3789b396b64 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 28 Jan 2021 19:38:02 +0500 Subject: [PATCH 14/20] tests: refact --- optimizely/helpers/audience.py | 4 +--- optimizely/optimizely.py | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/optimizely/helpers/audience.py b/optimizely/helpers/audience.py index 6f7882fd0..e9914c66f 100644 --- a/optimizely/helpers/audience.py +++ b/optimizely/helpers/audience.py @@ -41,7 +41,7 @@ def does_user_meet_audience_conditions(config, decide_reasons = [] message = audience_logs.EVALUATING_AUDIENCES_COMBINED.format(logging_key, json.dumps(audience_conditions)) logger.debug(message) - # decide_reasons.append(message) + decide_reasons.append(message) # Return True in case there are no audiences if audience_conditions is None or audience_conditions == []: @@ -69,7 +69,6 @@ def evaluate_audience(audience_id): return None _message = audience_logs.EVALUATING_AUDIENCE.format(audience_id, audience.conditions) logger.debug(_message) - decide_reasons.append(_message) result = condition_tree_evaluator.evaluate( audience.conditionStructure, lambda index: evaluate_custom_attr(audience_id, index), @@ -78,7 +77,6 @@ def evaluate_audience(audience_id): result_str = str(result).upper() if result is not None else 'UNKNOWN' _message = audience_logs.AUDIENCE_EVALUATION_RESULT.format(audience_id, result_str) logger.debug(_message) - decide_reasons.append(_message) return result diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index cac37e800..e805a7be2 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -264,8 +264,7 @@ def _get_feature_variable_for_type( feature_enabled = False source_info = {} variable_value = variable.defaultValue - (decision, _) = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, - attributes) + decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes) if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -348,7 +347,7 @@ def _get_all_feature_variables_for_type( feature_enabled = False source_info = {} - (decision, _) = self.decision_service.get_variation_for_feature( + decision, _ = self.decision_service.get_variation_for_feature( project_config, feature_flag, user_id, attributes) if decision.variation: @@ -541,7 +540,7 @@ def get_variation(self, experiment_key, user_id, attributes=None): if not self._validate_user_inputs(attributes): return None - (variation, _) = self.decision_service.get_variation(project_config, experiment, user_id, attributes) + variation, _ = self.decision_service.get_variation(project_config, experiment, user_id, attributes) if variation: variation_key = variation.key @@ -598,7 +597,7 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None): feature_enabled = False source_info = {} - (decision, _) = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) + decision, _ = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT From 76666cf1ccd96216ea40c4b452d6959ec6b3130a Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 28 Jan 2021 21:48:09 +0500 Subject: [PATCH 15/20] tests: Add unit tests --- .../decision/optimizely_decision_message.py | 4 +- tests/test_user_context.py | 467 +++++++++--------- 2 files changed, 239 insertions(+), 232 deletions(-) diff --git a/optimizely/decision/optimizely_decision_message.py b/optimizely/decision/optimizely_decision_message.py index f3875a7cd..5b1ab4172 100644 --- a/optimizely/decision/optimizely_decision_message.py +++ b/optimizely/decision/optimizely_decision_message.py @@ -14,5 +14,5 @@ class OptimizelyDecisionMessage(object): SDK_NOT_READY = 'Optimizely SDK not configured properly yet.' - FLAG_KEY_INVALID = 'No flag was found for key "%s".' - VARIABLE_VALUE_INVALID = 'Variable value for key "%s" is invalid or wrong type.' + FLAG_KEY_INVALID = 'No flag was found for key "{}".' + VARIABLE_VALUE_INVALID = 'Variable value for key "{}" is invalid or wrong type.' diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 9ff21aac5..39be39e6a 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -11,16 +11,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -import logging import mock -from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption -from optimizely.event.event_factory import EventFactory +from optimizely.decision.optimizely_decision import OptimizelyDecision from optimizely.helpers import enums -from optimizely.user_profile import UserProfileService, UserProfile from . import base -from optimizely import logger, optimizely, decision_service +from optimizely import optimizely, decision_service from optimizely.optimizely_user_context import OptimizelyUserContext @@ -28,6 +25,15 @@ class UserContextTest(base.BaseTest): def setUp(self): base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + def compare_opt_decisions(self, expected, actual): + self.assertEqual(expected.variation_key, actual.variation_key) + self.assertEqual(expected.enabled, actual.enabled) + self.assertEqual(expected.rule_key, actual.rule_key) + self.assertEqual(expected.flag_key, actual.flag_key) + self.assertEqual(expected.variables, actual.variables) + self.assertEqual(expected.user_context.user_id, actual.user_context.user_id) + self.assertEqual(expected.user_context.get_user_attributes(), actual.user_context.get_user_attributes()) + def test_user_context(self): """ tests user context creating and setting attributes @@ -97,317 +103,318 @@ def test_user_context_is_cloned_when_passed_to_optimizely_APIs(self): decisions = user_context.decide_for_keys(['test_feature_in_rollout']) self.assertNotEqual(user_context, decisions['test_feature_in_rollout'].user_context) - def test_decide_feature_test(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST), []), - ): - user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - self.assertTrue(decision.enabled, "decision should be enabled") - - def test_decide_rollout(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - + def test_decide__SDK_not_ready(self): + opt_obj = optimizely.Optimizely("") user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_rollout') - self.assertFalse(decision.enabled) - self.assertEqual(decision.flag_key, 'test_feature_in_rollout') - - def test_decide_for_keys(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - user_context = opt_obj.create_user_context('test_user') - decisions = user_context.decide_for_keys(['test_feature_in_rollout', 'test_feature_in_experiment']) - self.assertTrue(len(decisions) == 2) + expected = OptimizelyDecision( + variation_key=None, + rule_key=None, + enabled=False, + variables={}, + flag_key='test_feature', + user_context=user_context + ) - self.assertFalse(decisions['test_feature_in_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') + actual = user_context.decide('test_feature') - self.assertFalse(decisions['test_feature_in_experiment'].enabled) - self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + self.compare_opt_decisions(expected, actual) - def test_decide_all(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + self.assertIn( + 'Optimizely SDK not configured properly yet.', + actual.reasons + ) + def test_decide__invalid_flag_key(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - user_context = opt_obj.create_user_context('test_user') - decisions = user_context.decide_all() - self.assertTrue(len(decisions) == 4) - - self.assertFalse(decisions['test_feature_in_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_rollout'].flag_key, 'test_feature_in_rollout') - self.assertFalse(decisions['test_feature_in_experiment'].enabled) - self.assertEqual(decisions['test_feature_in_experiment'].flag_key, 'test_feature_in_experiment') + expected = OptimizelyDecision( + variation_key=None, + rule_key=None, + enabled=False, + variables={}, + flag_key=123, + user_context=user_context + ) - self.assertFalse(decisions['test_feature_in_group'].enabled) - self.assertEqual(decisions['test_feature_in_group'].flag_key, 'test_feature_in_group') + actual = user_context.decide(123) - self.assertFalse(decisions['test_feature_in_experiment_and_rollout'].enabled) - self.assertEqual(decisions['test_feature_in_experiment_and_rollout'].flag_key, - 'test_feature_in_experiment_and_rollout') + self.compare_opt_decisions(expected, actual) - def test_decide_all_enabled_only(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + self.assertIn( + 'No flag was found for key "123".', + actual.reasons + ) + def test_decide__unknown_flag_key(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - user_context = opt_obj.create_user_context('test_user') - decisions = user_context.decide_all([DecideOption.ENABLED_FLAGS_ONLY]) - self.assertTrue(len(decisions) == 0) - def test_track(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ + expected = OptimizelyDecision( + variation_key=None, + rule_key=None, + enabled=False, + variables={}, + flag_key='unknown_flag_key', + user_context=user_context + ) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + actual = user_context.decide('unknown_flag_key') - 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: - user_context = opt_obj.create_user_context('test_user') - user_context.track_event('test_event') + self.compare_opt_decisions(expected, actual) - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], opt_obj.logger) - self.assertEqual(log_event.params['visitors'][0]['visitor_id'], 'test_user') - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['timestamp'], 42000) - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['uuid'], - 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c') - self.assertEqual(log_event.params['visitors'][0]['snapshots'][0]['events'][0]['key'], 'test_event') + self.assertIn( + 'No flag was found for key "unknown_flag_key".', + actual.reasons + ) - def test_decide_sendEvent(self): + def test_decide__feature_test(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() mock_experiment = project_config.get_experiment_from_key('test_experiment') mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) - with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []) + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), ), 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 - ): - context = opt_obj.create_user_context('test_user') - decision = context.decide('test_feature_in_experiment') - self.assertTrue(decision.enabled) + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user') + actual = user_context.decide('test_feature_in_experiment') + + expected_variables = { + 'is_working': True, + 'environment': 'staging', + 'cost': 10.02, + 'count': 4243, + 'variable_without_usage': 45, + 'object': {"test": 123}, + 'true_object': {"true_test": 1.4} + } + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + # assert notification mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, 'flag', 'test_user', {}, { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, 'decision_event_dispatched': True, - 'variables': decision.variables, + 'variables': expected.variables, }, ) - # Check that impression event is sent for rollout and send_flag_decisions = True - self.assertEqual(1, mock_process.call_count) - def test_decide_doNotSendEvent_withOption(self): + # assert event count + self.assertEqual(1, mock_send_event.call_count) + + # assert event payload + mock_send_event.assert_called_with( + project_config, + mock_experiment, + mock_variation, + expected.flag_key, + expected.rule_key, + 'feature-test', + expected.enabled, + 'test_user', + {} + ) + + def test_decide__feature_test__send_flag_decision_false(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() + project_config.send_flag_decisions = False mock_experiment = project_config.get_experiment_from_key('test_experiment') mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) - with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), ), 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 - ): - context = opt_obj.create_user_context('test_user') - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - self.assertTrue(decision.enabled) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'flag', - 'test_user', - {}, - { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': False, - 'variables': decision.variables, + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: - }, + user_context = opt_obj.create_user_context('test_user') + actual = user_context.decide('test_feature_in_experiment') + + expected_variables = { + 'is_working': True, + 'environment': 'staging', + 'cost': 10.02, + 'count': 4243, + 'variable_without_usage': 45, + 'object': {"test": 123}, + 'true_object': {"true_test": 1.4} + } + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context ) - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) - - def test_decide_options_bypass_UPS(self): - user_id = 'test_user' - experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - class Ups(UserProfileService): + self.compare_opt_decisions(expected, actual) - def lookup(self, user_id): - return profile + # assert notification count + self.assertEqual(1, mock_broadcast_decision.call_count) - def save(self, user_profile): - super(Ups, self).save(user_profile) + # assert event count + self.assertEqual(1, mock_send_event.call_count) - ups = Ups() - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) + def test_decide_feature_rollout(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) - with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', - return_value=(mock_variation, []), - ), 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 - ): - context = opt_obj.create_user_context(user_id) - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - DecideOption.IGNORE_USER_PROFILE_SERVICE, - DecideOption.EXCLUDE_VARIABLES]) - self.assertTrue(decision.enabled) + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_attributes = {'test_attribute': 'test_value_1'} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout') + + expected_variables = { + 'is_running': True, + 'message': 'Hello audience', + 'price': 39.99, + 'count': 399, + 'object': {"field": 12} + } + + expected = OptimizelyDecision( + variation_key='211129', + rule_key='211127', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_rollout', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification count + self.assertEqual(1, mock_broadcast_decision.call_count) + # assert notification mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, 'flag', 'test_user', - {}, + user_attributes, { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, - 'decision_event_dispatched': False, - 'variables': decision.variables, - + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': True, + 'variables': expected.variables, }, ) - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) - - def test_decide_options_reasons(self): - user_id = 'test_user' - experiment_bucket_map = {'111127': {'variation_id': '111128'}} - - profile = UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - - class Ups(UserProfileService): - - def lookup(self, user_id): - return profile - - def save(self, user_profile): - super(Ups, self).save(user_profile) + # assert event count + self.assertEqual(1, mock_send_event.call_count) + + # assert event payload + expected_experiment = project_config.get_experiment_from_key(expected.rule_key) + expected_var = project_config.get_variation_from_key(expected.rule_key, expected.variation_key) + mock_send_event.assert_called_with( + project_config, + expected_experiment, + expected_var, + expected.flag_key, + expected.rule_key, + 'rollout', + expected.enabled, + 'test_user', + user_attributes + ) - ups = Ups() - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), - logger=logger.SimpleLogger(min_level=logging.DEBUG), - user_profile_service=ups) + def test_decide_feature_rollout__send_flag_decision_false(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() - - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - self.assertTrue(mock_variation.featureEnabled) + project_config.send_flag_decisions = False with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', - return_value=(mock_variation, []), - ), 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 - ): - context = opt_obj.create_user_context(user_id) - decision = context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT, - DecideOption.IGNORE_USER_PROFILE_SERVICE, - DecideOption.EXCLUDE_VARIABLES, - DecideOption.INCLUDE_REASONS]) - self.assertTrue(decision.enabled) + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_attributes = {'test_attribute': 'test_value_1'} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout') + + expected_variables = { + 'is_running': True, + 'message': 'Hello audience', + 'price': 39.99, + 'count': 399, + 'object': {"field": 12} + } + + expected = OptimizelyDecision( + variation_key='211129', + rule_key='211127', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_rollout', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification count + self.assertEqual(1, mock_broadcast_decision.call_count) + # assert notification mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, 'flag', 'test_user', - {}, + user_attributes, { - 'flag_key': 'test_feature_in_experiment', - 'enabled': True, - 'variation_key': decision.variation_key, - 'rule_key': decision.rule_key, - 'reasons': decision.reasons, + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, 'decision_event_dispatched': False, - 'variables': decision.variables, - + 'variables': expected.variables, }, ) - # Check that impression event is NOT sent for rollout and send_flag_decisions = True - # with disable decision event decision option - self.assertEqual(0, mock_process.call_count) + # assert event count + self.assertEqual(0, mock_send_event.call_count) From a84e53a037946162de9309c1e5d93bbae73a6fef Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 29 Jan 2021 15:14:51 +0500 Subject: [PATCH 16/20] remove reasons from find_bucket --- optimizely/bucketer.py | 16 ++++++---------- optimizely/decision_service.py | 3 +-- tests/test_decision_service.py | 4 ++-- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/optimizely/bucketer.py b/optimizely/bucketer.py index aba43874e..ca5e0f284 100644 --- a/optimizely/bucketer.py +++ b/optimizely/bucketer.py @@ -72,23 +72,20 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio Returns: Entity ID which may represent experiment or variation and - array of log messages representing decision making. """ - decide_reasons = [] bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) bucketing_number = self._generate_bucket_value(bucketing_key) message = 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id) project_config.logger.debug( message ) - decide_reasons.append(message) for traffic_allocation in traffic_allocations: current_end_of_range = traffic_allocation.get('endOfRange') if bucketing_number < current_end_of_range: - return traffic_allocation.get('entityId'), decide_reasons + return traffic_allocation.get('entityId') - return None, decide_reasons + return None def bucket(self, project_config, experiment, user_id, bucketing_id): """ For a given experiment and bucketing ID determines variation to be shown to user. @@ -116,10 +113,10 @@ def bucket(self, project_config, experiment, user_id, bucketing_id): if not group: return None, decide_reasons - user_experiment_id, find_bucket_reasons = self.find_bucket( + user_experiment_id = self.find_bucket( project_config, bucketing_id, experiment.groupId, group.trafficAllocation, ) - decide_reasons += find_bucket_reasons + if not user_experiment_id: message = 'User "%s" is in no experiment.' % user_id project_config.logger.info(message) @@ -142,9 +139,8 @@ def bucket(self, project_config, experiment, user_id, bucketing_id): decide_reasons.append(message) # Bucket user if not in white-list and in group (if any) - variation_id, find_bucket_reasons = self.find_bucket(project_config, bucketing_id, - experiment.id, experiment.trafficAllocation) - decide_reasons += find_bucket_reasons + variation_id = self.find_bucket(project_config, bucketing_id, + experiment.id, experiment.trafficAllocation) if variation_id: variation = project_config.get_variation_from_id(experiment.key, variation_id) return variation, decide_reasons diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 1a835c98f..87da4283a 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -433,9 +433,8 @@ def get_experiment_in_group(self, project_config, group, bucketing_id): and array of log messages representing decision making. """ decide_reasons = [] - experiment_id, reasons = self.bucketer.find_bucket( + experiment_id = self.bucketer.find_bucket( project_config, bucketing_id, group.id, group.trafficAllocation) - decide_reasons += reasons if experiment_id: experiment = project_config.get_experiment_from_id(experiment_id) if experiment: diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 92c7d4ceb..c664d7466 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1447,7 +1447,7 @@ def test_get_experiment_in_group(self): group = self.project_config.get_group("19228") experiment = self.project_config.get_experiment_from_id("32222") with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value=["32222", []] + "optimizely.bucketer.Bucketer.find_bucket", return_value="32222" ), self.mock_decision_logger as mock_decision_service_logging: variation_received, _ = self.decision_service.get_experiment_in_group( self.project_config, group, "test_user" @@ -1466,7 +1466,7 @@ def test_get_experiment_in_group__returns_none_if_user_not_in_group(self): group = self.project_config.get_group("19228") with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value=[None, []] + "optimizely.bucketer.Bucketer.find_bucket", return_value=None ), self.mock_decision_logger as mock_decision_service_logging: variation_received, _ = self.decision_service.get_experiment_in_group( self.project_config, group, "test_user" From 83aa04597823799f181ddf7b7ea8a09c951da83d Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 29 Jan 2021 15:26:32 +0500 Subject: [PATCH 17/20] address comments --- optimizely/decision_service.py | 14 ++++---------- optimizely/optimizely.py | 6 +----- tests/test_decision_service.py | 4 ++-- tests/test_user_context.py | 8 ++++---- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 87da4283a..b788b3a7a 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -137,7 +137,6 @@ def get_forced_variation(self, project_config, experiment_key, user_id): if user_id not in self.forced_variation_map: message = 'User "%s" is not in the forced variation map.' % user_id self.logger.debug(message) - decide_reasons.append(message) return None, decide_reasons experiment = project_config.get_experiment_from_key(experiment_key) @@ -152,14 +151,12 @@ def get_forced_variation(self, project_config, experiment_key, user_id): self.logger.debug( message ) - decide_reasons.append(message) return None, decide_reasons variation_id = experiment_to_variation_map.get(experiment.id) if variation_id is None: message = 'No variation mapped to experiment "%s" in the forced variation map.' % experiment_key self.logger.debug(message) - decide_reasons.append(message) return None, decide_reasons variation = project_config.get_variation_from_id(experiment_key, variation_id) @@ -206,9 +203,8 @@ def get_stored_variation(self, project_config, experiment, user_profile): user_profile: UserProfile object representing the user's profile. Returns: - Variation if available. None otherwise And an array of log messages representing decision making. + Variation if available. None otherwise. """ - decide_reasons = [] user_id = user_profile.user_id variation_id = user_profile.get_variation_for_experiment(experiment.id) @@ -220,13 +216,12 @@ def get_stored_variation(self, project_config, experiment, user_profile): self.logger.info( message ) - decide_reasons.append(message) - return variation, decide_reasons + return variation - return None, decide_reasons + return None def get_variation( - self, project_config, experiment, user_id, attributes, ignore_user_profile=False, decide_options=[] + self, project_config, experiment, user_id, attributes, ignore_user_profile=False ): """ Top-level function to help determine variation user should be put in. @@ -242,7 +237,6 @@ def get_variation( user_id: ID for user. attributes: Dict representing user attributes. ignore_user_profile: True to ignore the user profile lookup. Defaults to False. - decideOptions: Options to customize evaluation. Returns: Variation user should see. None if user is not in experiment or experiment is not running diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index e805a7be2..5de8527d0 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1067,9 +1067,7 @@ def _decide(self, user_context, key, decide_options=None): all_variables[variable_key] = actual_value - should_include_reasons = False - if OptimizelyDecideOption.INCLUDE_REASONS in decide_options: - should_include_reasons = True + should_include_reasons = OptimizelyDecideOption.INCLUDE_REASONS in decide_options # Send notification self.notification_center.send_notifications( @@ -1114,10 +1112,8 @@ def _decide_all(self, user_context, decide_options=None): return {} config = self.config_manager.get_config() - reasons = [] if not config: self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide')) - reasons.append(OptimizelyDecisionMessage.SDK_NOT_READY) return {} keys = [] diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index c664d7466..b7d2f37c5 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -406,7 +406,7 @@ def test_get_stored_variation__stored_decision_available(self): with mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging: - variation, _ = self.decision_service.get_stored_variation( + variation = self.decision_service.get_stored_variation( self.project_config, experiment, profile ) self.assertEqual( @@ -423,7 +423,7 @@ def test_get_stored_variation__no_stored_decision_available(self): experiment = self.project_config.get_experiment_from_key("test_experiment") profile = user_profile.UserProfile("test_user") - variation, _ = self.decision_service.get_stored_variation( + variation = self.decision_service.get_stored_variation( self.project_config, experiment, profile ) self.assertIsNone( diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 39be39e6a..14508c7f1 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -127,7 +127,7 @@ def test_decide__SDK_not_ready(self): def test_decide__invalid_flag_key(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - user_context = opt_obj.create_user_context('test_user') + user_context = opt_obj.create_user_context('test_user', {'some-key': 'some-value'}) expected = OptimizelyDecision( variation_key=None, @@ -186,7 +186,7 @@ def test_decide__feature_test(self): 'optimizely.optimizely.Optimizely._send_impression_event' ) as mock_send_event: - user_context = opt_obj.create_user_context('test_user') + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) actual = user_context.decide('test_feature_in_experiment') expected_variables = { @@ -215,7 +215,7 @@ def test_decide__feature_test(self): enums.NotificationTypes.DECISION, 'flag', 'test_user', - {}, + {'browser': 'chrome'}, { 'flag_key': expected.flag_key, 'enabled': expected.enabled, @@ -240,7 +240,7 @@ def test_decide__feature_test(self): 'feature-test', expected.enabled, 'test_user', - {} + {'browser': 'chrome'} ) def test_decide__feature_test__send_flag_decision_false(self): From d355150c10337f15a9b82fde843d950bfd96e089 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 29 Jan 2021 18:43:35 +0500 Subject: [PATCH 18/20] tests: decide --- optimizely/decision_service.py | 2 +- tests/test_decision_service.py | 8 +- tests/test_user_context.py | 695 +++++++++++++++++++++++++++++++++ 3 files changed, 700 insertions(+), 5 deletions(-) diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index b788b3a7a..200dee6ca 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -273,7 +273,7 @@ def get_variation( if validator.is_user_profile_valid(retrieved_profile): user_profile = UserProfile(**retrieved_profile) - variation, reasons_received = self.get_stored_variation(project_config, experiment, user_profile) + variation = self.get_stored_variation(project_config, experiment, user_profile) decide_reasons += reasons_received if variation: message = 'Returning previously activated variation ID "{}" of experiment ' \ diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index b7d2f37c5..f4023d0a4 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -478,7 +478,7 @@ def test_get_variation__bucketing_id_provided(self): return_value=[None, []], ), mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=[None, []], + return_value=None, ), mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), mock.patch( @@ -545,7 +545,7 @@ def test_get_variation__user_has_stored_decision(self): return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=[entities.Variation("111128", "control"), []], + return_value=entities.Variation("111128", "control"), ) as mock_get_stored_variation, mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions" ) as mock_audience_check, mock.patch( @@ -597,7 +597,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=[None, []], + return_value=None, ) as mock_get_stored_variation, mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, mock.patch( @@ -706,7 +706,7 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", - return_value=[None, []], + return_value=None, ) as mock_get_stored_variation, mock.patch( "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[False, []] ) as mock_audience_check, mock.patch( diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 14508c7f1..ed85c2028 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -19,6 +19,7 @@ from . import base from optimizely import optimizely, decision_service from optimizely.optimizely_user_context import OptimizelyUserContext +from optimizely.user_profile import UserProfileService class UserContextTest(base.BaseTest): @@ -418,3 +419,697 @@ def test_decide_feature_rollout__send_flag_decision_false(self): # assert event count self.assertEqual(0, mock_send_event.call_count) + + def test_decide_feature_null_variation(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = None + mock_variation = None + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment') + + expected_variables = { + 'is_working': True, + 'environment': 'devel', + 'cost': 10.99, + 'count': 999, + 'variable_without_usage': 45, + 'object': {"test": 12}, + 'true_object': {"true_test": 23.54} + } + + expected = OptimizelyDecision( + variation_key=None, + rule_key=None, + enabled=False, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': True, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(1, mock_send_event.call_count) + + # assert event payload + mock_send_event.assert_called_with( + project_config, + mock_experiment, + mock_variation, + expected.flag_key, + '', + 'rollout', + expected.enabled, + 'test_user', + {'browser': 'chrome'} + ) + + def test_decide_feature_null_variation__send_flag_decision_false(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + project_config.send_flag_decisions = False + + mock_experiment = None + mock_variation = None + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment') + + expected_variables = { + 'is_working': True, + 'environment': 'devel', + 'cost': 10.99, + 'count': 999, + 'variable_without_usage': 45, + 'object': {"test": 12}, + 'true_object': {"true_test": 23.54} + } + + expected = OptimizelyDecision( + variation_key=None, + rule_key=None, + enabled=False, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': False, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(0, mock_send_event.call_count) + + def test_decide__option__disable_decision_event(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment', ['DISABLE_DECISION_EVENT']) + + expected_variables = { + 'is_working': True, + 'environment': 'staging', + 'cost': 10.02, + 'count': 4243, + 'variable_without_usage': 45, + 'object': {"test": 123}, + 'true_object': {"true_test": 1.4} + } + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': False, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(0, mock_send_event.call_count) + + def test_decide__default_option__disable_decision_event(self): + opt_obj = optimizely.Optimizely( + datafile=json.dumps(self.config_dict_with_features), + default_decide_options=['DISABLE_DECISION_EVENT'] + ) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment') + + expected_variables = { + 'is_working': True, + 'environment': 'staging', + 'cost': 10.02, + 'count': 4243, + 'variable_without_usage': 45, + 'object': {"test": 123}, + 'true_object': {"true_test": 1.4} + } + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': False, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(0, mock_send_event.call_count) + + def test_decide__option__exclude_variables(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment', ['EXCLUDE_VARIABLES']) + + expected_variables = {} + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': True, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(1, mock_send_event.call_count) + + # assert event payload + mock_send_event.assert_called_with( + project_config, + mock_experiment, + mock_variation, + expected.flag_key, + expected.rule_key, + 'feature-test', + expected.enabled, + 'test_user', + {'browser': 'chrome'} + ) + + def test_decide__option__include_reasons__feature_test(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + + expected_reasons = [ + 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.' + ] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide__option__include_reasons__feature_rollout(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_attributes = {'test_attribute': 'test_value_1'} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + + expected_reasons = [ + 'Evaluating audiences for rule 1: ["11154"].', + 'Audiences for rule 1 collectively evaluated to TRUE.', + 'User "test_user" meets audience conditions for targeting rule 1.', + 'User "test_user" is in the traffic group of targeting rule 1.' + ] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide__option__enabled_flags_only(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + expected_experiment = project_config.get_experiment_from_key('211127') + expected_var = project_config.get_variation_from_key('211127', '211229') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(expected_experiment, expected_var, + enums.DecisionSources.ROLLOUT), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_attributes = {'test_attribute': 'test_value_1'} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout', 'ENABLED_FLAGS_ONLY') + + expected_variables = { + 'is_running': False, + 'message': 'Hello', + 'price': 99.99, + 'count': 999, + 'object': {"field": 1} + } + + expected = OptimizelyDecision( + variation_key='211229', + rule_key='211127', + enabled=False, + variables=expected_variables, + flag_key='test_feature_in_rollout', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification count + self.assertEqual(1, mock_broadcast_decision.call_count) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + user_attributes, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': True, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(1, mock_send_event.call_count) + + # assert event payload + mock_send_event.assert_called_with( + project_config, + expected_experiment, + expected_var, + expected.flag_key, + expected.rule_key, + 'rollout', + expected.enabled, + 'test_user', + user_attributes + ) + + def test_decide__default_options__with__options(self): + opt_obj = optimizely.Optimizely( + datafile=json.dumps(self.config_dict_with_features), + default_decide_options=['DISABLE_DECISION_EVENT'] + ) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST), []), + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + + user_context = opt_obj.create_user_context('test_user', {'browser': 'chrome'}) + actual = user_context.decide('test_feature_in_experiment', ['EXCLUDE_VARIABLES']) + + expected_variables = {} + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {'browser': 'chrome'}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': False, + 'variables': expected.variables, + }, + ) + + # assert event count + self.assertEqual(0, mock_send_event.call_count) + + def test_decide_for_keys(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + + mocked_decision_1 = OptimizelyDecision(flag_key='test_feature_in_experiment', enabled=True) + mocked_decision_2 = OptimizelyDecision(flag_key='test_feature_in_rollout', enabled=False) + + def side_effect(*args, **kwargs): + flag = args[1] + if flag == 'test_feature_in_experiment': + return mocked_decision_1 + else: + return mocked_decision_2 + + with mock.patch( + 'optimizely.optimizely.Optimizely._decide', side_effect=side_effect + ) as mock_decide, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext._clone', + return_value=user_context + ): + + flags = ['test_feature_in_rollout', 'test_feature_in_experiment'] + options = [] + decisions = user_context.decide_for_keys(flags, options) + + self.assertEqual(2, len(decisions)) + + mock_decide.assert_any_call( + user_context, + 'test_feature_in_experiment', + options + ) + + mock_decide.assert_any_call( + user_context, + 'test_feature_in_rollout', + options + ) + + self.assertEqual(mocked_decision_1, decisions['test_feature_in_experiment']) + self.assertEqual(mocked_decision_2, decisions['test_feature_in_rollout']) + + def test_decide_for_keys__option__enabled_flags_only(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + + mocked_decision_1 = OptimizelyDecision(flag_key='test_feature_in_experiment', enabled=True) + mocked_decision_2 = OptimizelyDecision(flag_key='test_feature_in_rollout', enabled=False) + + def side_effect(*args, **kwargs): + flag = args[1] + if flag == 'test_feature_in_experiment': + return mocked_decision_1 + else: + return mocked_decision_2 + + with mock.patch( + 'optimizely.optimizely.Optimizely._decide', side_effect=side_effect + ) as mock_decide, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext._clone', + return_value=user_context + ): + + flags = ['test_feature_in_rollout', 'test_feature_in_experiment'] + options = ['ENABLED_FLAGS_ONLY'] + decisions = user_context.decide_for_keys(flags, options) + + self.assertEqual(1, len(decisions)) + + mock_decide.assert_any_call( + user_context, + 'test_feature_in_experiment', + options + ) + + mock_decide.assert_any_call( + user_context, + 'test_feature_in_rollout', + options + ) + + self.assertEqual(mocked_decision_1, decisions['test_feature_in_experiment']) + + def test_decide_for_keys__default_options__with__options(self): + opt_obj = optimizely.Optimizely( + datafile=json.dumps(self.config_dict_with_features), + default_decide_options=['ENABLED_FLAGS_ONLY'] + ) + + user_context = opt_obj.create_user_context('test_user') + + with mock.patch( + 'optimizely.optimizely.Optimizely._decide' + ) as mock_decide, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext._clone', + return_value=user_context + ): + + flags = ['test_feature_in_experiment'] + options = ['EXCLUDE_VARIABLES'] + user_context.decide_for_keys(flags, options) + + mock_decide.assert_called_with( + user_context, + 'test_feature_in_experiment', + ['EXCLUDE_VARIABLES', 'ENABLED_FLAGS_ONLY'] + ) + + def test_decide_for_all(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context('test_user') + + with mock.patch( + 'optimizely.optimizely.Optimizely._decide_for_keys', return_value='response from decide_for_keys' + ) as mock_decide, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext._clone', + return_value=user_context + ): + + options = ['DISABLE_DECISION_EVENT'] + decisions = user_context.decide_all(options) + + mock_decide.assert_called_with( + user_context, + [ + 'test_feature_in_experiment', + 'test_feature_in_rollout', + 'test_feature_in_group', + 'test_feature_in_experiment_and_rollout' + ], + options + ) + + self.assertEqual('response from decide_for_keys', decisions) + + def test_decide_options_bypass_UPS(self): + user_id = 'test_user' + + lookup_profile = { + 'user_id': user_id, + 'experiment_bucket_map': { + '111127': { + 'variation_id': '111128' + } + } + } + + save_profile = [] + + class Ups(UserProfileService): + + def lookup(self, user_id): + return lookup_profile + + def save(self, user_profile): + print(user_profile) + save_profile.append(user_profile) + + ups = Ups() + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) + project_config = opt_obj.config_manager.get_config() + + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + with mock.patch( + 'optimizely.bucketer.Bucketer.bucket', + return_value=(mock_variation, []), + ), mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ): + user_context = opt_obj.create_user_context(user_id) + options = [ + 'IGNORE_USER_PROFILE_SERVICE' + ] + + actual = user_context.decide('test_feature_in_experiment', options) + + expected_variables = { + 'is_working': True, + 'environment': 'staging', + 'cost': 10.02, + 'count': 4243, + 'variable_without_usage': 45, + 'object': {"test": 123}, + 'true_object': {"true_test": 1.4} + } + + expected = OptimizelyDecision( + variation_key='variation', + rule_key='test_experiment', + enabled=True, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context + ) + + self.compare_opt_decisions(expected, actual) + + self.assertEqual([], save_profile) From 675408a4b8cc1ddc4c01d6a96bef8c48f6f86e9c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 29 Jan 2021 19:15:01 +0500 Subject: [PATCH 19/20] fix: import --- tests/test_optimizely.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 16920ffef..1c21dc6a0 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -26,7 +26,7 @@ from optimizely import optimizely_config from optimizely import project_config from optimizely import version -from optimizely.decision.decide_option import DecideOption +from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums from . import base From c7c93ef36843fe497251609bd04acfaf1a28fc8b Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 1 Feb 2021 16:02:50 +0500 Subject: [PATCH 20/20] tests: Add reasons tests --- optimizely/decision_service.py | 1 - optimizely/entities.py | 8 +- optimizely/optimizely.py | 10 +-- tests/base.py | 2 +- tests/test_user_context.py | 134 ++++++++++++++++++++++++++++++++- 5 files changed, 146 insertions(+), 9 deletions(-) diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 200dee6ca..52e9d02bb 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -274,7 +274,6 @@ def get_variation( if validator.is_user_profile_valid(retrieved_profile): user_profile = UserProfile(**retrieved_profile) variation = self.get_stored_variation(project_config, experiment, user_profile) - decide_reasons += reasons_received if variation: message = 'Returning previously activated variation ID "{}" of experiment ' \ '"{}" for user "{}" from user profile.'.format(variation, experiment, user_id) diff --git a/optimizely/entities.py b/optimizely/entities.py index c182c4dae..88cd49c4f 100644 --- a/optimizely/entities.py +++ b/optimizely/entities.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, 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 @@ -71,6 +71,9 @@ def get_audience_conditions_or_ids(self): """ Returns audienceConditions if present, otherwise audienceIds. """ return self.audienceConditions if self.audienceConditions is not None else self.audienceIds + def __str__(self): + return self.key + class FeatureFlag(BaseEntity): def __init__(self, id, key, experimentIds, rolloutId, variables, groupId=None, **kwargs): @@ -122,3 +125,6 @@ def __init__(self, id, key, featureEnabled=False, variables=None, **kwargs): self.key = key self.featureEnabled = featureEnabled self.variables = variables or [] + + def __str__(self): + return self.key diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 5de8527d0..1383674a2 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -1142,15 +1142,15 @@ def _decide_for_keys(self, user_context, keys, decide_options=None): return {} # merge decide_options and default_decide_options + merged_decide_options = [] if isinstance(decide_options, list): - decide_options += self.default_decide_options + merged_decide_options = decide_options[:] + merged_decide_options += self.default_decide_options else: self.logger.debug('Provided decide options is not an array. Using default decide options.') - decide_options = self.default_decide_options + merged_decide_options = self.default_decide_options - enabled_flags_only = False - if decide_options is not None: - enabled_flags_only = OptimizelyDecideOption.ENABLED_FLAGS_ONLY in decide_options + enabled_flags_only = OptimizelyDecideOption.ENABLED_FLAGS_ONLY in merged_decide_options decisions = {} for key in keys: diff --git a/tests/base.py b/tests/base.py index 88d5b73ff..254be7c53 100644 --- a/tests/base.py +++ b/tests/base.py @@ -135,7 +135,7 @@ def setUp(self, config_dict='config_dict'): { 'key': 'test_experiment', 'status': 'Running', - 'forcedVariations': {}, + 'forcedVariations': {'user_1': 'control'}, 'layerId': '111182', 'audienceIds': [], 'trafficAllocation': [ diff --git a/tests/test_user_context.py b/tests/test_user_context.py index ed85c2028..abc18a87d 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -1016,7 +1016,7 @@ def test_decide_for_keys__default_options__with__options(self): mock_decide.assert_called_with( user_context, 'test_feature_in_experiment', - ['EXCLUDE_VARIABLES', 'ENABLED_FLAGS_ONLY'] + ['EXCLUDE_VARIABLES'] ) def test_decide_for_all(self): @@ -1113,3 +1113,135 @@ def save(self, user_profile): self.compare_opt_decisions(expected, actual) self.assertEqual([], save_profile) + + def test_decide_reasons__hit_everyone_else_rule__fails_bucketing(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_attributes = {} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + + expected_reasons = [ + 'Evaluating audiences for rule 1: ["11154"].', + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule 1.', + 'Evaluating audiences for rule 2: ["11159"].', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule 2.', + 'Evaluating audiences for rule Everyone Else: [].', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'Bucketed into an empty traffic range. Returning nil.' + ] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide_reasons__hit_everyone_else_rule(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_attributes = {} + user_context = opt_obj.create_user_context('abcde', user_attributes) + actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + + expected_reasons = [ + 'Evaluating audiences for rule 1: ["11154"].', + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "abcde" does not meet conditions for targeting rule 1.', + 'Evaluating audiences for rule 2: ["11159"].', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "abcde" does not meet conditions for targeting rule 2.', + 'Evaluating audiences for rule Everyone Else: [].', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "abcde" meets conditions for targeting rule "Everyone Else".' + ] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide_reasons__hit_rule2__fails_bucketing(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_attributes = {'test_attribute': 'test_value_2'} + user_context = opt_obj.create_user_context('test_user', user_attributes) + actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + + expected_reasons = [ + 'Evaluating audiences for rule 1: ["11154"].', + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule 1.', + 'Evaluating audiences for rule 2: ["11159"].', + 'Audiences for rule 2 collectively evaluated to TRUE.', + 'User "test_user" meets audience conditions for targeting rule 2.', + 'Bucketed into an empty traffic range. Returning nil.', + 'User "test_user" is not in the traffic group for targeting rule 2. Checking "Everyone Else" rule now.', + 'Evaluating audiences for rule Everyone Else: [].', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'Bucketed into an empty traffic range. Returning nil.' + ] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide_reasons__hit_user_profile_service(self): + user_id = 'test_user' + + lookup_profile = { + 'user_id': user_id, + 'experiment_bucket_map': { + '111127': { + 'variation_id': '111128' + } + } + } + + save_profile = [] + + class Ups(UserProfileService): + + def lookup(self, user_id): + return lookup_profile + + def save(self, user_profile): + print(user_profile) + save_profile.append(user_profile) + + ups = Ups() + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), user_profile_service=ups) + + user_context = opt_obj.create_user_context(user_id) + options = ['INCLUDE_REASONS'] + + actual = user_context.decide('test_feature_in_experiment', options) + + expected_reasons = [('Returning previously activated variation ID "control" of experiment ' + '"test_experiment" for user "test_user" from user profile.')] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide_reasons__forced_variation(self): + user_id = 'test_user' + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context(user_id) + options = ['INCLUDE_REASONS'] + + opt_obj.set_forced_variation('test_experiment', user_id, 'control') + + actual = user_context.decide('test_feature_in_experiment', options) + + expected_reasons = [('Variation "control" is mapped to experiment ' + '"test_experiment" and user "test_user" in the forced variation map')] + + self.assertEquals(expected_reasons, actual.reasons) + + def test_decide_reasons__whitelisted_variation(self): + user_id = 'user_1' + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + user_context = opt_obj.create_user_context(user_id) + options = ['INCLUDE_REASONS'] + + actual = user_context.decide('test_feature_in_experiment', options) + + expected_reasons = ['User "user_1" is forced in variation "control".'] + + self.assertEquals(expected_reasons, actual.reasons)