Skip to content
Merged
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: 11 additions & 13 deletions lib/optimizely/bucketer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,8 @@ def bucket(experiment_key, user_id)
if group_id
group = @config.group_key_map.fetch(group_id)
if Helpers::Group.random_policy?(group)
bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: group_id)
traffic_allocations = group.fetch('trafficAllocation')
bucket_value = generate_bucket_value(bucketing_id)
@config.logger.log(Logger::DEBUG, "Assigned experiment bucket #{bucket_value} to user '#{user_id}'.")
bucketed_experiment_id = find_bucket(bucket_value, traffic_allocations)

bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
# return if the user is not bucketed into any experiment
unless bucketed_experiment_id
@config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
Expand All @@ -78,11 +74,8 @@ def bucket(experiment_key, user_id)
end
end

bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: experiment_id)
bucket_value = generate_bucket_value(bucketing_id)
@config.logger.log(Logger::DEBUG, "Assigned variation bucket #{bucket_value} to user '#{user_id}'.")
traffic_allocations = @config.get_traffic_allocation(experiment_key)
variation_id = find_bucket(bucket_value, traffic_allocations)
variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
if variation_id && variation_id != ''
variation_key = @config.get_variation_key_from_id(experiment_key, variation_id)
@config.logger.log(
Expand All @@ -101,16 +94,19 @@ def bucket(experiment_key, user_id)
nil
end

private

def find_bucket(bucket_value, traffic_allocations)
def find_bucket(user_id, parent_id, traffic_allocations)
# Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
#
# bucket_value - Integer bucket value
# user_id - String ID for user
# parent_id - String entity ID to use for bucketing ID
# traffic_allocations - Array of traffic allocations
#
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.

bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: parent_id)
bucket_value = generate_bucket_value(bucketing_id)
@config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")

traffic_allocations.each do |traffic_allocation|
current_end_of_range = traffic_allocation['endOfRange']
if bucket_value < current_end_of_range
Expand All @@ -122,6 +118,8 @@ def find_bucket(bucket_value, traffic_allocations)
nil
end

private

def generate_bucket_value(bucketing_id)
# Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
#
Expand Down
87 changes: 85 additions & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,91 @@ def get_variation(experiment_key, user_id, attributes = nil)
variation_id
end

def get_variation_for_feature(feature_flag, user_id, attributes = nil)
# Get the variation the user is bucketed into for the given FeatureFlag.
#
# feature_flag - The feature flag the user wants to access
# user_id - String ID for the user
# attributes - Hash representing user attributes
#
# Returns variation where visitor will be bucketed (nil if the user is not bucketed into any of the experiments on the feature)

# check if the feature is being experiment on and whether the user is bucketed into the experiment
variation = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
return variation

# @TODO(mng) next check if the user feature being rolled out and whether the user is part of the rollout
end

private


def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
# Gets the variation the user is bucketed into for the feature flag's experiment
#
# feature_flag - The feature flag the user wants to access
# user_id - String ID for the user
# attributes - Hash representing user attributes
#
# Returns variation where visitor will be bucketed
# or nil if the user is not bucketed into any of the experiments on the feature

feature_flag_key = feature_flag['key']
unless feature_flag['experimentIds'].empty?
# check if experiment is part of mutex group
experiment_id = feature_flag['experimentIds'][0]
experiment = @config.experiment_id_map[experiment_id]
unless experiment
@config.logger.log(
Logger::DEBUG,
"Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
)
return nil
end

group_id = experiment['groupId']
# if experiment is part of mutex group we first determine which experiment (if any) in the group the user is part of
if group_id and @config.group_key_map.has_key?(group_id)
group = @config.group_key_map[group_id]
bucketed_experiment_id = @bucketer.find_bucket(user_id, group_id, group['trafficAllocation'])
if bucketed_experiment_id.nil?
@config.logger.log(
Logger::INFO,
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
)
return nil
end
else
bucketed_experiment_id = experiment_id
end

if feature_flag['experimentIds'].include?(bucketed_experiment_id)
experiment = @config.experiment_id_map[bucketed_experiment_id]
experiment_key = experiment['key']
variation_id = get_variation(experiment_key, user_id, attributes)
unless variation_id.nil?
variation = @config.variation_id_map[experiment_key][variation_id]
@config.logger.log(
Logger::INFO,
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
)
return variation
else
@config.logger.log(
Logger::INFO,
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
)
end
end
else
@config.logger.log(
Logger::DEBUG,
"The feature flag '#{feature_flag_key}' is not used in any experiments."
)
end

return nil
end

def get_forced_variation_id(experiment_key, user_id)
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
#
Expand Down Expand Up @@ -147,7 +230,7 @@ def get_user_profile(user_id)
#
# user_id - String ID for the user
#
# Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
# Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)

user_profile = {
:user_id => user_id,
Expand Down
19 changes: 8 additions & 11 deletions spec/bucketing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,10 @@ def get_bucketing_id(user_id, entity_id=nil)

expect(bucketer.bucket('group1_exp1', 'test_user')).to eq('130001')
expect(spy_logger).to have_received(:log).exactly(4).times
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned experiment bucket 3000 to user 'test_user'.")
expect(spy_logger).to have_received(:log).twice
.with(Logger::DEBUG, "Assigned bucket 3000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is in experiment 'group1_exp1' of group 101.")
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned variation bucket 3000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is in variation 'g1_e1_v1' of experiment 'group1_exp1'.")
end
Expand All @@ -72,13 +70,12 @@ def get_bucketing_id(user_id, entity_id=nil)

expect(bucketer.bucket('group1_exp2', 'test_user')).to be_nil
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned experiment bucket 3000 to user 'test_user'.")
.with(Logger::DEBUG, "Assigned bucket 3000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is not in experiment 'group1_exp2' of group 101.")
end

it 'should return nil when user is not bucketed into any bucket' do
expect(bucketer).to receive(:generate_bucket_value).once.and_return(3000)
expect(bucketer).to receive(:find_bucket).once.and_return(nil)

expect(bucketer.bucket('group1_exp2', 'test_user')).to be_nil
Expand All @@ -92,7 +89,7 @@ def get_bucketing_id(user_id, entity_id=nil)
expect(bucketer.bucket('group2_exp1', 'test_user')).to eq('144443')
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned variation bucket 3000 to user 'test_user'.")
.with(Logger::DEBUG, "Assigned bucket 3000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is in variation 'g2_e1_v1' of experiment 'group2_exp1'.")
end
Expand All @@ -103,7 +100,7 @@ def get_bucketing_id(user_id, entity_id=nil)
expect(bucketer.bucket('group2_exp1', 'test_user')).to be_nil
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned variation bucket 50000 to user 'test_user'.")
.with(Logger::DEBUG, "Assigned bucket 50000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is in no variation.")
end
Expand Down Expand Up @@ -135,7 +132,7 @@ def get_bucketing_id(user_id, entity_id=nil)

bucketer.bucket('test_experiment', 'test_user')
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log).with(Logger::DEBUG, "Assigned variation bucket 50 to user 'test_user'.")
expect(spy_logger).to have_received(:log).with(Logger::DEBUG, "Assigned bucket 50 to user 'test_user'.")
expect(spy_logger).to have_received(:log).with(
Logger::INFO,
"User 'test_user' is in variation 'control' of experiment 'test_experiment'."
Expand All @@ -148,7 +145,7 @@ def get_bucketing_id(user_id, entity_id=nil)
bucketer.bucket('test_experiment', 'test_user')
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned variation bucket 5050 to user 'test_user'.")
.with(Logger::DEBUG, "Assigned bucket 5050 to user 'test_user'.")
expect(spy_logger).to have_received(:log).with(
Logger::INFO,
"User 'test_user' is in variation 'variation' of experiment 'test_experiment'."
Expand All @@ -161,7 +158,7 @@ def get_bucketing_id(user_id, entity_id=nil)
bucketer.bucket('test_experiment', 'test_user')
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log)
.with(Logger::DEBUG, "Assigned variation bucket 50000 to user 'test_user'.")
.with(Logger::DEBUG, "Assigned bucket 50000 to user 'test_user'.")
expect(spy_logger).to have_received(:log)
.with(Logger::INFO, "User 'test_user' is in no variation.")
end
Expand Down
109 changes: 109 additions & 0 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,113 @@
end
end
end

describe '#get_variation_for_feature' do
user_attributes = {}
user_id = 'user_1'

describe 'when the feature flag\'s experiment ids array is empty' do
it 'should return nil and log a message' do
user_attributes = {}
feature_flag = config.feature_flag_key_map['double_single_variable_feature']
expect(decision_service.get_variation_for_feature(feature_flag, 'user_1', user_attributes)).to eq(nil)

expect(spy_logger).to have_received(:log).once
.with(Logger::DEBUG, "The feature flag 'double_single_variable_feature' is not used in any experiments.")
end
end

describe 'when the feature flag is associated with a non-mutex experiment' do
describe 'and the experiment is not in the datafile' do
it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['boolean_feature'].dup
feature_flag['experimentIds'] = ['1333333337'] # totally invalid exp id
expect(decision_service.get_variation_for_feature(feature_flag, user_id, user_attributes)).to eq(nil)

expect(spy_logger).to have_received(:log).once
.with(Logger::DEBUG, "Feature flag experiment with ID '1333333337' is not in the datafile.")
end
end

describe 'and the user is not bucketed into the feature flag\'s experiments' do
before(:each) do
multivariate_experiment = config.experiment_key_map['test_experiment_multivariate']

# make sure the user is not bucketed into the feature experiment
allow(decision_service).to receive(:get_variation)
.with(multivariate_experiment['key'], 'user_1', user_attributes)
.and_return(nil)
end
it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['multi_variate_feature']
expect(decision_service.get_variation_for_feature(feature_flag, 'user_1', user_attributes)).to eq(nil)

expect(spy_logger).to have_received(:log).once
.with(Logger::INFO, "The user 'user_1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'.")
end
end

describe 'and the user is bucketed into a variation for the experiment on the feature flag' do
before(:each) do
# mock and return the first variation of the `test_experiment_multivariate` experiment, which is attached to the `multi_variate_feature`
allow(decision_service).to receive(:get_variation).and_return('122231')
end

it 'should return the variation' do
user_attributes = {}
feature_flag = config.feature_flag_key_map['multi_variate_feature']
expected_variation = config.variation_id_map['test_experiment_multivariate']['122231']
expect(decision_service.get_variation_for_feature(feature_flag, 'user_1', user_attributes)).to eq(expected_variation)

expect(spy_logger).to have_received(:log).once
.with(Logger::INFO, "The user 'user_1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'.")
end
end
end

describe 'when the feature flag is associated with a mutex experiment' do
mutex_exp = nil
expected_variation = nil
describe 'and the user is bucketed into one of the experiments' do
before(:each) do
group_1 = config.group_key_map['101']
mutex_exp = config.experiment_key_map['group1_exp1']
expected_variation = mutex_exp['variations'][0]
allow(decision_service.bucketer).to receive(:find_bucket)
.with(user_id, group_1['id'], group_1['trafficAllocation'])
.and_return(mutex_exp['id'])

allow(decision_service).to receive(:get_variation)
.and_return(expected_variation['id'])
end

it 'should return the variation the user is bucketed into' do
feature_flag = config.feature_flag_key_map['boolean_feature']
expect(decision_service.get_variation_for_feature(feature_flag, user_id, user_attributes)).to eq(expected_variation)

expect(spy_logger).to have_received(:log).once
.with(Logger::INFO, "The user 'user_1' is bucketed into experiment 'group1_exp1' of feature 'boolean_feature'.")
end
end

describe 'and the user is not bucketed into any of the mutex experiments' do
before(:each) do
group_1 = config.group_key_map['101']
mutex_exp = config.experiment_key_map['group1_exp1']
expected_variation = mutex_exp['variations'][0]
allow(decision_service.bucketer).to receive(:find_bucket)
.with(user_id, group_1['id'], group_1['trafficAllocation'])
.and_return(nil)
end

it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['boolean_feature']
expect(decision_service.get_variation_for_feature(feature_flag, user_id, user_attributes)).to eq(nil)

expect(spy_logger).to have_received(:log).once
.with(Logger::INFO, "The user 'user_1' is not bucketed into any of the experiments on the feature 'boolean_feature'.")
end
end
end
end
end
Loading