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
50 changes: 29 additions & 21 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def get_variation(experiment_key, user_id, attributes = nil)
}, @logger, Logger::ERROR
)

experiment = @config.get_experiment_from_key(experiment_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil if experiment.nil?

unless user_inputs_valid?(attributes)
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
return nil
Expand All @@ -145,10 +148,14 @@ def get_variation(experiment_key, user_id, attributes = nil)
variation_id = @decision_service.get_variation(experiment_key, user_id, attributes)
variation = @config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
variation_key = variation['key'] if variation

decision_notification_type = if @config.feature_experiment?(experiment['id'])
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
else
Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
end
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_INFO_TYPES['EXPERIMENT'], user_id, (attributes || {}),
decision_notification_type, user_id, (attributes || {}),
experiment_key: experiment_key,
variation_key: variation_key
)
Expand Down Expand Up @@ -264,14 +271,16 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)

feature_enabled = false
source_string = Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
if decision.is_a?(Optimizely::DecisionService::Decision)
variation = decision['variation']
feature_enabled = variation['featureEnabled']
if decision.source == Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
source_string = Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
experiment_key = decision.experiment['key']
variation_key = variation['key']
if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_info = {
experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
# Send event if Decision came from an experiment.
send_impression(decision.experiment, variation['key'], user_id, attributes)
else
Expand All @@ -282,13 +291,12 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)

@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_INFO_TYPES['FEATURE'],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
user_id, (attributes || {}),
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string.upcase,
source_experiment_key: experiment_key,
source_variation_key: variation_key
source: source_string,
source_info: source_info || {}
)

if feature_enabled == true
Expand Down Expand Up @@ -481,22 +489,23 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
return nil if variable.nil?

feature_enabled = false

# Returns nil if type differs
if variable['type'] != variable_type
@logger.log(Logger::WARN,
"Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
return nil
else
source_string = Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
variable_value = variable['defaultValue']
if decision
variation = decision['variation']
if decision['source'] == Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
experiment_key = decision.experiment['key']
variation_key = variation['key']
source_string = Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
if decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_info = {
experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
end
feature_enabled = variation['featureEnabled']
if feature_enabled == true
Expand Down Expand Up @@ -524,15 +533,14 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,

@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_INFO_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string,
variable_key: variable_key,
variable_type: variable_type,
variable_value: variable_value,
source: source_string.upcase,
source_experiment_key: experiment_key,
source_variation_key: variation_key
source_info: source_info || {}
)

variable_value
Expand Down
15 changes: 9 additions & 6 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2017-2018, Optimizely and contributors
# Copyright 2017-2019, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,8 +35,11 @@ class DecisionService
attr_reader :config

Decision = Struct.new(:experiment, :variation, :source)
DECISION_SOURCE_EXPERIMENT = 'experiment'
DECISION_SOURCE_ROLLOUT = 'rollout'

DECISION_SOURCES = {
'FEATURE_TEST' => 'feature-test',
'ROLLOUT' => 'rollout'
}.freeze

def initialize(config, user_profile_service = nil)
@config = config
Expand Down Expand Up @@ -172,7 +175,7 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
Logger::INFO,
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
)
return Decision.new(experiment, variation, DECISION_SOURCE_EXPERIMENT)
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'])
end

@config.logger.log(
Expand Down Expand Up @@ -236,7 +239,7 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)

# Evaluate if user satisfies the traffic allocation for this rollout rule
variation = @bucketer.bucket(rollout_rule, bucketing_id, user_id)
return Decision.new(rollout_rule, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?

break
end
Expand All @@ -255,7 +258,7 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
return nil
end
variation = @bucketer.bucket(everyone_else_experiment, bucketing_id, user_id)
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?

nil
end
Expand Down
7 changes: 4 additions & 3 deletions lib/optimizely/helpers/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,11 @@ module Constants
'to upgrade to a newer release of the Optimizely SDK.'
}.freeze

DECISION_INFO_TYPES = {
'EXPERIMENT' => 'experiment',
DECISION_NOTIFICATION_TYPES = {
'AB_TEST' => 'ab-test',
'FEATURE' => 'feature',
'FEATURE_VARIABLE' => 'feature_variable'
'FEATURE_TEST' => 'feature-test',
'FEATURE_VARIABLE' => 'feature-variable'
}.freeze
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/optimizely/project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ProjectConfig
attr_reader :attribute_key_map
attr_reader :audience_id_map
attr_reader :event_key_map
attr_reader :experiment_feature_map
attr_reader :experiment_id_map
attr_reader :experiment_key_map
attr_reader :feature_flag_key_map
Expand Down Expand Up @@ -140,9 +141,13 @@ def initialize(datafile, logger, error_handler)
@variation_key_map[key] = generate_key_map(variations, 'key')
end
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
@experiment_feature_map = {}
@feature_variable_key_map = {}
@feature_flag_key_map.each do |key, feature_flag|
@feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
feature_flag['experimentIds'].each do |experiment_id|
@experiment_feature_map[experiment_id] = [feature_flag['id']]
end
end
end

Expand Down Expand Up @@ -451,6 +456,16 @@ def get_rollout_from_id(rollout_id)
nil
end

def feature_experiment?(experiment_id)
# Determines if given experiment is a feature test.
#
# experiment_id - String experiment ID
#
# Returns true if experiment belongs to any feature,
# false otherwise.
@experiment_feature_map.key?(experiment_id)
end

private

def generate_key_map(array, key)
Expand Down
22 changes: 11 additions & 11 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2017-2018, Optimizely and contributors
# Copyright 2017-2019, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -388,7 +388,7 @@
expected_decision = Optimizely::DecisionService::Decision.new(
config.experiment_key_map['test_experiment_multivariate'],
config.variation_id_map['test_experiment_multivariate']['122231'],
Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
expect(decision_service.get_variation_for_feature_experiment(feature_flag, 'user_1', user_attributes)).to eq(expected_decision)

Expand All @@ -408,18 +408,18 @@
expected_decision = Optimizely::DecisionService::Decision.new(
mutex_exp,
variation,
Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
allow(decision_service).to receive(:get_variation)
.and_return(variation['id'])
end

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

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'.")
.with(Logger::INFO, "The user 'user_1' is bucketed into experiment 'group1_exp1' of feature 'mutex_group_feature'.")
end
end

Expand All @@ -436,11 +436,11 @@
end

it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['boolean_feature']
feature_flag = config.feature_flag_key_map['mutex_group_feature']
expect(decision_service.get_variation_for_feature_experiment(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'.")
.with(Logger::INFO, "The user 'user_1' is not bucketed into any of the experiments on the feature 'mutex_group_feature'.")
end
end
end
Expand Down Expand Up @@ -487,7 +487,7 @@
feature_flag = config.feature_flag_key_map['boolean_single_variable_feature']
rollout_experiment = config.rollout_id_map[feature_flag['rolloutId']]['experiments'][0]
variation = rollout_experiment['variations'][0]
expected_decision = Optimizely::DecisionService::Decision.new(rollout_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
expected_decision = Optimizely::DecisionService::Decision.new(rollout_experiment, variation, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'])
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(true)
allow(decision_service.bucketer).to receive(:bucket)
.with(rollout_experiment, user_id, user_id)
Expand Down Expand Up @@ -527,7 +527,7 @@
rollout = config.rollout_id_map[feature_flag['rolloutId']]
everyone_else_experiment = rollout['experiments'][2]
variation = everyone_else_experiment['variations'][0]
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'])
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(true)
allow(decision_service.bucketer).to receive(:bucket)
.with(rollout['experiments'][0], user_id, user_id)
Expand All @@ -554,7 +554,7 @@
rollout = config.rollout_id_map[feature_flag['rolloutId']]
everyone_else_experiment = rollout['experiments'][2]
variation = everyone_else_experiment['variations'][0]
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'])
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(false)

allow(Optimizely::Audience).to receive(:user_in_experiment?)
Expand Down Expand Up @@ -652,7 +652,7 @@
expected_decision = Optimizely::DecisionService::Decision.new(
nil,
variation,
Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT
Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
)
allow(decision_service).to receive(:get_variation_for_feature_experiment).and_return(nil)
allow(decision_service).to receive(:get_variation_for_feature_rollout).and_return(expected_decision)
Expand Down
25 changes: 25 additions & 0 deletions spec/project_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@
'test_event_not_running' => config_body['events'][3]
}

expected_experiment_feature_map = {
'122227' => [config_body['featureFlags'][0]['id']],
'133331' => [config_body['featureFlags'][6]['id']],
'133332' => [config_body['featureFlags'][6]['id']],
'122238' => [config_body['featureFlags'][1]['id']],
'122241' => [config_body['featureFlags'][2]['id']],
'122235' => [config_body['featureFlags'][4]['id']],
'122230' => [config_body['featureFlags'][5]['id']]
}

expected_experiment_key_map = {
'test_experiment' => config_body['experiments'][0],
'test_experiment_not_started' => config_body['experiments'][1],
Expand Down Expand Up @@ -650,6 +660,7 @@
expect(project_config.attribute_key_map).to eq(expected_attribute_key_map)
expect(project_config.audience_id_map).to eq(expected_audience_id_map)
expect(project_config.event_key_map).to eq(expected_event_key_map)
expect(project_config.experiment_feature_map).to eq(expected_experiment_feature_map)
expect(project_config.experiment_key_map).to eq(expected_experiment_key_map)
expect(project_config.feature_flag_key_map).to eq(expected_feature_flag_key_map)
expect(project_config.feature_variable_key_map).to eq(expected_feature_variable_key_map)
Expand Down Expand Up @@ -1069,4 +1080,18 @@
expect(config.get_attribute_id('$opt_user_agent')).to eq('$opt_user_agent')
end
end

describe '#feature_experiment' do
let(:config) { Optimizely::ProjectConfig.new(config_body_JSON, logger, error_handler) }

it 'should return true if the experiment is a feature test' do
experiment = config.get_experiment_from_key('test_experiment_double_feature')
expect(config.feature_experiment?(experiment['id'])).to eq(true)
end

it 'should return false if the experiment is not a feature test' do
experiment = config.get_experiment_from_key('test_experiment')
expect(config.feature_experiment?(experiment['id'])).to eq(false)
end
end
end
Loading