Skip to content
Closed
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
10 changes: 8 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac

should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
unless should_ignore_user_profile_service && user_profile_tracker
# Note: CMAB experiments are excluded from User Profile Service (UPS) because UPS maintains
# decisions across the experiment lifetime without considering TTL or user attributes,
# which contradicts CMAB's dynamic nature.
unless should_ignore_user_profile_service && user_profile_tracker || experiment.key?('cmab')
saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)
decide_reasons.push(*reasons_received)
if saved_variation_id
Expand Down Expand Up @@ -155,7 +158,10 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
decide_reasons.push(message) if message

# Persist bucketing decision
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
# Note: CMAB experiments are excluded from User Profile Service (UPS) because UPS maintains
# decisions across the experiment lifetime without considering TTL or user attributes,
# which contradicts CMAB's dynamic nature.
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker || experiment.key?('cmab')
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
end

Expand Down
78 changes: 78 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1166,5 +1166,83 @@
expect(spy_cmab_service).not_to have_received(:get_decision)
end
end

describe 'CMAB experiments exclude User Profile Service' do
it 'should not use UPS for sticky bucketing on CMAB experiments' do
# Create a CMAB experiment
cmab_experiment = {
'id' => '111150',
'key' => 'cmab_experiment',
'status' => 'Running',
'layerId' => '111150',
'audienceIds' => [],
'forcedVariations' => {},
'variations' => [
{'id' => '111151', 'key' => 'variation_1'},
{'id' => '111152', 'key' => 'variation_2'}
],
'trafficAllocation' => [
{'entityId' => '111151', 'endOfRange' => 5000},
{'entityId' => '111152', 'endOfRange' => 10_000}
],
'cmab' => {'trafficAllocation' => 5000}
}
user_context = project_instance.create_user_context('test_user', {})

# Create user profile service and tracker
ups = spy_user_profile_service
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', ups, spy_logger)

# Pre-populate user profile with a stored decision (simulating previous bucketing)
allow(ups).to receive(:lookup).and_return(
user_id: 'test_user',
experiment_bucket_map: {
'111150' => {
variation_id: '111151' # Previously bucketed to variation_1
}
}
)

# Mock experiment lookup
allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment)
allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true)

# Mock audience evaluation
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])

# Mock bucketer to return valid entity ID (user is in traffic allocation)
allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

# Mock CMAB service to return a different variation
allow(spy_cmab_service).to receive(:get_decision)
.with(config, user_context, '111150', [])
.and_return(Optimizely::CmabDecision.new(variation_id: '111152', cmab_uuid: 'test-cmab-uuid-456'))

# Mock variation lookup
allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111152')
.and_return({'id' => '111152', 'key' => 'variation_2'})

# Spy on get_saved_variation_id to verify it's NOT called
allow(decision_service).to receive(:get_saved_variation_id).and_call_original

variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker)

# Verify that get_saved_variation_id was NOT called (UPS retrieval skipped for CMAB)
expect(decision_service).not_to have_received(:get_saved_variation_id)

# Verify that UPS save was NOT called (UPS storage skipped for CMAB)
expect(ups).not_to have_received(:save)

# Verify that CMAB decision was used (variation_2, not the stored variation_1)
expect(variation_result.variation_id).to eq('111152')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-456')

# Verify CMAB service was called
expect(spy_cmab_service).to have_received(:get_decision).once
end
end
end
end
Loading