From caeaffe403cfe683aa3858a250214ef6ddffb5cf Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 26 Dec 2025 22:09:25 +0600 Subject: [PATCH 1/4] Python: Exclude CMAB experiments from user profile updates and add related tests --- optimizely/decision_service.py | 15 ++- tests/test_decision_service.py | 174 +++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 4 deletions(-) diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 28275ef6..1941aa1b 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -529,11 +529,18 @@ def get_variation( self.logger.info(message) decide_reasons.append(message) # Store this new decision and return the variation for the user + # CMAB experiments are excluded from user profile storage to allow dynamic decision-making if user_profile_tracker is not None and not ignore_user_profile: - try: - user_profile_tracker.update_user_profile(experiment, variation) - except: - self.logger.exception(f'Unable to save user profile for user "{user_id}".') + if not experiment.cmab: + try: + user_profile_tracker.update_user_profile(experiment, variation) + except: + self.logger.exception(f'Unable to save user profile for user "{user_id}".') + else: + self.logger.debug( + f'Skipping user profile update for CMAB experiment "{experiment.key}". ' + f'CMAB decisions are dynamic and not stored for sticky bucketing.' + ) return { 'cmab_uuid': cmab_uuid, 'error': False, diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index dbcb7436..66c00d55 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1074,6 +1074,180 @@ def test_get_variation_cmab_experiment_with_whitelisted_variation(self): mock_bucket.assert_not_called() mock_cmab_decision.assert_not_called() + def test_get_variation_cmab_experiment_does_not_save_user_profile(self): + """Test that CMAB experiments do not save bucketing decisions to user profile.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a user profile service and tracker + user_profile_service = user_profile.UserProfileService() + user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [ + entities.Variation('111151', 'variation_1'), + entities.Variation('111152', 'variation_2') + ], + [ + {'entityId': '111151', 'endOfRange': 5000}, + {'entityId': '111152', 'endOfRange': 10000} + ], + cmab={'trafficAllocation': 5000} + ) + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id', + return_value=['$', []]), \ + mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \ + mock.patch.object(self.project_config, 'get_variation_from_id', + return_value=entities.Variation('111151', 'variation_1')), \ + mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \ + mock.patch.object(self.decision_service, 'logger') as mock_logger: + + # Configure CMAB service to return a decision + mock_cmab_service.get_decision.return_value = ( + { + 'variation_id': '111151', + 'cmab_uuid': 'test-cmab-uuid-123' + }, + [] # reasons list + ) + + # Call get_variation with the CMAB experiment and user profile tracker + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + user_profile_tracker + ) + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + + # Verify the variation and cmab_uuid are returned + self.assertEqual(entities.Variation('111151', 'variation_1'), variation) + self.assertEqual('test-cmab-uuid-123', cmab_uuid) + + # Verify user profile was NOT updated for CMAB experiment + mock_update_profile.assert_not_called() + + # Verify debug log was called to explain CMAB exclusion + mock_logger.debug.assert_called_with( + 'Skipping user profile update for CMAB experiment "cmab_experiment". ' + 'CMAB decisions are dynamic and not stored for sticky bucketing.' + ) + + def test_get_variation_standard_experiment_saves_user_profile(self): + """Test that standard (non-CMAB) experiments DO save bucketing decisions to user profile.""" + + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a user profile service and tracker + user_profile_service = user_profile.UserProfileService() + user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service) + + # Get a standard (non-CMAB) experiment + experiment = self.project_config.get_experiment_from_key("test_experiment") + + with mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', + return_value=[None, []]), \ + mock.patch('optimizely.decision_service.DecisionService.get_stored_variation', + return_value=None), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', + return_value=[True, []]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket', + return_value=[entities.Variation("111129", "variation"), []]), \ + mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile: + + # Call get_variation with standard experiment and user profile tracker + variation_result = self.decision_service.get_variation( + self.project_config, + experiment, + user, + user_profile_tracker + ) + variation = variation_result['variation'] + + # Verify variation was returned + self.assertEqual(entities.Variation("111129", "variation"), variation) + + # Verify user profile WAS updated for standard experiment + mock_update_profile.assert_called_once_with(experiment, variation) + + def test_get_variation_cmab_experiment_with_ignore_ups_option(self): + """Test that CMAB experiments with IGNORE_USER_PROFILE_SERVICE option don't attempt profile update.""" + + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a user profile tracker + user_profile_service = user_profile.UserProfileService() + user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], + {}, + [entities.Variation('111151', 'variation_1')], + [{'entityId': '111151', 'endOfRange': 10000}], + cmab={'trafficAllocation': 5000} + ) + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id', + return_value=['$', []]), \ + mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \ + mock.patch.object(self.project_config, 'get_variation_from_id', + return_value=entities.Variation('111151', 'variation_1')), \ + mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \ + mock.patch.object(self.decision_service, 'logger'): + + mock_cmab_service.get_decision.return_value = ( + {'variation_id': '111151', 'cmab_uuid': 'test-uuid'}, + [] + ) + + # Call with IGNORE_USER_PROFILE_SERVICE option + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + user_profile_tracker, + [], + options=['IGNORE_USER_PROFILE_SERVICE'] + ) + + # Verify variation returned but profile not updated + self.assertIsNotNone(variation_result['variation']) + mock_update_profile.assert_not_called() + class FeatureFlagDecisionTests(base.BaseTest): def setUp(self): From 022ccb25d71eaa1021adf028480c333b9fa5e7f7 Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 26 Dec 2025 22:14:33 +0600 Subject: [PATCH 2/4] linting fix --- tests/test_decision_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 66c00d55..c7674089 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1116,7 +1116,7 @@ def test_get_variation_cmab_experiment_does_not_save_user_profile(self): mock.patch.object(self.project_config, 'get_variation_from_id', return_value=entities.Variation('111151', 'variation_1')), \ mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \ - mock.patch.object(self.decision_service, 'logger') as mock_logger: + mock.patch.object(self.decision_service, 'logger') as mock_logger: # Configure CMAB service to return a decision mock_cmab_service.get_decision.return_value = ( @@ -1175,7 +1175,7 @@ def test_get_variation_standard_experiment_saves_user_profile(self): return_value=[True, []]), \ mock.patch('optimizely.bucketer.Bucketer.bucket', return_value=[entities.Variation("111129", "variation"), []]), \ - mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile: + mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile: # Call get_variation with standard experiment and user profile tracker variation_result = self.decision_service.get_variation( @@ -1227,7 +1227,7 @@ def test_get_variation_cmab_experiment_with_ignore_ups_option(self): mock.patch.object(self.project_config, 'get_variation_from_id', return_value=entities.Variation('111151', 'variation_1')), \ mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \ - mock.patch.object(self.decision_service, 'logger'): + mock.patch.object(self.decision_service, 'logger'): mock_cmab_service.get_decision.return_value = ( {'variation_id': '111151', 'cmab_uuid': 'test-uuid'}, From 343bdcf218e5f036734fd861111a0008e62d6220 Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 26 Dec 2025 22:45:00 +0600 Subject: [PATCH 3/4] chore: trigger tests From 3cbea1f13e1f87d8c2f45c7befa5dcf520fca4af Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Sat, 27 Dec 2025 03:17:52 +0600 Subject: [PATCH 4/4] Python: Exclude CMAB experiments from user profile updates and update related tests --- optimizely/decision_service.py | 25 +++++++++++++------------ tests/test_decision_service.py | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 1941aa1b..9d2d064c 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -413,7 +413,15 @@ def get_variation( - 'error': Boolean indicating if an error occurred during the decision process. """ user_id = user_context.user_id - if options: + + if experiment.cmab: + # CMAB experiments are excluded from user profile storage to allow dynamic decision-making + ignore_user_profile = True + self.logger.debug( + f'Skipping user profile service for CMAB experiment "{experiment.key}". ' + f'CMAB decisions are dynamic and not stored for sticky bucketing.' + ) + elif options: ignore_user_profile = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in options else: ignore_user_profile = False @@ -529,18 +537,11 @@ def get_variation( self.logger.info(message) decide_reasons.append(message) # Store this new decision and return the variation for the user - # CMAB experiments are excluded from user profile storage to allow dynamic decision-making if user_profile_tracker is not None and not ignore_user_profile: - if not experiment.cmab: - try: - user_profile_tracker.update_user_profile(experiment, variation) - except: - self.logger.exception(f'Unable to save user profile for user "{user_id}".') - else: - self.logger.debug( - f'Skipping user profile update for CMAB experiment "{experiment.key}". ' - f'CMAB decisions are dynamic and not stored for sticky bucketing.' - ) + try: + user_profile_tracker.update_user_profile(experiment, variation) + except: + self.logger.exception(f'Unable to save user profile for user "{user_id}".') return { 'cmab_uuid': cmab_uuid, 'error': False, diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index c7674089..e9224b8e 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1145,8 +1145,8 @@ def test_get_variation_cmab_experiment_does_not_save_user_profile(self): mock_update_profile.assert_not_called() # Verify debug log was called to explain CMAB exclusion - mock_logger.debug.assert_called_with( - 'Skipping user profile update for CMAB experiment "cmab_experiment". ' + mock_logger.debug.assert_any_call( + 'Skipping user profile service for CMAB experiment "cmab_experiment". ' 'CMAB decisions are dynamic and not stored for sticky bucketing.' )