Skip to content

Commit 5b38416

Browse files
authored
Add decision service (#46)
1 parent 4edeebf commit 5b38416

File tree

7 files changed

+247
-105
lines changed

7 files changed

+247
-105
lines changed

lib/optimizely.rb

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
#
1616
require_relative 'optimizely/audience'
17-
require_relative 'optimizely/bucketer'
17+
require_relative 'optimizely/decision_service'
1818
require_relative 'optimizely/error_handler'
1919
require_relative 'optimizely/event_builder'
2020
require_relative 'optimizely/event_dispatcher'
@@ -28,14 +28,14 @@ module Optimizely
2828
class Project
2929

3030
# Boolean representing if the instance represents a usable Optimizely Project
31-
attr_reader :is_valid
31+
attr_reader :is_valid
3232

33-
attr_accessor :config
34-
attr_accessor :bucketer
35-
attr_accessor :event_builder
36-
attr_accessor :event_dispatcher
37-
attr_accessor :logger
38-
attr_accessor :error_handler
33+
attr_reader :config
34+
attr_reader :decision_service
35+
attr_reader :error_handler
36+
attr_reader :event_builder
37+
attr_reader :event_dispatcher
38+
attr_reader :logger
3939

4040
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false)
4141
# Constructor for Projects.
@@ -77,7 +77,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
7777
return
7878
end
7979

80-
@bucketer = Bucketer.new(@config)
80+
@decision_service = DecisionService.new(@config)
8181
@event_builder = EventBuilderV2.new(@config)
8282
end
8383

@@ -135,24 +135,13 @@ def get_variation(experiment_key, user_id, attributes = nil)
135135
return nil
136136
end
137137

138-
unless preconditions_valid?(experiment_key, attributes)
138+
unless user_inputs_valid?(attributes)
139139
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
140140
return nil
141141
end
142142

143-
variation_id = @bucketer.get_forced_variation_id(experiment_key, user_id)
143+
variation_id = @decision_service.get_variation(experiment_key, user_id, attributes)
144144

145-
unless variation_id.nil?
146-
return @config.get_variation_key_from_id(experiment_key, variation_id)
147-
end
148-
149-
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
150-
@logger.log(Logger::INFO,
151-
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'.")
152-
return nil
153-
end
154-
155-
variation_id = @bucketer.bucket(experiment_key, user_id)
156145
unless variation_id.nil?
157146
return @config.get_variation_key_from_id(experiment_key, variation_id)
158147
end
@@ -240,25 +229,6 @@ def get_valid_experiments_for_event(event_key, user_id, attributes)
240229
valid_experiments
241230
end
242231

243-
def preconditions_valid?(experiment_key, attributes = nil, event_tags = nil)
244-
# Validates preconditions for bucketing a user.
245-
#
246-
# experiment_key - String key for an experiment.
247-
# user_id - String ID of user.
248-
# attributes - Hash of user attributes.
249-
#
250-
# Returns boolean representing whether all preconditions are valid.
251-
252-
return false unless user_inputs_valid?(attributes, event_tags)
253-
254-
unless @config.experiment_running?(experiment_key)
255-
@logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
256-
return false
257-
end
258-
259-
true
260-
end
261-
262232
def user_inputs_valid?(attributes = nil, event_tags = nil)
263233
# Helper method to validate user inputs.
264234
#

lib/optimizely/bucketer.rb

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -101,36 +101,6 @@ def bucket(experiment_key, user_id)
101101
nil
102102
end
103103

104-
def get_forced_variation_id(experiment_key, user_id)
105-
# Determine if a user is forced into a variation for the given experiment and return the id of that variation.
106-
#
107-
# experiment_key - Key representing the experiment for which user is to be bucketed.
108-
# user_id - ID for the user.
109-
#
110-
# Returns variation ID in which the user with ID user_id is forced into. Nil if no variation.
111-
112-
forced_variations = @config.get_forced_variations(experiment_key)
113-
114-
return nil unless forced_variations
115-
116-
forced_variation_key = forced_variations[user_id]
117-
118-
return nil unless forced_variation_key
119-
120-
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
121-
122-
unless forced_variation_id
123-
@config.logger.log(
124-
Logger::INFO,
125-
"Variation key '#{forced_variation_key}' is not in datafile. Not activating user '#{user_id}'."
126-
)
127-
return nil
128-
end
129-
130-
@config.logger.log(Logger::INFO, "User '#{user_id}' is forced in variation '#{forced_variation_key}'.")
131-
forced_variation_id
132-
end
133-
134104
private
135105

136106
def find_bucket(bucket_value, traffic_allocations)

lib/optimizely/decision_service.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#
2+
# Copyright 2017, Optimizely and contributors
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
require_relative './bucketer'
17+
18+
module Optimizely
19+
class DecisionService
20+
# Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
21+
#
22+
# The decision service contains all logic relating to how a user bucketing decisions is made.
23+
# This includes all of the following (in order):
24+
#
25+
# 1. Checking experiment status
26+
# 2. Checking whitelisting
27+
# 3. Checking audience targeting
28+
# 4. Using Murmurhash3 to bucket the user
29+
30+
attr_reader :bucketer
31+
attr_reader :config
32+
33+
def initialize(config)
34+
@config = config
35+
@bucketer = Bucketer.new(@config)
36+
end
37+
38+
def get_variation(experiment_key, user_id, attributes = nil)
39+
# Determines variation into which user will be bucketed.
40+
#
41+
# experiment_key - Experiment for which visitor variation needs to be determined
42+
# user_id - String ID for user
43+
# attributes - Hash representing user attributes
44+
#
45+
# Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
46+
47+
# Check to make sure experiment is active
48+
unless @config.experiment_running?(experiment_key)
49+
@config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
50+
return nil
51+
end
52+
53+
# Check if user is in a forced variation
54+
variation_id = get_forced_variation_id(experiment_key, user_id)
55+
56+
if variation_id.nil?
57+
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
58+
@config.logger.log(
59+
Logger::INFO,
60+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
61+
)
62+
return nil
63+
end
64+
65+
variation_id = @bucketer.bucket(experiment_key, user_id)
66+
end
67+
68+
variation_id
69+
end
70+
71+
private
72+
73+
def get_forced_variation_id(experiment_key, user_id)
74+
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
75+
#
76+
# experiment_key - Key representing the experiment for which user is to be bucketed
77+
# user_id - ID for the user
78+
#
79+
# Returns variation ID into which user_id is forced (nil if no variation)
80+
81+
forced_variations = @config.get_forced_variations(experiment_key)
82+
83+
return nil unless forced_variations
84+
85+
forced_variation_key = forced_variations[user_id]
86+
87+
return nil unless forced_variation_key
88+
89+
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
90+
91+
unless forced_variation_id
92+
@config.logger.log(
93+
Logger::INFO,
94+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}', which is not in the datafile."
95+
)
96+
return nil
97+
end
98+
99+
@config.logger.log(
100+
Logger::INFO,
101+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}' of experiment '#{experiment_key}'."
102+
)
103+
forced_variation_id
104+
end
105+
end
106+
end

lib/optimizely/event_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def ==(event)
4242

4343
class BaseEventBuilder
4444
attr_reader :config
45-
attr_accessor :params
45+
attr_reader :params
4646

4747
def initialize(config)
4848
@config = config

spec/bucketing_spec.rb

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -129,32 +129,6 @@ def get_bucketing_id(user_id, entity_id=nil)
129129
.with(Logger::DEBUG, "Bucketed into an empty traffic range. Returning nil.")
130130
end
131131

132-
describe '#get_forced_variation_id' do
133-
it 'should return correct variation ID if user ID is in forcedVariations and variation is valid' do
134-
expect(bucketer.get_forced_variation_id('test_experiment', 'forced_user1')).to eq('111128')
135-
expect(spy_logger).to have_received(:log)
136-
.once.with(Logger::INFO, "User 'forced_user1' is forced in variation 'control'.")
137-
138-
expect(bucketer.get_forced_variation_id('test_experiment', 'forced_user2')).to eq('111129')
139-
expect(spy_logger).to have_received(:log)
140-
.once.with(Logger::INFO, "User 'forced_user2' is forced in variation 'variation'.")
141-
end
142-
143-
it 'should return null if forced variation ID is not in the datafile' do
144-
expect(bucketer.get_forced_variation_id('test_experiment', 'forced_user_with_invalid_variation')).to be_nil
145-
end
146-
147-
it 'should respect forced variations within mutually exclusive grouped experiments' do
148-
expect(bucketer).not_to receive(:generate_bucket_value)
149-
150-
expect(bucketer.get_forced_variation_id('group1_exp2', 'forced_group_user1')).to eq('130004')
151-
expect(spy_logger).to have_received(:log)
152-
.once.with(Logger::INFO, "User 'forced_group_user1' is forced in variation 'g1_e2_v2'.")
153-
end
154-
155-
156-
end
157-
158132
describe 'logging' do
159133
it 'should log the results of bucketing a user into variation 1' do
160134
expect(bucketer).to receive(:generate_bucket_value).and_return(50)

0 commit comments

Comments
 (0)