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
20 changes: 18 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,15 @@ 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
# CMAB experiments are excluded from UPS lookup because UPS maintains decisions
# across the experiment lifetime without considering TTL or user attributes,
# which contradicts CMAB's dynamic nature.
if experiment.key?('cmab')
message = "Skipping UPS lookup for CMAB experiment '#{experiment_key}'."
@logger.log(Logger::INFO, message)
decide_reasons.push(message)
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
unless should_ignore_user_profile_service && user_profile_tracker
elsif !(should_ignore_user_profile_service && user_profile_tracker)
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 +162,16 @@ 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 save because UPS maintains decisions
# across the experiment lifetime without considering TTL or user attributes,
# which contradicts CMAB's dynamic nature.
if experiment.key?('cmab')
message = "Skipping UPS save for CMAB experiment '#{experiment_key}'."
@logger.log(Logger::INFO, message)
decide_reasons.push(message)
elsif !(should_ignore_user_profile_service && user_profile_tracker)
user_profile_tracker.update_user_profile(experiment_id, variation_id)
end
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
end

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

describe 'CMAB UPS exclusion' 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', {})

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'))
allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})

# Set up a stored decision that should be ignored for CMAB
allow(spy_user_profile_service).to receive(:lookup).and_return({
user_id: 'test_user',
experiment_bucket_map: {
'111150' => { variation_id: '111152' }
}
})

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

# Should NOT return the stored variation (111152), should use CMAB decision instead
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.reasons).to include(
"Skipping UPS lookup for CMAB experiment 'cmab_experiment'."
)
expect(spy_logger).to have_received(:log).with(Logger::INFO, "Skipping UPS lookup for CMAB experiment 'cmab_experiment'.")
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'))
allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})
allow(spy_user_profile_service).to receive(:lookup).and_return(nil)

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

expect(variation_result.variation_id).to eq('111151')
expect(variation_result.reasons).to include(
"Skipping UPS save for CMAB experiment 'cmab_experiment'."
)
expect(spy_logger).to have_received(:log).with(Logger::INFO, "Skipping UPS save for CMAB experiment 'cmab_experiment'.")
# Verify user profile service save was not called
expect(spy_user_profile_service).not_to have_received(:save)
end

it 'should still use UPS for non-CMAB experiments' do
experiment = config.get_experiment_from_key('test_experiment')
experiment_id = experiment['id']
user_context = project_instance.create_user_context('test_user', {})

allow(spy_user_profile_service).to receive(:lookup).and_return({
user_id: 'test_user',
experiment_bucket_map: {
experiment_id => { variation_id: '111128' }
}
})
allow(config).to receive(:variation_id_exists?).with(experiment_id, '111128').and_return(true)

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

# Should return the stored variation for non-CMAB experiment
expect(variation_result.variation_id).to eq('111128')
expect(variation_result.reasons).to include(
"Returning previously activated variation ID 111128 of experiment 'test_experiment' for user 'test_user' from user profile."
)
# Should NOT contain any CMAB skip messages
expect(variation_result.reasons).not_to include(
"Skipping UPS lookup for CMAB experiment 'test_experiment'."
)
end
end
end
end
Loading