Skip to content

Commit d41cf68

Browse files
committed
[AI-FSSDK] [FSSDK-12262] Exclude CMAB from UserProfileService
1 parent 88b0644 commit d41cf68

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

optimizely/decision_service.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,8 @@ def get_variation(
457457
}
458458

459459
# Check to see if user has a decision available for the given experiment
460-
if user_profile_tracker is not None and not ignore_user_profile:
460+
# CMAB experiments are excluded from UserProfileService to allow dynamic decisions
461+
if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab:
461462
variation = self.get_stored_variation(project_config, experiment, user_profile_tracker.get_user_profile())
462463
if variation:
463464
message = f'Returning previously activated variation ID "{variation}" of experiment ' \
@@ -472,6 +473,10 @@ def get_variation(
472473
}
473474
else:
474475
self.logger.warning('User profile has invalid format.')
476+
elif user_profile_tracker is not None and not ignore_user_profile and experiment.cmab:
477+
message = 'User profile service excluded for CMAB experiment to allow dynamic decisions.'
478+
self.logger.info(message)
479+
decide_reasons.append(message)
475480

476481
# Check audience conditions
477482
audience_conditions = experiment.get_audience_conditions_or_ids()
@@ -529,7 +534,8 @@ def get_variation(
529534
self.logger.info(message)
530535
decide_reasons.append(message)
531536
# Store this new decision and return the variation for the user
532-
if user_profile_tracker is not None and not ignore_user_profile:
537+
# CMAB experiments are excluded from UserProfileService
538+
if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab:
533539
try:
534540
user_profile_tracker.update_user_profile(experiment, variation)
535541
except:

tests/test_decision_service.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,93 @@ def test_get_variation_cmab_experiment_with_whitelisted_variation(self):
10741074
mock_bucket.assert_not_called()
10751075
mock_cmab_decision.assert_not_called()
10761076

1077+
def test_get_variation_cmab_experiment_excludes_user_profile_service(self):
1078+
"""Test that CMAB experiments exclude UserProfileService for both load and save operations."""
1079+
1080+
# Create a user context
1081+
user = optimizely_user_context.OptimizelyUserContext(
1082+
optimizely_client=None,
1083+
logger=None,
1084+
user_id="test_user",
1085+
user_attributes={}
1086+
)
1087+
1088+
# Create a CMAB experiment
1089+
cmab_experiment = entities.Experiment(
1090+
id='111150',
1091+
key='cmab_experiment',
1092+
status='Running',
1093+
audienceIds=[],
1094+
variations=[entities.Variation('111151', 'variation_1')],
1095+
forcedVariations={},
1096+
trafficAllocation=[{'entityId': '111151', 'endOfRange': 10000}],
1097+
layerId='111150',
1098+
cmab=True
1099+
)
1100+
1101+
# Create a mock user profile service
1102+
mock_ups = mock.Mock()
1103+
mock_ups.lookup.return_value = {
1104+
'user_id': 'test_user',
1105+
'experiment_bucket_map': {
1106+
'111150': {'variation_id': '111152'} # Different variation in profile
1107+
}
1108+
}
1109+
1110+
# Create decision service with user profile service
1111+
decision_service_with_ups = decision_service.DecisionService(
1112+
mock.MagicMock(),
1113+
mock_ups,
1114+
mock.MagicMock()
1115+
)
1116+
1117+
# Mock the CMAB decision to return variation_1
1118+
cmab_decision_result = {
1119+
'error': False,
1120+
'result': {'variation_id': '111151', 'cmab_uuid': 'test-uuid'},
1121+
'reasons': ['CMAB decision made']
1122+
}
1123+
1124+
with mock.patch('optimizely.helpers.experiment.is_experiment_running',
1125+
return_value=True), \
1126+
mock.patch.object(self.project_config, 'get_variation_from_id',
1127+
return_value=entities.Variation('111151', 'variation_1')), \
1128+
mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id',
1129+
return_value=('111151', [])), \
1130+
mock.patch.object(decision_service_with_ups, '_get_decision_for_cmab_experiment',
1131+
return_value=cmab_decision_result):
1132+
1133+
# Create user profile tracker
1134+
from optimizely.user_profile import UserProfileTracker
1135+
user_profile_tracker = UserProfileTracker('test_user', mock_ups, mock.MagicMock())
1136+
1137+
# Call get_variation with user profile tracker
1138+
variation_result = decision_service_with_ups.get_variation(
1139+
self.project_config,
1140+
cmab_experiment,
1141+
user,
1142+
user_profile_tracker
1143+
)
1144+
1145+
variation = variation_result['variation']
1146+
cmab_uuid = variation_result['cmab_uuid']
1147+
reasons = variation_result['reasons']
1148+
1149+
# Verify that UPS was NOT used to load the saved variation (111152)
1150+
# Instead, CMAB decision returned variation_1 (111151)
1151+
self.assertEqual('variation_1', variation.key)
1152+
self.assertEqual('111151', variation.id)
1153+
self.assertEqual('test-uuid', cmab_uuid)
1154+
1155+
# Verify the exclusion reason is in the decision reasons
1156+
self.assertIn('User profile service excluded for CMAB experiment to allow dynamic decisions.', reasons)
1157+
1158+
# Verify UPS lookup was NOT called (CMAB should bypass UPS load)
1159+
mock_ups.lookup.assert_not_called()
1160+
1161+
# Verify UPS save was NOT called (CMAB should bypass UPS save)
1162+
mock_ups.save.assert_not_called()
1163+
10771164

10781165
class FeatureFlagDecisionTests(base.BaseTest):
10791166
def setUp(self):

0 commit comments

Comments
 (0)