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
24 changes: 17 additions & 7 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,24 @@ 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
# Skip UPS for CMAB experiments as they use dynamic decision-making
unless 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
message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
@logger.log(Logger::INFO, message)
unless 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
message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
@logger.log(Logger::INFO, message)
decide_reasons.push(message)
return VariationResult.new(nil, false, decide_reasons, saved_variation_id)
end
else
message = "Skipping user profile service for CMAB experiment '#{experiment_key}'."
@logger.log(Logger::DEBUG, message)
decide_reasons.push(message)
return VariationResult.new(nil, false, decide_reasons, saved_variation_id)
end
end

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
# Skip UPS update for CMAB experiments as they use dynamic decision-making
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
129 changes: 129 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1166,5 +1166,134 @@
expect(spy_cmab_service).not_to have_received(:get_decision)
end
end

describe 'when user profile service is configured' do
it 'should exclude CMAB experiments from user profile service lookup' do
cmab_experiment = {
'id' => '111150',
'key' => 'cmab_experiment',
'layerId' => '111150',
'status' => 'Running',
'audienceIds' => [],
'audienceConditions' => [],
'variations' => [
{'id' => '111151', 'key' => 'variation_1', 'variables' => []},
{'id' => '111152', 'key' => 'variation_2', 'variables' => []}
],
'trafficAllocation' => [
{'entityId' => '111151', 'endOfRange' => 5000},
{'entityId' => '111152', 'endOfRange' => 10000}
],
'forcedVariations' => {},
'cmab' => {'trafficAllocation' => 5000, 'attributeIds' => []}
}

user_context = Optimizely::OptimizelyUserContext.new(project, 'test_user', {})

# Create a mock user profile service with stored decision
mock_ups = double('user_profile_service')
allow(mock_ups).to receive(:lookup).and_return(
'user_id' => 'test_user',
'experiment_bucket_map' => {
'111150' => {
'variation_id' => '111152' # Different from what CMAB will return
}
}
)

# Create decision service with user profile service
decision_service_with_ups = Optimizely::DecisionService.new(spy_logger, mock_ups, spy_cmab_service)

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

# Mock bucketing to return traffic allocation success
allow_any_instance_of(Optimizely::Bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

# Mock CMAB service to return a decision
allow(spy_cmab_service).to receive(:get_decision)
.with(config, user_context, '111150', [])
.and_return([
Optimizely::Cmab::CmabDecision.new('111151', 'test-cmab-uuid'),
[]
])

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

# Verify CMAB decision was used (variation_1/111151) instead of UPS stored decision (variation_2/111152)
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid')
expect(variation_result.error).to eq(false)

# Verify UPS exclusion reason was added
expect(variation_result.reasons).to include("Skipping user profile service for CMAB experiment 'cmab_experiment'.")

# Verify CMAB service was called (proving UPS lookup was skipped)
expect(spy_cmab_service).to have_received(:get_decision)
end

it 'should not save CMAB experiment decisions to user profile service' do
cmab_experiment = {
'id' => '111150',
'key' => 'cmab_experiment',
'layerId' => '111150',
'status' => 'Running',
'audienceIds' => [],
'audienceConditions' => [],
'variations' => [
{'id' => '111151', 'key' => 'variation_1', 'variables' => []},
{'id' => '111152', 'key' => 'variation_2', 'variables' => []}
],
'trafficAllocation' => [
{'entityId' => '111151', 'endOfRange' => 5000},
{'entityId' => '111152', 'endOfRange' => 10000}
],
'forcedVariations' => {},
'cmab' => {'trafficAllocation' => 5000, 'attributeIds' => []}
}

user_context = Optimizely::OptimizelyUserContext.new(project, 'test_user', {})

# Create a mock user profile service
mock_ups = double('user_profile_service')
allow(mock_ups).to receive(:lookup).and_return(nil)
allow(mock_ups).to receive(:save)

# Create decision service with user profile service
decision_service_with_ups = Optimizely::DecisionService.new(spy_logger, mock_ups, spy_cmab_service)

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

# Mock bucketing to return traffic allocation success
allow_any_instance_of(Optimizely::Bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

# Mock CMAB service to return a decision
allow(spy_cmab_service).to receive(:get_decision)
.with(config, user_context, '111150', [])
.and_return([
Optimizely::Cmab::CmabDecision.new('111151', 'test-cmab-uuid'),
[]
])

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

# Verify variation was returned
expect(variation_result.variation_id).to eq('111151')

# Verify UPS save was NOT called for CMAB experiment
expect(mock_ups).not_to have_received(:save)
end
end
end
end
Loading