diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 28275ef..0648952 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -457,7 +457,8 @@ def get_variation( } # Check to see if user has a decision available for the given experiment - if user_profile_tracker is not None and not ignore_user_profile: + # CMAB experiments are excluded from UserProfileService to allow dynamic decisions + if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab: variation = self.get_stored_variation(project_config, experiment, user_profile_tracker.get_user_profile()) if variation: message = f'Returning previously activated variation ID "{variation}" of experiment ' \ @@ -472,6 +473,10 @@ def get_variation( } else: self.logger.warning('User profile has invalid format.') + elif user_profile_tracker is not None and not ignore_user_profile and experiment.cmab: + message = 'User profile service excluded for CMAB experiment to allow dynamic decisions.' + self.logger.info(message) + decide_reasons.append(message) # Check audience conditions audience_conditions = experiment.get_audience_conditions_or_ids() @@ -529,7 +534,8 @@ def get_variation( self.logger.info(message) decide_reasons.append(message) # Store this new decision and return the variation for the user - if user_profile_tracker is not None and not ignore_user_profile: + # CMAB experiments are excluded from UserProfileService + if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab: try: user_profile_tracker.update_user_profile(experiment, variation) except: diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index dbcb743..97356e6 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1074,6 +1074,93 @@ 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_excludes_user_profile_service(self): + """Test that CMAB experiments exclude UserProfileService for both load and save operations.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + id='111150', + key='cmab_experiment', + status='Running', + audienceIds=[], + variations=[entities.Variation('111151', 'variation_1')], + forcedVariations={}, + trafficAllocation=[{'entityId': '111151', 'endOfRange': 10000}], + layerId='111150', + cmab=True + ) + + # Create a mock user profile service + mock_ups = mock.Mock() + mock_ups.lookup.return_value = { + 'user_id': 'test_user', + 'experiment_bucket_map': { + '111150': {'variation_id': '111152'} # Different variation in profile + } + } + + # Create decision service with user profile service + decision_service_with_ups = decision_service.DecisionService( + mock.MagicMock(), + mock_ups, + mock.MagicMock() + ) + + # Mock the CMAB decision to return variation_1 + cmab_decision_result = { + 'error': False, + 'result': {'variation_id': '111151', 'cmab_uuid': 'test-uuid'}, + 'reasons': ['CMAB decision made'] + } + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', + return_value=True), \ + mock.patch.object(self.project_config, 'get_variation_from_id', + return_value=entities.Variation('111151', 'variation_1')), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id', + return_value=('111151', [])), \ + mock.patch.object(decision_service_with_ups, '_get_decision_for_cmab_experiment', + return_value=cmab_decision_result): + + # Create user profile tracker + from optimizely.user_profile import UserProfileTracker + user_profile_tracker = UserProfileTracker('test_user', mock_ups, mock.MagicMock()) + + # Call get_variation with user profile tracker + variation_result = decision_service_with_ups.get_variation( + self.project_config, + cmab_experiment, + user, + user_profile_tracker + ) + + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + reasons = variation_result['reasons'] + + # Verify that UPS was NOT used to load the saved variation (111152) + # Instead, CMAB decision returned variation_1 (111151) + self.assertEqual('variation_1', variation.key) + self.assertEqual('111151', variation.id) + self.assertEqual('test-uuid', cmab_uuid) + + # Verify the exclusion reason is in the decision reasons + self.assertIn('User profile service excluded for CMAB experiment to allow dynamic decisions.', reasons) + + # Verify UPS lookup was NOT called (CMAB should bypass UPS load) + mock_ups.lookup.assert_not_called() + + # Verify UPS save was NOT called (CMAB should bypass UPS save) + mock_ups.save.assert_not_called() + class FeatureFlagDecisionTests(base.BaseTest): def setUp(self):