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: 15 additions & 6 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,22 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac

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
# CMAB experiments are excluded from UserProfileService to allow dynamic decisions
is_cmab_experiment = experiment.key?('cmab')
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."
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 = 'User profile service excluded for CMAB experiment to allow dynamic decisions.'
@logger.log(Logger::INFO, message)
decide_reasons.push(message)
return VariationResult.new(nil, false, decide_reasons, saved_variation_id)
end
end

Expand Down Expand Up @@ -155,7 +163,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 UserProfileService
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
63 changes: 63 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1166,5 +1166,68 @@
expect(spy_cmab_service).not_to have_received(:get_decision)
end
end

describe 'UserProfileService exclusion for CMAB' do
it 'should exclude CMAB experiments from UserProfileService and include decision reason' do
# Create a CMAB experiment
cmab_experiment = {
'id' => '111150',
'key' => 'cmab_experiment',
'status' => 'Running',
'audienceIds' => [],
'audienceConditions' => [],
'forcedVariations' => {},
'layerId' => '1',
'trafficAllocation' => [
{'entityId' => '111151', 'endOfRange' => 10_000}
],
'variations' => [
{'id' => '111151', 'key' => 'variation_1', 'variables' => []}
],
'cmab' => true
}

# Setup mocks for CMAB decision
cmab_decision = Optimizely::CmabService::CmabDecision.new('111151', 'test-uuid')
allow(spy_cmab_service).to receive(:get_decision)
.and_return([cmab_decision, ['CMAB decision made']])

# Setup user profile service with a stored variation
stored_profile = {
'user_id' => 'test_user',
'experiment_bucket_map' => {
'111150' => {'variation_id' => '111152'} # Different variation
}
}
allow(spy_user_profile_service).to receive(:lookup).and_return(stored_profile)

# Setup config mocks
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'})

# Create user profile tracker
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)

# Call get_variation
user_context = project_instance.create_user_context('test_user')
variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker)

# Verify the CMAB decision was used (variation_1), not the stored profile (111152)
expect(variation_result.variation_id).to eq('111151')
expect(variation_result.cmab_uuid).to eq('test-uuid')

# Verify the UPS exclusion reason is in the decision reasons
expect(variation_result.reasons).to include('User profile service excluded for CMAB experiment to allow dynamic decisions.')

# Verify UPS lookup was NOT called (bypassed)
expect(spy_user_profile_service).not_to have_received(:lookup)

# Verify UPS save was NOT called (bypassed)
expect(spy_user_profile_service).not_to have_received(:save)
end
end
end
end
Loading