Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
return new DecisionResponse(variation, reasons);
}
}
boolean ignoreUPS = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we enforce to false, shouldn't we set the decideOptions value.

Copy link
Contributor Author

@FarhanAnjum-opti FarhanAnjum-opti Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the decideOptions value is binded with UserProfileTracker. If decideOptions disables UPS, tracker is null. This ignoreUPS variable is only for cmab in this context. We can initialize it using the decideOptions field too. Should work fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case leave a comment.


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

// Skip UPS for CMAB experiments as decisions are dynamic and not stored for sticky bucketing
ignoreUPS = true;
logger.debug(
"Skipping user profile service for CMAB experiment \"{}\". CMAB decisions are dynamic and not stored for sticky bucketing.",
experiment.getKey()
);

CmabDecision cmabResult = cmabDecision.getResult();
if (cmabResult != null) {
String variationId = cmabResult.getVariationId();
Expand All @@ -194,7 +202,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
}

if (variation != null) {
if (userProfileTracker != null) {
if (userProfileTracker != null && !ignoreUPS) {
userProfileTracker.updateUserProfile(experiment, variation);
} else {
logger.debug("This decision will not be saved since the UserProfileService is null.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,103 @@ public void getVariationCmabExperimentUserNotInTrafficAllocation() {
verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), any(DecisionPath.class));
}

/**
* Verify that CMAB experiments do NOT save bucketing decisions to user profile.
* CMAB decisions are dynamic and should not be stored for sticky bucketing.
*/
@Test
public void getVariationCmabExperimentDoesNotSaveUserProfile() throws Exception {
// Create a CMAB experiment
Experiment cmabExperiment = createMockCmabExperiment();
Variation variation1 = cmabExperiment.getVariations().get(0);

// Setup user profile service and tracker
UserProfileService mockUserProfileService = mock(UserProfileService.class);
when(mockUserProfileService.lookup(genericUserId)).thenReturn(null);

// Setup bucketer to return a variation (pass traffic allocation)
Bucketer mockBucketer = mock(Bucketer.class);
when(mockBucketer.bucket(eq(cmabExperiment), anyString(), eq(v4ProjectConfig), any(DecisionPath.class)))
.thenReturn(DecisionResponse.responseNoReasons(variation1));

// Setup CMAB service to return a decision
CmabDecision mockCmabDecision = mock(CmabDecision.class);
when(mockCmabDecision.getVariationId()).thenReturn(variation1.getId());
when(mockCmabDecision.getCmabUuid()).thenReturn("test-cmab-uuid-123");
when(mockCmabService.getDecision(any(), any(), any(), any()))
.thenReturn(mockCmabDecision);

DecisionService decisionServiceWithUPS = new DecisionService(
mockBucketer,
mockErrorHandler,
mockUserProfileService,
mockCmabService
);

// Call getVariation with CMAB experiment
DecisionResponse<Variation> result = decisionServiceWithUPS.getVariation(
cmabExperiment,
optimizely.createUserContext(genericUserId, Collections.emptyMap()),
v4ProjectConfig
);

// Verify variation and cmab_uuid are returned
assertEquals(variation1, result.getResult());
assertEquals("test-cmab-uuid-123", result.getCmabUuid());

// Verify user profile service was NEVER called to save
verify(mockUserProfileService, never()).save(anyMapOf(String.class, Object.class));

// Verify debug log was called to explain CMAB exclusion
logbackVerifier.expectMessage(Level.DEBUG,
"Skipping user profile service for CMAB experiment \"cmab_experiment\". " +
"CMAB decisions are dynamic and not stored for sticky bucketing.");
}

/**
* Verify that standard (non-CMAB) experiments DO save bucketing decisions to user profile.
* Standard experiments should use sticky bucketing.
*/
@Test
public void getVariationStandardExperimentSavesUserProfile() throws Exception {
final Experiment experiment = noAudienceProjectConfig.getExperiments().get(0);
final Variation variation = experiment.getVariations().get(0);
final Decision decision = new Decision(variation.getId());

UserProfileService mockUserProfileService = mock(UserProfileService.class);
when(mockUserProfileService.lookup(genericUserId)).thenReturn(null);

Bucketer mockBucketer = mock(Bucketer.class);
when(mockBucketer.bucket(eq(experiment), eq(genericUserId), eq(noAudienceProjectConfig), any(DecisionPath.class)))
.thenReturn(DecisionResponse.responseNoReasons(variation));

DecisionService decisionServiceWithUPS = new DecisionService(
mockBucketer,
mockErrorHandler,
mockUserProfileService,
null // No CMAB service for standard experiment
);

// Call getVariation with standard experiment
DecisionResponse<Variation> result = decisionServiceWithUPS.getVariation(
experiment,
optimizely.createUserContext(genericUserId, Collections.emptyMap()),
noAudienceProjectConfig
);

// Verify variation was returned
assertEquals(variation, result.getResult());

// Verify user profile WAS saved for standard experiment
UserProfile expectedUserProfile = new UserProfile(genericUserId,
Collections.singletonMap(experiment.getId(), decision));
verify(mockUserProfileService, times(1)).save(eq(expectedUserProfile.toMap()));

// Verify appropriate logging
logbackVerifier.expectMessage(Level.INFO,
String.format("Saved user profile of user \"%s\".", genericUserId));
}

private Experiment createMockCmabExperiment() {
List<Variation> variations = Arrays.asList(
new Variation("111151", "variation_1"),
Expand Down