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
22 changes: 20 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ 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
# Exclude CMAB experiments from UPS sticky bucketing
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 +113,12 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
end
end

if is_cmab_experiment
message = 'Skipped user profile lookup for CMAB experiment.'
@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 +163,17 @@ 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
# Exclude CMAB experiments from UPS sticky bucketing
unless should_ignore_user_profile_service && user_profile_tracker || is_cmab_experiment
user_profile_tracker.update_user_profile(experiment_id, variation_id)
end

if is_cmab_experiment
save_message = 'Skipped saving user profile for CMAB experiment.'
@logger.log(Logger::DEBUG, save_message)
decide_reasons.push(save_message)
end

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 'when excluding User Profile Service' do
it 'should skip UPS lookup and save for 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 a mock user profile service and tracker
mock_ups = instance_double(Optimizely::UserProfileService)
mock_tracker = instance_double(Optimizely::UserProfileTracker)
allow(mock_tracker).to receive(:user_profile).and_return(
{
user_id: 'test_user',
experiment_bucket_map: {'111150' => {variation_id: '111151'}}
}
)
allow(mock_tracker).to receive(:update_user_profile)

# 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 to pass
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 than stored
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'})

# Don't mock get_saved_variation_id - we want 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, mock_tracker)

# Verify the variation is from CMAB service, not from stored profile
expect(variation_result.variation_id).to eq('111152')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-456')
expect(variation_result.error).to eq(false)

# Verify decision reasons include UPS exclusion messages
expect(variation_result.reasons).to include('Skipped user profile lookup for CMAB experiment.')
expect(variation_result.reasons).to include('Skipped saving user profile for CMAB experiment.')

# Verify UPS methods were NOT called
expect(decision_service).not_to have_received(:get_saved_variation_id)
expect(mock_tracker).not_to have_received(:update_user_profile)

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