Skip to content

Commit 9566f6b

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

File tree

2 files changed

+253
-2
lines changed

2 files changed

+253
-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+
# Skip UPS lookup for CMAB experiments - CMAB uses dynamic bucketing
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+
# Skip UPS save for CMAB experiments - CMAB uses dynamic bucketing
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: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,3 +1890,252 @@ def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_25
18901890
mock_config_logging.debug.assert_called_with(
18911891
'Assigned bucket 4000 to user with bucketing ID "test_user".')
18921892
mock_generate_bucket_value.assert_called_with("test_user211147")
1893+
1894+
def test_get_variation_cmab_experiment_excludes_ups_lookup(self):
1895+
"""Test that CMAB experiments skip UPS lookup for stored variations."""
1896+
1897+
# Create a user context
1898+
user = optimizely_user_context.OptimizelyUserContext(
1899+
optimizely_client=None,
1900+
logger=None,
1901+
user_id="test_user",
1902+
user_attributes={}
1903+
)
1904+
1905+
# Create a CMAB experiment
1906+
cmab_experiment = entities.Experiment(
1907+
'111150',
1908+
'cmab_experiment',
1909+
'Running',
1910+
'111150',
1911+
[], # No audience IDs
1912+
{},
1913+
[
1914+
entities.Variation('111151', 'variation_1'),
1915+
entities.Variation('111152', 'variation_2')
1916+
],
1917+
[
1918+
{'entityId': '111151', 'endOfRange': 5000},
1919+
{'entityId': '111152', 'endOfRange': 10000}
1920+
],
1921+
cmab={'trafficAllocation': 5000, 'attributeIds': []}
1922+
)
1923+
1924+
# Create a mock user profile tracker with a stored variation
1925+
mock_user_profile_tracker = mock.Mock()
1926+
mock_user_profile = user_profile.UserProfile(
1927+
user_id="test_user",
1928+
experiment_bucket_map={'111150': {'variation_id': '111152'}} # Stored as variation_2
1929+
)
1930+
mock_user_profile_tracker.get_user_profile.return_value = mock_user_profile
1931+
1932+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1933+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
1934+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
1935+
return_value=['$', []]), \
1936+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
1937+
mock.patch.object(self.project_config, 'get_variation_from_id',
1938+
return_value=entities.Variation('111151', 'variation_1')), \
1939+
mock.patch.object(self.decision_service, 'get_stored_variation') as mock_get_stored:
1940+
1941+
# Configure CMAB service to return variation_1
1942+
mock_cmab_service.get_decision.return_value = (
1943+
{
1944+
'variation_id': '111151',
1945+
'cmab_uuid': 'test-cmab-uuid-123'
1946+
},
1947+
[]
1948+
)
1949+
1950+
# Call get_variation with CMAB experiment and user profile tracker
1951+
variation_result = self.decision_service.get_variation(
1952+
self.project_config,
1953+
cmab_experiment,
1954+
user,
1955+
mock_user_profile_tracker
1956+
)
1957+
1958+
# Verify that get_stored_variation was NOT called (UPS lookup skipped)
1959+
mock_get_stored.assert_not_called()
1960+
1961+
# Verify that CMAB decision was used (variation_1, not stored variation_2)
1962+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation_result['variation'])
1963+
self.assertEqual('test-cmab-uuid-123', variation_result['cmab_uuid'])
1964+
1965+
def test_get_variation_cmab_experiment_excludes_ups_save(self):
1966+
"""Test that CMAB experiments skip UPS save for new decisions."""
1967+
1968+
# Create a user context
1969+
user = optimizely_user_context.OptimizelyUserContext(
1970+
optimizely_client=None,
1971+
logger=None,
1972+
user_id="test_user",
1973+
user_attributes={}
1974+
)
1975+
1976+
# Create a CMAB experiment
1977+
cmab_experiment = entities.Experiment(
1978+
'111150',
1979+
'cmab_experiment',
1980+
'Running',
1981+
'111150',
1982+
[],
1983+
{},
1984+
[
1985+
entities.Variation('111151', 'variation_1'),
1986+
entities.Variation('111152', 'variation_2')
1987+
],
1988+
[
1989+
{'entityId': '111151', 'endOfRange': 5000},
1990+
{'entityId': '111152', 'endOfRange': 10000}
1991+
],
1992+
cmab={'trafficAllocation': 5000, 'attributeIds': []}
1993+
)
1994+
1995+
# Create a mock user profile tracker
1996+
mock_user_profile_tracker = mock.Mock()
1997+
1998+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
1999+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
2000+
mock.patch.object(self.decision_service.bucketer, 'bucket_to_entity_id',
2001+
return_value=['$', []]), \
2002+
mock.patch.object(self.decision_service, 'cmab_service') as mock_cmab_service, \
2003+
mock.patch.object(self.project_config, 'get_variation_from_id',
2004+
return_value=entities.Variation('111151', 'variation_1')):
2005+
2006+
# Configure CMAB service to return a decision
2007+
mock_cmab_service.get_decision.return_value = (
2008+
{
2009+
'variation_id': '111151',
2010+
'cmab_uuid': 'test-cmab-uuid-123'
2011+
},
2012+
[]
2013+
)
2014+
2015+
# Call get_variation with CMAB experiment and user profile tracker
2016+
variation_result = self.decision_service.get_variation(
2017+
self.project_config,
2018+
cmab_experiment,
2019+
user,
2020+
mock_user_profile_tracker
2021+
)
2022+
2023+
# Verify that update_user_profile was NOT called (UPS save skipped)
2024+
mock_user_profile_tracker.update_user_profile.assert_not_called()
2025+
2026+
# Verify variation was still returned correctly
2027+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation_result['variation'])
2028+
self.assertEqual('test-cmab-uuid-123', variation_result['cmab_uuid'])
2029+
2030+
def test_get_variation_non_cmab_experiment_uses_ups_lookup(self):
2031+
"""Test that non-CMAB experiments still use UPS lookup for stored variations."""
2032+
2033+
# Create a user context
2034+
user = optimizely_user_context.OptimizelyUserContext(
2035+
optimizely_client=None,
2036+
logger=None,
2037+
user_id="test_user",
2038+
user_attributes={}
2039+
)
2040+
2041+
# Create a NON-CMAB experiment (cmab=None)
2042+
regular_experiment = entities.Experiment(
2043+
'111150',
2044+
'regular_experiment',
2045+
'Running',
2046+
'111150',
2047+
[],
2048+
{},
2049+
[
2050+
entities.Variation('111151', 'variation_1'),
2051+
entities.Variation('111152', 'variation_2')
2052+
],
2053+
[
2054+
{'entityId': '111151', 'endOfRange': 5000},
2055+
{'entityId': '111152', 'endOfRange': 10000}
2056+
],
2057+
cmab=None # Not a CMAB experiment
2058+
)
2059+
2060+
# Create a mock user profile tracker with a stored variation
2061+
mock_user_profile_tracker = mock.Mock()
2062+
mock_user_profile = user_profile.UserProfile(
2063+
user_id="test_user",
2064+
experiment_bucket_map={'111150': {'variation_id': '111152'}}
2065+
)
2066+
mock_user_profile_tracker.get_user_profile.return_value = mock_user_profile
2067+
2068+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
2069+
mock.patch.object(self.decision_service, 'get_stored_variation',
2070+
return_value=entities.Variation('111152', 'variation_2')) as mock_get_stored:
2071+
2072+
# Call get_variation with non-CMAB experiment and user profile tracker
2073+
variation_result = self.decision_service.get_variation(
2074+
self.project_config,
2075+
regular_experiment,
2076+
user,
2077+
mock_user_profile_tracker
2078+
)
2079+
2080+
# Verify that get_stored_variation WAS called (UPS lookup used)
2081+
mock_get_stored.assert_called_once()
2082+
2083+
# Verify that stored variation was returned
2084+
self.assertEqual(entities.Variation('111152', 'variation_2'), variation_result['variation'])
2085+
self.assertIsNone(variation_result['cmab_uuid'])
2086+
2087+
def test_get_variation_non_cmab_experiment_uses_ups_save(self):
2088+
"""Test that non-CMAB experiments still use UPS save for new decisions."""
2089+
2090+
# Create a user context
2091+
user = optimizely_user_context.OptimizelyUserContext(
2092+
optimizely_client=None,
2093+
logger=None,
2094+
user_id="test_user",
2095+
user_attributes={}
2096+
)
2097+
2098+
# Create a NON-CMAB experiment
2099+
regular_experiment = entities.Experiment(
2100+
'111150',
2101+
'regular_experiment',
2102+
'Running',
2103+
'111150',
2104+
[],
2105+
{},
2106+
[
2107+
entities.Variation('111151', 'variation_1'),
2108+
entities.Variation('111152', 'variation_2')
2109+
],
2110+
[
2111+
{'entityId': '111151', 'endOfRange': 5000},
2112+
{'entityId': '111152', 'endOfRange': 10000}
2113+
],
2114+
cmab=None # Not a CMAB experiment
2115+
)
2116+
2117+
# Create a mock user profile tracker
2118+
mock_user_profile_tracker = mock.Mock()
2119+
2120+
with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \
2121+
mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \
2122+
mock.patch.object(self.decision_service.bucketer, 'bucket',
2123+
return_value=[entities.Variation('111151', 'variation_1'), []]):
2124+
2125+
# Call get_variation with non-CMAB experiment and user profile tracker
2126+
variation_result = self.decision_service.get_variation(
2127+
self.project_config,
2128+
regular_experiment,
2129+
user,
2130+
mock_user_profile_tracker
2131+
)
2132+
2133+
# Verify that update_user_profile WAS called (UPS save used)
2134+
mock_user_profile_tracker.update_user_profile.assert_called_once_with(
2135+
regular_experiment,
2136+
entities.Variation('111151', 'variation_1')
2137+
)
2138+
2139+
# Verify variation was returned correctly
2140+
self.assertEqual(entities.Variation('111151', 'variation_1'), variation_result['variation'])
2141+
self.assertIsNone(variation_result['cmab_uuid'])

0 commit comments

Comments
 (0)