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
15 changes: 13 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ 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
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')
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
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,9 @@ 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 because UPS maintains decisions across the experiment
# lifetime without considering TTL or user attributes, which contradicts CMAB's dynamic nature.
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
70 changes: 70 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1166,5 +1166,75 @@
expect(spy_cmab_service).not_to have_received(:get_decision)
end
end

describe 'when user profile service is available' do
it 'should exclude CMAB experiments from user profile service' do
# Create a CMAB experiment configuration
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 user profile tracker with a mock user profile service
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)

# Mock experiment lookup to return our CMAB experiment
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 a 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 decision
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'))

# Mock variation lookup
allow(config).to receive(:get_variation_from_id_by_experiment_id)
.with('111150', '111151')
.and_return({'id' => '111151', 'key' => 'variation_1'})

# Spy on user profile methods
allow(decision_service).to receive(:get_saved_variation_id).and_call_original
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)

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 decision reasons include the UPS exclusion message
expect(variation_result.reasons).to include(
"Skipping user profile service for CMAB experiment 'cmab_experiment'."
)

# Verify get_saved_variation_id was NOT called (UPS lookup skipped)
expect(decision_service).not_to have_received(:get_saved_variation_id)

# Verify update_user_profile was NOT called (UPS save skipped)
expect(user_profile_tracker).not_to have_received(:update_user_profile)
end
end
end
end
Loading