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
21 changes: 18 additions & 3 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,16 @@ 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
# 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 because UPS maintains decisions
# across the experiment lifetime without considering TTL or user attributes,
# which contradicts CMAB's dynamic nature.
is_cmab_experiment = experiment.key?('cmab')
if is_cmab_experiment && !should_ignore_user_profile_service && user_profile_tracker
message = "Skipping UPS lookup for CMAB experiment '#{experiment_key}'. CMAB decisions are excluded from user profile service."
@logger.log(Logger::DEBUG, message)
decide_reasons.push(message)
elsif !should_ignore_user_profile_service && user_profile_tracker
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
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 +163,14 @@ 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 preserve dynamic decision-making
if is_cmab_experiment && !should_ignore_user_profile_service && user_profile_tracker
message = "Skipping UPS save for CMAB experiment '#{experiment_key}'. CMAB decisions are excluded from user profile service."
@logger.log(Logger::DEBUG, 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
146 changes: 146 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1166,5 +1166,151 @@
expect(spy_cmab_service).not_to have_received(:get_decision)
end
end

describe 'CMAB experiments should be excluded from UPS' do
it 'should skip UPS lookup for CMAB experiments' do
cmab_experiment = {
'id' => '111150',
'key' => 'cmab_experiment',
'status' => 'Running',
'layerId' => '111150',
'audienceIds' => [],
'audienceConditions' => [],
'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 a user profile tracker with a stored variation for this experiment
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
user_profile_tracker.instance_variable_set(:@user_profile, {
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-ups'))
allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})

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

# Should get CMAB decision, NOT stored variation
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-ups')
expect(variation_result.error).to eq(false)

# Should have UPS exclusion reason
expect(variation_result.reasons).to include(
"Skipping UPS lookup for CMAB experiment 'cmab_experiment'. CMAB decisions are excluded from user profile service."
)

# CMAB service should still be called
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' => [],
'audienceConditions' => [],
'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', {})

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

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

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

# UPS save should NOT be called for CMAB experiments
# Verify the user profile was not updated (experiment_bucket_map should be empty)
expect(user_profile_tracker.user_profile[:experiment_bucket_map]).to be_empty

# Should still get the correct variation
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-save')
expect(variation_result.error).to eq(false)

# Should have UPS save exclusion reason
expect(variation_result.reasons).to include(
"Skipping UPS save for CMAB experiment 'cmab_experiment'. CMAB decisions are excluded from user profile service."
)
end

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

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.instance_variable_set(:@user_profile, {
user_id: 'test_user',
experiment_bucket_map: {
experiment_id => {variation_id: '111128'}
}
})

allow(config).to receive(:experiment_running?).with(experiment).and_return(true)
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, user_profile_tracker)

# Should get stored variation from UPS
expect(variation_result.variation_id).to eq('111128')
expect(variation_result.cmab_uuid).to be_nil
expect(variation_result.error).to eq(false)

# Should NOT have CMAB UPS exclusion reason
variation_result.reasons&.each do |reason|
next if reason.nil?

expect(reason).not_to include('CMAB decisions are excluded')
end
end
end
end
end
Loading