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
14 changes: 12 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,11 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
return VariationResult.new(nil, false, decide_reasons, whitelisted_variation_id) if whitelisted_variation_id

should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
is_cmab_experiment = experiment.key?('cmab')

# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
unless should_ignore_user_profile_service && user_profile_tracker
# CMAB experiments are excluded from UPS to ensure decisions are always computed dynamically
unless should_ignore_user_profile_service && user_profile_tracker || is_cmab_experiment
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 All @@ -111,6 +114,12 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
end
end

if is_cmab_experiment && !should_ignore_user_profile_service && user_profile_tracker
message = "Skipping UPS lookup and save for CMAB experiment '#{experiment_key}'."
@logger.log(Logger::INFO, message)
decide_reasons.push(message)
end

# Check audience conditions
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, experiment, user_context, @logger)
decide_reasons.push(*reasons_received)
Expand Down Expand Up @@ -155,7 +164,8 @@ 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
# CMAB experiments are excluded from UPS to ensure decisions remain dynamic
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker || is_cmab_experiment
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
end

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

describe 'UPS exclusion for CMAB experiments' do
it 'should skip UPS lookup for CMAB experiments' do
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', {})

# Set up a user profile with a stored decision for this experiment
allow(spy_user_profile_service).to receive(:lookup).and_return(
user_id: 'test_user',
experiment_bucket_map: {
'111150' => {variation_id: '111152'}
}
)

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)
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])

allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

allow(spy_cmab_service).to receive(:get_decision)
.with(config, user_context, '111150', [])
.and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid-456'))

allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})

user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
user_profile_tracker.load_user_profile

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

# Should get CMAB decision, NOT stored UPS variation
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-456')
expect(variation_result.reasons).to include(
"Skipping UPS lookup and save for CMAB experiment 'cmab_experiment'."
)

# Verify CMAB service was called (not bypassed by UPS)
expect(spy_cmab_service).to have_received(:get_decision).once
end

it 'should skip UPS save for CMAB experiments' do
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', {})

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)
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])

allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

allow(spy_cmab_service).to receive(:get_decision)
.with(config, user_context, '111150', [])
.and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid-789'))

allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})

user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)

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

expect(variation_result.variation_id).to eq('111151')

# Verify UPS save was NOT called for CMAB experiment
expect(user_profile_tracker.user_profile[:experiment_bucket_map]).not_to have_key('111150')
end

it 'should still use UPS for non-CMAB experiments' do
experiment = config.get_experiment_from_key('test_experiment')
experiment_id = experiment['id']

allow(spy_user_profile_service).to receive(:lookup).and_return(
user_id: 'test_user',
experiment_bucket_map: {
experiment_id => {variation_id: '111129'}
}
)

user_context = project_instance.create_user_context('test_user', {})
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
user_profile_tracker.load_user_profile

variation_result = decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)

# Should return stored UPS variation for non-CMAB experiment
expect(variation_result.variation_id).to eq('111129')
expect(variation_result.reasons).to include(
"Returning previously activated variation ID 111129 of experiment 'test_experiment' for user 'test_user' from user profile."
)
end
end
end
end
Loading