Skip to content

Commit 7a7cf79

Browse files
[FSSDK-12030] Update: Exclude CMAB from UserProfileService (#595)
* feat: exclude CMAB experiments from saving user profile decisions * fix: add comment to clarify ignoreUPS variable purpose in DecisionService
1 parent 0c9c329 commit 7a7cf79

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
164164
return new DecisionResponse(variation, reasons);
165165
}
166166
}
167+
boolean ignoreUPS = false; // whether to ignore user profile service for cmab experiments
167168

168169
DecisionResponse<Boolean> decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, user, EXPERIMENT, experiment.getKey());
169170
reasons.merge(decisionMeetAudience.getReasons());
@@ -181,6 +182,13 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
181182
return new DecisionResponse<>(null, reasons, true, null);
182183
}
183184

185+
// Skip UPS for CMAB experiments as decisions are dynamic and not stored for sticky bucketing
186+
ignoreUPS = true;
187+
logger.debug(
188+
"Skipping user profile service for CMAB experiment \"{}\". CMAB decisions are dynamic and not stored for sticky bucketing.",
189+
experiment.getKey()
190+
);
191+
184192
CmabDecision cmabResult = cmabDecision.getResult();
185193
if (cmabResult != null) {
186194
String variationId = cmabResult.getVariationId();
@@ -194,7 +202,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
194202
}
195203

196204
if (variation != null) {
197-
if (userProfileTracker != null) {
205+
if (userProfileTracker != null && !ignoreUPS) {
198206
userProfileTracker.updateUserProfile(experiment, variation);
199207
} else {
200208
logger.debug("This decision will not be saved since the UserProfileService is null.");

core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,103 @@ public void getVariationCmabExperimentUserNotInTrafficAllocation() {
17041704
verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), any(DecisionPath.class));
17051705
}
17061706

1707+
/**
1708+
* Verify that CMAB experiments do NOT save bucketing decisions to user profile.
1709+
* CMAB decisions are dynamic and should not be stored for sticky bucketing.
1710+
*/
1711+
@Test
1712+
public void getVariationCmabExperimentDoesNotSaveUserProfile() throws Exception {
1713+
// Create a CMAB experiment
1714+
Experiment cmabExperiment = createMockCmabExperiment();
1715+
Variation variation1 = cmabExperiment.getVariations().get(0);
1716+
1717+
// Setup user profile service and tracker
1718+
UserProfileService mockUserProfileService = mock(UserProfileService.class);
1719+
when(mockUserProfileService.lookup(genericUserId)).thenReturn(null);
1720+
1721+
// Setup bucketer to return a variation (pass traffic allocation)
1722+
Bucketer mockBucketer = mock(Bucketer.class);
1723+
when(mockBucketer.bucket(eq(cmabExperiment), anyString(), eq(v4ProjectConfig), any(DecisionPath.class)))
1724+
.thenReturn(DecisionResponse.responseNoReasons(variation1));
1725+
1726+
// Setup CMAB service to return a decision
1727+
CmabDecision mockCmabDecision = mock(CmabDecision.class);
1728+
when(mockCmabDecision.getVariationId()).thenReturn(variation1.getId());
1729+
when(mockCmabDecision.getCmabUuid()).thenReturn("test-cmab-uuid-123");
1730+
when(mockCmabService.getDecision(any(), any(), any(), any()))
1731+
.thenReturn(mockCmabDecision);
1732+
1733+
DecisionService decisionServiceWithUPS = new DecisionService(
1734+
mockBucketer,
1735+
mockErrorHandler,
1736+
mockUserProfileService,
1737+
mockCmabService
1738+
);
1739+
1740+
// Call getVariation with CMAB experiment
1741+
DecisionResponse<Variation> result = decisionServiceWithUPS.getVariation(
1742+
cmabExperiment,
1743+
optimizely.createUserContext(genericUserId, Collections.emptyMap()),
1744+
v4ProjectConfig
1745+
);
1746+
1747+
// Verify variation and cmab_uuid are returned
1748+
assertEquals(variation1, result.getResult());
1749+
assertEquals("test-cmab-uuid-123", result.getCmabUuid());
1750+
1751+
// Verify user profile service was NEVER called to save
1752+
verify(mockUserProfileService, never()).save(anyMapOf(String.class, Object.class));
1753+
1754+
// Verify debug log was called to explain CMAB exclusion
1755+
logbackVerifier.expectMessage(Level.DEBUG,
1756+
"Skipping user profile service for CMAB experiment \"cmab_experiment\". " +
1757+
"CMAB decisions are dynamic and not stored for sticky bucketing.");
1758+
}
1759+
1760+
/**
1761+
* Verify that standard (non-CMAB) experiments DO save bucketing decisions to user profile.
1762+
* Standard experiments should use sticky bucketing.
1763+
*/
1764+
@Test
1765+
public void getVariationStandardExperimentSavesUserProfile() throws Exception {
1766+
final Experiment experiment = noAudienceProjectConfig.getExperiments().get(0);
1767+
final Variation variation = experiment.getVariations().get(0);
1768+
final Decision decision = new Decision(variation.getId());
1769+
1770+
UserProfileService mockUserProfileService = mock(UserProfileService.class);
1771+
when(mockUserProfileService.lookup(genericUserId)).thenReturn(null);
1772+
1773+
Bucketer mockBucketer = mock(Bucketer.class);
1774+
when(mockBucketer.bucket(eq(experiment), eq(genericUserId), eq(noAudienceProjectConfig), any(DecisionPath.class)))
1775+
.thenReturn(DecisionResponse.responseNoReasons(variation));
1776+
1777+
DecisionService decisionServiceWithUPS = new DecisionService(
1778+
mockBucketer,
1779+
mockErrorHandler,
1780+
mockUserProfileService,
1781+
null // No CMAB service for standard experiment
1782+
);
1783+
1784+
// Call getVariation with standard experiment
1785+
DecisionResponse<Variation> result = decisionServiceWithUPS.getVariation(
1786+
experiment,
1787+
optimizely.createUserContext(genericUserId, Collections.emptyMap()),
1788+
noAudienceProjectConfig
1789+
);
1790+
1791+
// Verify variation was returned
1792+
assertEquals(variation, result.getResult());
1793+
1794+
// Verify user profile WAS saved for standard experiment
1795+
UserProfile expectedUserProfile = new UserProfile(genericUserId,
1796+
Collections.singletonMap(experiment.getId(), decision));
1797+
verify(mockUserProfileService, times(1)).save(eq(expectedUserProfile.toMap()));
1798+
1799+
// Verify appropriate logging
1800+
logbackVerifier.expectMessage(Level.INFO,
1801+
String.format("Saved user profile of user \"%s\".", genericUserId));
1802+
}
1803+
17071804
private Experiment createMockCmabExperiment() {
17081805
List<Variation> variations = Arrays.asList(
17091806
new Variation("111151", "variation_1"),

0 commit comments

Comments
 (0)