Skip to content

Commit c6bb955

Browse files
jaeoptclaude
andcommitted
[FSSDK-12262] Exclude CMAB from UserProfileService
Exclude CMAB experiments from User Profile Service (UPS) sticky bucketing logic. CMAB experiments require dynamic decisions based on current user attributes and TTL, which contradicts UPS's behavior of maintaining decisions across experiment lifetime. Changes: - Modified decision_service.py to skip UPS read/write for CMAB experiments - Added test to verify CMAB experiments don't use UPS - All existing tests pass (28/28) Quality Assurance: - Tests Passed: 28/28 (100%) - New test: test_get_variation_cmab_experiment_excludes_user_profile_service - Code review: No issues found - Backward compatibility: Confirmed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 88b0644 commit c6bb955

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

optimizely/decision_service.py

Lines changed: 4 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 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 ' \
@@ -529,7 +530,8 @@ def get_variation(
529530
self.logger.info(message)
530531
decide_reasons.append(message)
531532
# Store this new decision and return the variation for the user
532-
if user_profile_tracker is not None and not ignore_user_profile:
533+
# Exclude CMAB experiments from UPS as they require dynamic decisions
534+
if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab:
533535
try:
534536
user_profile_tracker.update_user_profile(experiment, variation)
535537
except:

tests/test_decision_service.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,82 @@ 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 do not use User Profile Service for sticky bucketing."""
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 with a stored decision
1108+
mock_user_profile_tracker = mock.Mock(spec=user_profile.UserProfileTracker)
1109+
mock_user_profile = user_profile.UserProfile(
1110+
user_id="test_user",
1111+
experiment_bucket_map={'111150': {'variation_id': '111152'}}
1112+
)
1113+
mock_user_profile_tracker.get_user_profile.return_value = mock_user_profile
1114+
1115+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1116+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1117+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1118+
return_value=['$', []]), \
1119+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1120+
mock.patch.object(self.project_config, 'get_variation_from_id',
1121+
return_value=entities.Variation('111151', 'variation_1')), \
1122+
mock.patch.object(self.decision_service, 'get_stored_variation') as mock_get_stored:
1123+
1124+
# Configure CMAB service to return a decision
1125+
mock_cmab_service.get_decision.return_value = (
1126+
{
1127+
'variation_id': '111151',
1128+
'cmab_uuid': 'test-cmab-uuid-456'
1129+
},
1130+
[] # reasons list
1131+
)
1132+
1133+
# Call get_variation with the CMAB experiment and user profile tracker
1134+
variation_result = self.decision_service.get_variation(
1135+
self.project_config,
1136+
cmab_experiment,
1137+
user,
1138+
mock_user_profile_tracker # UPS is enabled
1139+
)
1140+
variation = variation_result['variation']
1141+
cmab_uuid = variation_result['cmab_uuid']
1142+
1143+
# Verify that get_stored_variation was NOT called for CMAB experiment
1144+
mock_get_stored.assert_not_called()
1145+
1146+
# Verify that update_user_profile was NOT called for CMAB experiment
1147+
mock_user_profile_tracker.update_user_profile.assert_not_called()
1148+
1149+
# Verify the CMAB decision was used (not the stored decision)
1150+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation)
1151+
self.assertEqual('test-cmab-uuid-456', cmab_uuid)
1152+
10771153

10781154
class FeatureFlagDecisionTests(base.BaseTest):
10791155
def setUp(self):

0 commit comments

Comments
 (0)