Skip to content

Commit 090136e

Browse files
authored
Refactor order of bucketing operations. (#40)
1 parent 1d7122c commit 090136e

File tree

8 files changed

+197
-116
lines changed

8 files changed

+197
-116
lines changed

lib/optimizely.rb

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
5757
@event_dispatcher = event_dispatcher || EventDispatcher.new
5858

5959
begin
60-
validate_inputs(datafile, skip_json_validation)
60+
validate_instantiation_options(datafile, skip_json_validation)
6161
rescue InvalidInputError => e
6262
@is_valid = false
6363
logger = SimpleLogger.new
@@ -82,7 +82,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
8282
end
8383

8484
@bucketer = Bucketer.new(@config)
85-
@event_builder = EVENT_BUILDERS_BY_VERSION[@config.version].new(@config, @bucketer)
85+
@event_builder = EVENT_BUILDERS_BY_VERSION[@config.version].new(@config)
8686
end
8787

8888
def activate(experiment_key, user_id, attributes = nil)
@@ -101,24 +101,15 @@ def activate(experiment_key, user_id, attributes = nil)
101101
return nil
102102
end
103103

104-
if attributes && !attributes_valid?(attributes)
105-
@logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
106-
return nil
107-
end
108-
109-
unless preconditions_valid?(experiment_key, user_id, attributes)
110-
@logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
111-
return nil
112-
end
113-
114-
variation_id = @bucketer.bucket(experiment_key, user_id)
104+
variation_key = get_variation(experiment_key, user_id, attributes)
115105

116-
if not variation_id
106+
if variation_key.nil?
117107
@logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
118108
return nil
119109
end
120110

121111
# Create and dispatch impression event
112+
variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
122113
impression_event = @event_builder.create_impression_event(experiment_key, variation_id, user_id, attributes)
123114
@logger.log(Logger::INFO,
124115
'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
@@ -129,7 +120,7 @@ def activate(experiment_key, user_id, attributes = nil)
129120
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
130121
end
131122

132-
@config.get_variation_key_from_id(experiment_key, variation_id)
123+
variation_key
133124
end
134125

135126
def get_variation(experiment_key, user_id, attributes = nil)
@@ -148,24 +139,34 @@ def get_variation(experiment_key, user_id, attributes = nil)
148139
return nil
149140
end
150141

151-
if attributes && !attributes_valid?(attributes)
142+
unless preconditions_valid?(experiment_key, attributes)
152143
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
153144
return nil
154145
end
155146

156-
unless preconditions_valid?(experiment_key, user_id, attributes)
157-
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
147+
variation_id = @bucketer.get_forced_variation_id(experiment_key, user_id)
148+
149+
unless variation_id.nil?
150+
return @config.get_variation_key_from_id(experiment_key, variation_id)
151+
end
152+
153+
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
154+
@logger.log(Logger::INFO,
155+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'.")
158156
return nil
159157
end
160158

161159
variation_id = @bucketer.bucket(experiment_key, user_id)
162-
@config.get_variation_key_from_id(experiment_key, variation_id)
160+
unless variation_id.nil?
161+
return @config.get_variation_key_from_id(experiment_key, variation_id)
162+
end
163+
nil
163164
end
164165

165166
def track(event_key, user_id, attributes = nil, event_tags = nil)
166167
# Send conversion event to Optimizely.
167168
#
168-
# event_key - Goal key representing the event which needs to be recorded.
169+
# event_key - Event key representing the event which needs to be recorded.
169170
# user_id - String ID for user.
170171
# attributes - Hash representing visitor attributes and values which need to be recorded.
171172
# event_tags - Hash representing metadata associated with the event.
@@ -183,34 +184,26 @@ def track(event_key, user_id, attributes = nil, event_tags = nil)
183184
@logger.log(Logger::WARN, 'Event value is deprecated in track call. Use event tags to pass in revenue value instead.')
184185
end
185186

186-
return nil if attributes && !attributes_valid?(attributes)
187-
return nil if event_tags && !event_tags_valid?(event_tags)
187+
return nil unless user_inputs_valid?(attributes, event_tags)
188188

189-
experiment_ids = @config.get_experiment_ids_for_goal(event_key)
189+
experiment_ids = @config.get_experiment_ids_for_event(event_key)
190190
if experiment_ids.empty?
191191
@config.logger.log(Logger::INFO, "Not tracking user '#{user_id}'.")
192192
return nil
193193
end
194194

195195
# Filter out experiments that are not running or that do not include the user in audience conditions
196-
valid_experiment_keys = []
197-
experiment_ids.each do |experiment_id|
198-
experiment_key = @config.experiment_id_map[experiment_id]['key']
199-
unless preconditions_valid?(experiment_key, user_id, attributes)
200-
@config.logger.log(Logger::INFO, "Not tracking user '#{user_id}' for experiment '#{experiment_key}'.")
201-
next
202-
end
203-
valid_experiment_keys.push(experiment_key)
204-
end
196+
197+
experiment_variation_map = get_valid_experiments_for_event(event_key, user_id, attributes)
205198

206199
# Don't track events without valid experiments attached
207-
if valid_experiment_keys.empty?
200+
if experiment_variation_map.empty?
208201
@logger.log(Logger::INFO, "There are no valid experiments for event '#{event_key}' to track.")
209202
return nil
210203
end
211204

212205
conversion_event = @event_builder.create_conversion_event(event_key, user_id, attributes,
213-
event_tags, valid_experiment_keys)
206+
event_tags, experiment_variation_map)
214207
@logger.log(Logger::INFO,
215208
'Dispatching conversion event to URL %s with params %s.' % [conversion_event.url,
216209
conversion_event.params])
@@ -223,7 +216,35 @@ def track(event_key, user_id, attributes = nil, event_tags = nil)
223216

224217
private
225218

226-
def preconditions_valid?(experiment_key, user_id, attributes)
219+
def get_valid_experiments_for_event(event_key, user_id, attributes)
220+
# Get the experiments that we should be tracking for the given event.
221+
#
222+
# event_key - Event key representing the event which needs to be recorded.
223+
# user_id - String ID for user.
224+
# attributes - Map of attributes of the user.
225+
#
226+
# Returns Map where each object contains the ID of the experiment to track and the ID of the variation the user
227+
# is bucketed into.
228+
229+
valid_experiments = {}
230+
experiment_ids = @config.get_experiment_ids_for_event(event_key)
231+
experiment_ids.each do |experiment_id|
232+
experiment_key = @config.get_experiment_key(experiment_id)
233+
variation_key = get_variation(experiment_key, user_id, attributes)
234+
235+
if variation_key.nil?
236+
@logger.log(Logger::INFO, "Not tracking user '#{user_id}' for experiment '#{experiment_key}'.")
237+
next
238+
end
239+
240+
variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
241+
valid_experiments[experiment_id] = variation_id
242+
end
243+
244+
valid_experiments
245+
end
246+
247+
def preconditions_valid?(experiment_key, attributes = nil, event_tags = nil)
227248
# Validates preconditions for bucketing a user.
228249
#
229250
# experiment_key - String key for an experiment.
@@ -232,18 +253,29 @@ def preconditions_valid?(experiment_key, user_id, attributes)
232253
#
233254
# Returns boolean representing whether all preconditions are valid.
234255

256+
return false unless user_inputs_valid?(attributes, event_tags)
257+
235258
unless @config.experiment_running?(experiment_key)
236259
@logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
237260
return false
238261
end
239262

240-
if @config.user_in_forced_variation?(experiment_key, user_id)
241-
return true
263+
true
264+
end
265+
266+
def user_inputs_valid?(attributes = nil, event_tags = nil)
267+
# Helper method to validate user inputs.
268+
#
269+
# attributes - Dict representing user attributes.
270+
# event_tags - Dict representing metadata associated with an event.
271+
#
272+
# Returns boolean True if inputs are valid. False otherwise.
273+
274+
if !attributes.nil? && !attributes_valid?(attributes)
275+
return false
242276
end
243277

244-
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
245-
@logger.log(Logger::INFO,
246-
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'.")
278+
if !event_tags.nil? && !event_tags_valid?(event_tags)
247279
return false
248280
end
249281

@@ -268,7 +300,7 @@ def event_tags_valid?(event_tags)
268300
true
269301
end
270302

271-
def validate_inputs(datafile, skip_json_validation)
303+
def validate_instantiation_options(datafile, skip_json_validation)
272304
unless skip_json_validation
273305
raise InvalidInputError.new('datafile') unless Helpers::Validator.datafile_valid?(datafile)
274306
end

lib/optimizely/bucketer.rb

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright 2016, Optimizely and contributors
2+
# Copyright 2016-2017, Optimizely and contributors
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -43,23 +43,6 @@ def bucket(experiment_key, user_id)
4343
#
4444
# Returns String variation ID in which visitor with ID user_id has been placed. Nil if no variation.
4545

46-
# handle forced variations if applicable
47-
forced_variations = @config.get_forced_variations(experiment_key)
48-
forced_variation_key = forced_variations[user_id]
49-
if forced_variation_key
50-
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
51-
if forced_variation_id
52-
@config.logger.log(Logger::INFO, "User '#{user_id}' is forced in variation '#{forced_variation_key}'.")
53-
return forced_variation_id
54-
else
55-
@config.logger.log(
56-
Logger::INFO,
57-
"Variation key '#{forced_variation_key}' is not in datafile. Not activating user '#{user_id}'."
58-
)
59-
return nil
60-
end
61-
end
62-
6346
# check if experiment is in a group; if so, check if user is bucketed into specified experiment
6447
experiment_id = @config.get_experiment_id(experiment_key)
6548
group_id = @config.get_experiment_group_id(experiment_key)
@@ -118,6 +101,36 @@ def bucket(experiment_key, user_id)
118101
nil
119102
end
120103

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+
121134
private
122135

123136
def find_bucket(bucket_value, traffic_allocations)

lib/optimizely/event_builder.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ def ==(event)
4242

4343
class BaseEventBuilder
4444
attr_reader :config
45-
attr_reader :bucketer
4645
attr_accessor :params
4746

48-
def initialize(config, bucketer)
47+
def initialize(config)
4948
@config = config
50-
@bucketer = bucketer
5149
@params = {}
5250
end
5351

@@ -90,22 +88,22 @@ def create_impression_event(experiment_key, variation_id, user_id, attributes)
9088
Event.new(:post, IMPRESSION_EVENT_ENDPOINT, @params, POST_HEADERS)
9189
end
9290

93-
def create_conversion_event(event_key, user_id, attributes, event_tags, experiment_keys)
91+
def create_conversion_event(event_key, user_id, attributes, event_tags, experiment_variation_map)
9492
# Create conversion Event to be sent to the logging endpoint.
9593
#
9694
# event_key - Event key representing the event which needs to be recorded.
9795
# user_id - ID for user.
9896
# attributes - Hash representing user attributes and values which need to be recorded.
9997
# event_tags - Hash representing metadata associated with the event.
100-
# experiment_keys - Array of valid experiment keys for the event
98+
# experiment_variation_map - Map of experiment ID to the ID of the variation that the user is bucketed into.
10199
#
102100
# Returns event hash encapsulating the conversion event.
103101

104102
@params = {}
105103
add_common_params(user_id, attributes)
106104
add_conversion_event(event_key)
107105
add_event_tags(event_tags)
108-
add_layer_states(user_id, experiment_keys)
106+
add_layer_states(experiment_variation_map)
109107
Event.new(:post, CONVERSION_EVENT_ENDPOINT, @params, POST_HEADERS)
110108
end
111109

@@ -206,14 +204,16 @@ def add_conversion_event(event_key)
206204
@params['eventName'] = event_name
207205
end
208206

209-
def add_layer_states(user_id, experiment_keys)
207+
def add_layer_states(experiments_map)
208+
# Add layer states information to the event.
209+
#
210+
# experiments_map - Hash with experiment ID as a key and variation ID as a value.
211+
210212
@params['layerStates'] = []
211213

212-
experiment_keys.each do |experiment_key|
213-
variation_id = @bucketer.bucket(experiment_key, user_id)
214-
experiment_id = @config.experiment_key_map[experiment_key]['id']
214+
experiments_map.each do |experiment_id, variation_id|
215215
layer_state = {
216-
'layerId' => @config.experiment_key_map[experiment_key]['layerId'],
216+
'layerId' => @config.experiment_id_map[experiment_id]['layerId'],
217217
'decision' => {
218218
'variationId' => variation_id,
219219
'experimentId' => experiment_id,

lib/optimizely/project_config.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,30 @@ def get_experiment_id(experiment_key)
131131
nil
132132
end
133133

134-
def get_experiment_ids_for_goal(goal_key)
135-
# Get experiment IDs for the provided goal key.
134+
def get_experiment_key(experiment_id)
135+
# Retrieves experiment key for a given ID.
136136
#
137-
# goal_key - Goal key for which experiment IDs are to be retrieved.
137+
# experiment_id - String ID representing the experiment.
138138
#
139-
# Returns array of all experiment IDs for the goal.
139+
# Returns String key.
140140

141-
goal = @event_key_map[goal_key]
142-
return goal['experimentIds'] if goal
143-
@logger.log Logger::ERROR, "Event '#{goal_key}' is not in datafile."
141+
experiment = @experiment_id_map[experiment_id]
142+
return experiment['key'] unless experiment.nil?
143+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
144+
@error_handler.handle_error InvalidExperimentError
145+
nil
146+
end
147+
148+
def get_experiment_ids_for_event(event_key)
149+
# Get experiment IDs for the provided event key.
150+
#
151+
# event_key - Event key for which experiment IDs are to be retrieved.
152+
#
153+
# Returns array of all experiment IDs for the event.
154+
155+
event = @event_key_map[event_key]
156+
return event['experimentIds'] if event
157+
@logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
144158
@error_handler.handle_error InvalidEventError
145159
[]
146160
end

0 commit comments

Comments
 (0)