Skip to content

Commit 2f02f5d

Browse files
jaeoptclaude
andcommitted
[FSSDK-12262] Exclude CMAB from UserProfileService
- Skip UPS retrieval for CMAB experiments - Skip UPS saving for CMAB experiments - Add decision reason when UPS is excluded - Add test to verify CMAB excludes UPS Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 88b0644 commit 2f02f5d

2 files changed

Lines changed: 89 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+
# Exclude CMAB experiments from UPS - they handle dynamic decisions differently
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 = 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+
# Exclude CMAB experiments from UPS - they handle dynamic decisions differently
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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,87 @@ 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_ups(self):
1078+
"""Test that CMAB experiments exclude User Profile Service for both reading and saving."""
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 a mock user profile tracker
1108+
mock_user_profile_tracker = mock.MagicMock()
1109+
mock_user_profile = user_profile.UserProfile('test_user')
1110+
# Add a stored variation for the CMAB experiment (should be ignored)
1111+
mock_user_profile.experiment_bucket_map['111150'] = {'variation_id': '111152'}
1112+
mock_user_profile_tracker.get_user_profile.return_value = mock_user_profile
1113+
1114+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1115+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1116+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1117+
return_value=['$', []]) as mock_bucket, \
1118+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1119+
mock.patch.object(self.project_config, 'get_variation_from_id',
1120+
return_value=entities.Variation('111151', 'variation_1')), \
1121+
mock.patch.object(self.decision_service, 'logger') as mock_logger:
1122+
1123+
# Configure CMAB service to return a decision
1124+
mock_cmab_service.get_decision.return_value = (
1125+
{
1126+
'variation_id': '111151',
1127+
'cmab_uuid': 'test-cmab-uuid-123'
1128+
},
1129+
[] # reasons list
1130+
)
1131+
1132+
# Call get_variation with the CMAB experiment and user profile tracker
1133+
variation_result = self.decision_service.get_variation(
1134+
self.project_config,
1135+
cmab_experiment,
1136+
user,
1137+
mock_user_profile_tracker
1138+
)
1139+
variation = variation_result['variation']
1140+
cmab_uuid = variation_result['cmab_uuid']
1141+
reasons = variation_result['reasons']
1142+
error = variation_result['error']
1143+
1144+
# Verify the variation returned is from CMAB, not from stored profile
1145+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1146+
self.assertEqual('test-cmab-uuid-123', cmab_uuid)
1147+
self.assertStrictFalse(error)
1148+
1149+
# Verify UPS exclusion reason is in decision reasons
1150+
self.assertIn('Skipping User Profile Service for CMAB experiment "cmab_experiment".', reasons)
1151+
1152+
# Verify get_stored_variation was not called (implicitly, since we got CMAB decision)
1153+
mock_logger.debug.assert_any_call('Skipping User Profile Service for CMAB experiment "cmab_experiment".')
1154+
1155+
# Verify update_user_profile was NOT called (CMAB shouldn't save to UPS)
1156+
mock_user_profile_tracker.update_user_profile.assert_not_called()
1157+
10771158

10781159
class FeatureFlagDecisionTests(base.BaseTest):
10791160
def setUp(self):

0 commit comments

Comments
 (0)