Skip to content

Commit d2cb775

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

2 files changed

Lines changed: 191 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+
# Skip UPS lookup for CMAB experiments as they require 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 experiment.cmab:
477+
message = f'Skipping User Profile Service for CMAB experiment "{experiment.key}".'
478+
self.logger.debug(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+
# Skip UPS update for CMAB experiments as they require dynamic decisions
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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,189 @@ 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_excludes_ups_lookup(self):
1078+
"""Test that CMAB experiments skip User Profile Service lookup."""
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+
'111150',
1091+
'cmab_experiment',
1092+
'Running',
1093+
'111150',
1094+
[], # No audience IDs
1095+
{},
1096+
[
1097+
entities.Variation('111151', 'variation_1'),
1098+
entities.Variation('111152', 'variation_2')
1099+
],
1100+
[
1101+
{'entityId': '111151', 'endOfRange': 5000},
1102+
{'entityId': '111152', 'endOfRange': 10000}
1103+
],
1104+
cmab={'trafficAllocation': 5000}
1105+
)
1106+
1107+
# Create user profile service and tracker
1108+
user_profile_service = user_profile.UserProfileService()
1109+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1110+
1111+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1112+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1113+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1114+
return_value=['$', []]), \
1115+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1116+
mock.patch.object(self.decision_service, 'get_stored_variation') as mock_get_stored_variation, \
1117+
mock.patch.object(self.decision_service, 'logger') as mock_logger, \
1118+
mock.patch.object(self.project_config, 'get_variation_from_id',
1119+
return_value=entities.Variation('111151', 'variation_1')):
1120+
1121+
# Setup CMAB service to return a decision
1122+
mock_cmab_service.get_decision.return_value = (
1123+
{'variation_id': '111151', 'cmab_uuid': 'test-cmab-uuid-123'},
1124+
[]
1125+
)
1126+
1127+
# Call get_variation with the CMAB experiment and UPS tracker
1128+
variation_result = self.decision_service.get_variation(
1129+
self.project_config,
1130+
cmab_experiment,
1131+
user,
1132+
user_profile_tracker
1133+
)
1134+
variation = variation_result['variation']
1135+
reasons = variation_result['reasons']
1136+
1137+
# Verify we get a variation
1138+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1139+
1140+
# Verify UPS lookup was NOT called for CMAB
1141+
mock_get_stored_variation.assert_not_called()
1142+
1143+
# Verify decision reason includes UPS exclusion message
1144+
self.assertIn('Skipping User Profile Service for CMAB experiment "cmab_experiment".', reasons)
1145+
1146+
# Verify logger was called with UPS exclusion message
1147+
mock_logger.debug.assert_any_call('Skipping User Profile Service for CMAB experiment "cmab_experiment".')
1148+
1149+
def test_get_variation_cmab_excludes_ups_update(self):
1150+
"""Test that CMAB experiments skip User Profile Service updates."""
1151+
1152+
# Create a user context
1153+
user = optimizely_user_context.OptimizelyUserContext(
1154+
optimizely_client=None,
1155+
logger=None,
1156+
user_id="test_user",
1157+
user_attributes={}
1158+
)
1159+
1160+
# Create a CMAB experiment
1161+
cmab_experiment = entities.Experiment(
1162+
'111150',
1163+
'cmab_experiment',
1164+
'Running',
1165+
'111150',
1166+
[], # No audience IDs
1167+
{},
1168+
[
1169+
entities.Variation('111151', 'variation_1'),
1170+
entities.Variation('111152', 'variation_2')
1171+
],
1172+
[
1173+
{'entityId': '111151', 'endOfRange': 5000},
1174+
{'entityId': '111152', 'endOfRange': 10000}
1175+
],
1176+
cmab={'trafficAllocation': 5000}
1177+
)
1178+
1179+
# Create user profile service and tracker
1180+
user_profile_service = user_profile.UserProfileService()
1181+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1182+
1183+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1184+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1185+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1186+
return_value=['$', []]), \
1187+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1188+
mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \
1189+
mock.patch.object(self.project_config, 'get_variation_from_id',
1190+
return_value=entities.Variation('111151', 'variation_1')):
1191+
1192+
# Setup CMAB service to return a decision
1193+
mock_cmab_service.get_decision.return_value = (
1194+
{'variation_id': '111151', 'cmab_uuid': 'test-cmab-uuid-123'},
1195+
[]
1196+
)
1197+
1198+
# Call get_variation with the CMAB experiment and UPS tracker
1199+
variation_result = self.decision_service.get_variation(
1200+
self.project_config,
1201+
cmab_experiment,
1202+
user,
1203+
user_profile_tracker
1204+
)
1205+
variation = variation_result['variation']
1206+
1207+
# Verify we get a variation
1208+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1209+
1210+
# Verify UPS update was NOT called for CMAB
1211+
mock_update_profile.assert_not_called()
1212+
1213+
def test_get_variation_non_cmab_uses_ups(self):
1214+
"""Test that non-CMAB experiments still use User Profile Service."""
1215+
1216+
# Create a user context
1217+
user = optimizely_user_context.OptimizelyUserContext(
1218+
optimizely_client=None,
1219+
logger=None,
1220+
user_id="test_user",
1221+
user_attributes={}
1222+
)
1223+
1224+
# Create a regular (non-CMAB) experiment
1225+
regular_experiment = self.project_config.get_experiment_from_key("test_experiment")
1226+
1227+
# Create user profile service and tracker
1228+
user_profile_service = user_profile.UserProfileService()
1229+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1230+
1231+
with mock.patch.object(self.decision_service, 'get_forced_variation',
1232+
return_value=[None, []]), \
1233+
mock.patch.object(self.decision_service, 'get_whitelisted_variation',
1234+
return_value=[None, []]), \
1235+
mock.patch.object(self.decision_service, 'get_stored_variation',
1236+
return_value=None) as mock_get_stored_variation, \
1237+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1238+
mock.patch.object(self.decision_service.bucketer, 'bucket',
1239+
return_value=[entities.Variation('211129', 'variation'), []]), \
1240+
mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile:
1241+
1242+
# Call get_variation with regular experiment and UPS tracker
1243+
variation_result = self.decision_service.get_variation(
1244+
self.project_config,
1245+
regular_experiment,
1246+
user,
1247+
user_profile_tracker
1248+
)
1249+
variation = variation_result['variation']
1250+
1251+
# Verify we get a variation
1252+
self.assertIsNotNone(variation)
1253+
1254+
# Verify UPS lookup WAS called for non-CMAB
1255+
mock_get_stored_variation.assert_called_once()
1256+
1257+
# Verify UPS update WAS called for non-CMAB
1258+
mock_update_profile.assert_called_once_with(regular_experiment, variation)
1259+
10771260

10781261
class FeatureFlagDecisionTests(base.BaseTest):
10791262
def setUp(self):

0 commit comments

Comments
 (0)