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: 15 additions & 7 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,21 @@ 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
# Exclude CMAB experiments from UPS - they handle dynamic decisions differently
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 experiment.key?('cmab')
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 +162,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
# Exclude CMAB experiments from UPS - they handle dynamic decisions differently
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker && experiment.key?('cmab')
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 CMAB experiment excludes User Profile Service' do
it 'should skip UPS retrieval and saving for CMAB experiments' 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}
}

# Create a mock user profile service
mock_user_profile_service = double('user_profile_service')
decision_service_with_ups = Optimizely::DecisionService.new(spy_logger, spy_cmab_service, mock_user_profile_service)

# Create user profile with saved variation (should be ignored for CMAB)
user_profile = {
user_id: 'test_user',
experiment_bucket_map: {
'111150' => {variation_id: '111152'}
}
}

# Mock user profile service lookup
allow(mock_user_profile_service).to receive(:lookup).with('test_user').and_return(user_profile)
allow(mock_user_profile_service).to receive(:save)

user_context = project_instance.create_user_context('test_user', {})

# 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 a valid entity ID
allow(decision_service_with_ups.bucketer).to receive(:bucket_to_entity_id)
.with(config, cmab_experiment, 'test_user', 'test_user')
.and_return(['$', []])

# Mock CMAB service to return a decision (variation_1, not the stored variation_2)
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'})

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

# Verify the variation returned is from CMAB, not from stored profile
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 UPS exclusion reason is in decision reasons
expect(variation_result.reasons).to include(
"Skipping User Profile Service for CMAB experiment 'cmab_experiment'."
)

# Verify user profile service save was NOT called (CMAB shouldn't save to UPS)
expect(mock_user_profile_service).not_to have_received(:save)
end
end
end
end
Loading