Skip to content

Commit 46f7737

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

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

optimizely/decision_service.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,10 @@ 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+
# Note: CMAB experiments are excluded from User Profile Service (UPS) because UPS maintains
461+
# decisions across the experiment lifetime without considering TTL or user attributes,
462+
# which contradicts CMAB's dynamic nature.
463+
if user_profile_tracker is not None and not ignore_user_profile and not experiment.cmab:
461464
variation = self.get_stored_variation(project_config, experiment, user_profile_tracker.get_user_profile())
462465
if variation:
463466
message = f'Returning previously activated variation ID "{variation}" of experiment ' \
@@ -529,7 +532,10 @@ def get_variation(
529532
self.logger.info(message)
530533
decide_reasons.append(message)
531534
# Store this new decision and return the variation for the user
532-
if user_profile_tracker is not None and not ignore_user_profile:
535+
# Note: CMAB experiments are excluded from User Profile Service (UPS) because UPS maintains
536+
# decisions across the experiment lifetime without considering TTL or user attributes,
537+
# which contradicts CMAB's dynamic nature.
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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,95 @@ 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+
CMAB decisions should be dynamic and not rely on UPS, which maintains decisions
1081+
across the experiment lifetime without considering TTL or user attributes.
1082+
"""
1083+
1084+
# Create a user context
1085+
user = optimizely_user_context.OptimizelyUserContext(
1086+
optimizely_client=None,
1087+
logger=None,
1088+
user_id="test_user",
1089+
user_attributes={}
1090+
)
1091+
1092+
# Create a user profile service and tracker
1093+
user_profile_service = user_profile.UserProfileService()
1094+
user_profile_tracker = user_profile.UserProfileTracker(user.user_id, user_profile_service)
1095+
1096+
# Pre-populate the user profile with a stored decision (simulating previous bucketing)
1097+
user_profile_tracker.update_user_profile(
1098+
entities.Experiment('111150', 'cmab_experiment', 'Running', '111150', [], {},
1099+
[entities.Variation('111151', 'variation_1')],
1100+
[{'entityId': '111151', 'endOfRange': 10000}],
1101+
cmab={'trafficAllocation': 5000}),
1102+
entities.Variation('111151', 'variation_1')
1103+
)
1104+
1105+
# Create a CMAB experiment
1106+
cmab_experiment = entities.Experiment(
1107+
'111150',
1108+
'cmab_experiment',
1109+
'Running',
1110+
'111150',
1111+
[],
1112+
{},
1113+
[
1114+
entities.Variation('111151', 'variation_1'),
1115+
entities.Variation('111152', 'variation_2')
1116+
],
1117+
[
1118+
{'entityId': '111151', 'endOfRange': 5000},
1119+
{'entityId': '111152', 'endOfRange': 10000}
1120+
],
1121+
cmab={'trafficAllocation': 5000}
1122+
)
1123+
1124+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1125+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1126+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1127+
return_value=['$', []]), \
1128+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1129+
mock.patch.object(self.project_config, 'get_variation_from_id',
1130+
return_value=entities.Variation('111152', 'variation_2')), \
1131+
mock.patch.object(self.decision_service, 'get_stored_variation') as mock_get_stored_variation, \
1132+
mock.patch.object(user_profile_tracker, 'update_user_profile') as mock_update_profile:
1133+
1134+
# Configure CMAB service to return a different variation than what's stored
1135+
mock_cmab_service.get_decision.return_value = (
1136+
{
1137+
'variation_id': '111152',
1138+
'cmab_uuid': 'test-cmab-uuid-456'
1139+
},
1140+
[]
1141+
)
1142+
1143+
# Call get_variation with user profile tracker
1144+
variation_result = self.decision_service.get_variation(
1145+
self.project_config,
1146+
cmab_experiment,
1147+
user,
1148+
user_profile_tracker
1149+
)
1150+
variation = variation_result['variation']
1151+
cmab_uuid = variation_result['cmab_uuid']
1152+
1153+
# Verify that get_stored_variation was NOT called (UPS retrieval skipped for CMAB)
1154+
mock_get_stored_variation.assert_not_called()
1155+
1156+
# Verify that update_user_profile was NOT called (UPS storage skipped for CMAB)
1157+
mock_update_profile.assert_not_called()
1158+
1159+
# Verify that CMAB decision was used (variation_2, not the stored variation_1)
1160+
self.assertEqual(entities.Variation('111152', 'variation_2'), variation)
1161+
self.assertEqual('test-cmab-uuid-456', cmab_uuid)
1162+
1163+
# Verify CMAB service was called
1164+
mock_cmab_service.get_decision.assert_called_once()
1165+
10771166

10781167
class FeatureFlagDecisionTests(base.BaseTest):
10791168
def setUp(self):

0 commit comments

Comments
 (0)