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
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
# CMAB experiments are excluded from UPS (sticky bucketing) since CMAB
# decisions are managed by the CMAB service with its own caching.
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
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 user profile service for CMAB experiment '#{experiment_key}'."
@logger.log(Logger::DEBUG, 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 since CMAB manages its own decisions.
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 'CMAB experiments skip UPS' do
it 'should skip UPS lookup for CMAB experiments even when user profile tracker is available' 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', {})
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
user_profile_tracker.load_user_profile

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

# Spy on get_saved_variation_id to ensure it is NOT called for CMAB experiments
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 UPS lookup was NOT called for CMAB experiment
expect(decision_service).not_to have_received(:get_saved_variation_id)

# Verify variation was returned correctly
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-123')
expect(variation_result.error).to eq(false)

# Verify the skip reason was logged
expect(variation_result.reasons).to include(
"Skipping user profile service for CMAB experiment 'cmab_experiment'."
)
expect(spy_logger).to have_received(:log).with(
Logger::DEBUG,
"Skipping user profile service for CMAB experiment 'cmab_experiment'."
)
end

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

# Spy on update_user_profile
allow(user_profile_tracker).to receive(:update_user_profile).and_call_original

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

# Verify variation was returned correctly
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-123')

# Verify UPS was NOT updated for CMAB experiment
expect(user_profile_tracker).not_to have_received(:update_user_profile)
end

it 'should still use UPS for non-CMAB experiments (regression test)' do
experiment_id = '111127'
saved_user_profile = {
user_id: 'test_user',
experiment_bucket_map: {
experiment_id => {
variation_id: '111128'
}
}
}
expect(spy_user_profile_service).to receive(:lookup)
.once.with('test_user').and_return(saved_user_profile)

user_context = project_instance.create_user_context('test_user')
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, 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)

# Verify stored variation IS returned for non-CMAB experiment
expect(variation_result.variation_id).to eq('111128')

# Verify bucketing was NOT called since stored variation was found
expect(decision_service.bucketer).not_to have_received(:bucket)
end
end
end
end
Loading