Skip to content

Commit 1305f88

Browse files
committed
[AI-FSSDK] [FSSDK-12262] Exclude CMAB from UserProfileService
1 parent c6b2cde commit 1305f88

2 files changed

Lines changed: 188 additions & 2 deletions

File tree

optimizely/decision_service.py

Lines changed: 9 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 UPS to ensure decisions are always computed dynamically
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 ' \
@@ -473,6 +474,11 @@ def get_variation(
473474
else:
474475
self.logger.warning('User profile has invalid format.')
475476

477+
if experiment.cmab and user_profile_tracker is not None and not ignore_user_profile:
478+
message = f'Skipping UPS lookup and save for CMAB experiment "{experiment.key}".'
479+
self.logger.info(message)
480+
decide_reasons.append(message)
481+
476482
# Check audience conditions
477483
audience_conditions = experiment.get_audience_conditions_or_ids()
478484
user_meets_audience_conditions, reasons_received = audience_helper.does_user_meet_audience_conditions(
@@ -529,7 +535,8 @@ def get_variation(
529535
self.logger.info(message)
530536
decide_reasons.append(message)
531537
# Store this new decision and return the variation for the user
532-
if user_profile_tracker is not None and not ignore_user_profile:
538+
# CMAB experiments are excluded from UPS to ensure decisions remain dynamic
539+
if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab:
533540
try:
534541
user_profile_tracker.update_user_profile(experiment, variation)
535542
except:

tests/test_decision_service.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,185 @@ 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_skips_ups_lookup(self):
1078+
"""Test that get_variation skips UPS lookup for CMAB experiments."""
1079+
1080+
user = optimizely_user_context.OptimizelyUserContext(
1081+
optimizely_client=None,
1082+
logger=None,
1083+
user_id="test_user",
1084+
user_attributes={}
1085+
)
1086+
1087+
# Create a CMAB experiment
1088+
cmab_experiment = entities.Experiment(
1089+
'111150',
1090+
'cmab_experiment',
1091+
'Running',
1092+
'111150',
1093+
[],
1094+
{},
1095+
[
1096+
entities.Variation('111151', 'variation_1'),
1097+
entities.Variation('111152', 'variation_2')
1098+
],
1099+
[
1100+
{'entityId': '111151', 'endOfRange': 5000},
1101+
{'entityId': '111152', 'endOfRange': 10000}
1102+
],
1103+
cmab={'trafficAllocation': 5000}
1104+
)
1105+
1106+
user_profile_service = user_profile.UserProfileService()
1107+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1108+
1109+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1110+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1111+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1112+
return_value=['$', []]), \
1113+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1114+
mock.patch.object(self.project_config, 'get_variation_from_id',
1115+
return_value=entities.Variation('111151', 'variation_1')), \
1116+
mock.patch(
1117+
'optimizely.decision_service.DecisionService.get_stored_variation'
1118+
) as mock_get_stored_variation, \
1119+
mock.patch.object(self.decision_service, 'logger') as mock_logger:
1120+
1121+
mock_cmab_service.get_decision.return_value = (
1122+
{
1123+
'variation_id': '111151',
1124+
'cmab_uuid': 'test-cmab-uuid-123'
1125+
},
1126+
[]
1127+
)
1128+
1129+
variation_result = self.decision_service.get_variation(
1130+
self.project_config,
1131+
cmab_experiment,
1132+
user,
1133+
user_profile_tracker
1134+
)
1135+
variation = variation_result['variation']
1136+
reasons = variation_result['reasons']
1137+
1138+
# Verify variation was returned from CMAB service
1139+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1140+
1141+
# Verify UPS lookup was NOT called for CMAB experiment
1142+
mock_get_stored_variation.assert_not_called()
1143+
1144+
# Verify UPS exclusion reason is logged
1145+
self.assertIn(
1146+
'Skipping UPS lookup and save for CMAB experiment "cmab_experiment".',
1147+
reasons
1148+
)
1149+
mock_logger.info.assert_any_call(
1150+
'Skipping UPS lookup and save for CMAB experiment "cmab_experiment".'
1151+
)
1152+
1153+
def test_get_variation_cmab_experiment_skips_ups_save(self):
1154+
"""Test that get_variation skips UPS save for CMAB experiments."""
1155+
1156+
user = optimizely_user_context.OptimizelyUserContext(
1157+
optimizely_client=None,
1158+
logger=None,
1159+
user_id="test_user",
1160+
user_attributes={}
1161+
)
1162+
1163+
# Create a CMAB experiment
1164+
cmab_experiment = entities.Experiment(
1165+
'111150',
1166+
'cmab_experiment',
1167+
'Running',
1168+
'111150',
1169+
[],
1170+
{},
1171+
[
1172+
entities.Variation('111151', 'variation_1'),
1173+
entities.Variation('111152', 'variation_2')
1174+
],
1175+
[
1176+
{'entityId': '111151', 'endOfRange': 5000},
1177+
{'entityId': '111152', 'endOfRange': 10000}
1178+
],
1179+
cmab={'trafficAllocation': 5000}
1180+
)
1181+
1182+
user_profile_service = user_profile.UserProfileService()
1183+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1184+
1185+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1186+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1187+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1188+
return_value=['$', []]), \
1189+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1190+
mock.patch.object(self.project_config, 'get_variation_from_id',
1191+
return_value=entities.Variation('111151', 'variation_1')), \
1192+
mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile, \
1193+
mock.patch.object(self.decision_service, 'logger'):
1194+
1195+
mock_cmab_service.get_decision.return_value = (
1196+
{
1197+
'variation_id': '111151',
1198+
'cmab_uuid': 'test-cmab-uuid-123'
1199+
},
1200+
[]
1201+
)
1202+
1203+
variation_result = self.decision_service.get_variation(
1204+
self.project_config,
1205+
cmab_experiment,
1206+
user,
1207+
user_profile_tracker
1208+
)
1209+
variation = variation_result['variation']
1210+
1211+
# Verify variation was returned from CMAB service
1212+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1213+
1214+
# Verify UPS save was NOT called for CMAB experiment
1215+
mock_update_profile.assert_not_called()
1216+
1217+
def test_get_variation_non_cmab_experiment_uses_ups(self):
1218+
"""Test that get_variation still uses UPS for non-CMAB experiments."""
1219+
1220+
user = optimizely_user_context.OptimizelyUserContext(
1221+
optimizely_client=None,
1222+
logger=None,
1223+
user_id="test_user",
1224+
user_attributes={}
1225+
)
1226+
1227+
experiment = self.project_config.get_experiment_from_key("test_experiment")
1228+
user_profile_service = user_profile.UserProfileService()
1229+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1230+
1231+
stored_variation = entities.Variation("111129", "variation")
1232+
1233+
with mock.patch.object(self.decision_service, "logger"), \
1234+
mock.patch(
1235+
"optimizely.decision_service.DecisionService.get_whitelisted_variation",
1236+
return_value=[None, []]
1237+
), \
1238+
mock.patch(
1239+
"optimizely.decision_service.DecisionService.get_stored_variation",
1240+
return_value=stored_variation
1241+
) as mock_get_stored_variation:
1242+
1243+
variation_result = self.decision_service.get_variation(
1244+
self.project_config, experiment, user, user_profile_tracker
1245+
)
1246+
variation = variation_result['variation']
1247+
1248+
# Verify stored variation was returned
1249+
self.assertEqual(stored_variation, variation)
1250+
1251+
# Verify UPS lookup WAS called for non-CMAB experiment
1252+
mock_get_stored_variation.assert_called_once_with(
1253+
self.project_config, experiment, user_profile_tracker.get_user_profile()
1254+
)
1255+
10771256

10781257
class FeatureFlagDecisionTests(base.BaseTest):
10791258
def setUp(self):

0 commit comments

Comments
 (0)