optimizely-sdk 1.5.0 → 2.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +0 -47
- data/lib/optimizely/bucketer.rb +16 -18
- data/lib/optimizely/decision_service.rb +120 -135
- data/lib/optimizely/event_builder.rb +142 -158
- data/lib/optimizely/exceptions.rb +0 -9
- data/lib/optimizely/helpers/event_tag_utils.rb +5 -66
- data/lib/optimizely/helpers/validator.rb +0 -5
- data/lib/optimizely/project_config.rb +4 -107
- data/lib/optimizely/version.rb +1 -4
- metadata +7 -6
- data/lib/optimizely/notification_center.rb +0 -148
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6da10a1ad97624a7523adcdc6841b2999ab35d3
|
4
|
+
data.tar.gz: 3865376c1962511db8d8350041fbc830b29b16c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf35c547babd0be1d576bf8926e23fa9e9292b47bf6a74df9a8cda19b00bd2a3da974c7a840dba7d6ac3aa3132b192caa692b727bc5bdfdf3a3a85c933ef58da
|
7
|
+
data.tar.gz: b587d153eb46d6b779ebc298dfde188824f7c5f3fe1cc9c2b4e890878a9c190e9d9051baccbc8d18329670d81c5e5437e53f2d69d4c790a785b8fcb8fa2ac2e3
|
data/lib/optimizely.rb
CHANGED
@@ -24,7 +24,6 @@ require_relative 'optimizely/helpers/group'
|
|
24
24
|
require_relative 'optimizely/helpers/validator'
|
25
25
|
require_relative 'optimizely/helpers/variable_type'
|
26
26
|
require_relative 'optimizely/logger'
|
27
|
-
require_relative 'optimizely/notification_center'
|
28
27
|
require_relative 'optimizely/project_config'
|
29
28
|
|
30
29
|
module Optimizely
|
@@ -39,7 +38,6 @@ module Optimizely
|
|
39
38
|
attr_reader :event_builder
|
40
39
|
attr_reader :event_dispatcher
|
41
40
|
attr_reader :logger
|
42
|
-
attr_reader :notification_center
|
43
41
|
|
44
42
|
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
|
45
43
|
# Constructor for Projects.
|
@@ -85,7 +83,6 @@ module Optimizely
|
|
85
83
|
|
86
84
|
@decision_service = DecisionService.new(@config, @user_profile_service)
|
87
85
|
@event_builder = EventBuilder.new(@config)
|
88
|
-
@notification_center = NotificationCenter.new(@logger, @error_handler)
|
89
86
|
end
|
90
87
|
|
91
88
|
def activate(experiment_key, user_id, attributes = nil)
|
@@ -134,11 +131,6 @@ module Optimizely
|
|
134
131
|
return nil
|
135
132
|
end
|
136
133
|
|
137
|
-
unless user_id.is_a? String
|
138
|
-
@logger.log(Logger::ERROR, "User id: #{user_id} is not a string")
|
139
|
-
return nil
|
140
|
-
end
|
141
|
-
|
142
134
|
unless user_inputs_valid?(attributes)
|
143
135
|
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
|
144
136
|
return nil
|
@@ -155,36 +147,6 @@ module Optimizely
|
|
155
147
|
nil
|
156
148
|
end
|
157
149
|
|
158
|
-
def set_forced_variation(experiment_key, user_id, variation_key)
|
159
|
-
# Force a user into a variation for a given experiment.
|
160
|
-
#
|
161
|
-
# experiment_key - String - key identifying the experiment.
|
162
|
-
# user_id - String - The user ID to be used for bucketing.
|
163
|
-
# variation_key - The variation key specifies the variation which the user will
|
164
|
-
# be forced into. If nil, then clear the existing experiment-to-variation mapping.
|
165
|
-
#
|
166
|
-
# Returns - Boolean - indicates if the set completed successfully.
|
167
|
-
|
168
|
-
@config.set_forced_variation(experiment_key, user_id, variation_key);
|
169
|
-
end
|
170
|
-
|
171
|
-
def get_forced_variation(experiment_key, user_id)
|
172
|
-
# Gets the forced variation for a given user and experiment.
|
173
|
-
#
|
174
|
-
# experiment_key - String - Key identifying the experiment.
|
175
|
-
# user_id - String - The user ID to be used for bucketing.
|
176
|
-
#
|
177
|
-
# Returns String|nil The forced variation key.
|
178
|
-
|
179
|
-
forced_variation_key = nil
|
180
|
-
forced_variation = @config.get_forced_variation(experiment_key, user_id);
|
181
|
-
if forced_variation
|
182
|
-
forced_variation_key = forced_variation['key']
|
183
|
-
end
|
184
|
-
|
185
|
-
forced_variation_key
|
186
|
-
end
|
187
|
-
|
188
150
|
def track(event_key, user_id, attributes = nil, event_tags = nil)
|
189
151
|
# Send conversion event to Optimizely.
|
190
152
|
#
|
@@ -234,10 +196,6 @@ module Optimizely
|
|
234
196
|
rescue => e
|
235
197
|
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
|
236
198
|
end
|
237
|
-
@notification_center.send_notifications(
|
238
|
-
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
|
239
|
-
event_key, user_id, attributes, event_tags, conversion_event
|
240
|
-
)
|
241
199
|
end
|
242
200
|
|
243
201
|
def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
|
@@ -519,11 +477,6 @@ module Optimizely
|
|
519
477
|
rescue => e
|
520
478
|
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
|
521
479
|
end
|
522
|
-
variation = @config.get_variation_from_id(experiment_key, variation_id)
|
523
|
-
@notification_center.send_notifications(
|
524
|
-
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
|
525
|
-
experiment,user_id, attributes, variation, impression_event
|
526
|
-
)
|
527
480
|
end
|
528
481
|
end
|
529
482
|
end
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -20,7 +20,7 @@ module Optimizely
|
|
20
20
|
class Bucketer
|
21
21
|
# Optimizely bucketing algorithm that evenly distributes visitors.
|
22
22
|
|
23
|
-
BUCKETING_ID_TEMPLATE = '%{
|
23
|
+
BUCKETING_ID_TEMPLATE = '%{user_id}%{entity_id}'
|
24
24
|
HASH_SEED = 1
|
25
25
|
MAX_HASH_VALUE = 2**32
|
26
26
|
MAX_TRAFFIC_VALUE = 10_000
|
@@ -35,15 +35,13 @@ module Optimizely
|
|
35
35
|
@config = config
|
36
36
|
end
|
37
37
|
|
38
|
-
def bucket(experiment,
|
38
|
+
def bucket(experiment, user_id)
|
39
39
|
# Determines ID of variation to be shown for a given experiment key and user ID.
|
40
40
|
#
|
41
41
|
# experiment - Experiment for which visitor is to be bucketed.
|
42
|
-
# bucketing_id - String A customer-assigned value used to generate the bucketing key
|
43
42
|
# user_id - String ID for user.
|
44
43
|
#
|
45
44
|
# Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
|
46
|
-
return nil if experiment.nil?
|
47
45
|
|
48
46
|
# check if experiment is in a group; if so, check if user is bucketed into specified experiment
|
49
47
|
experiment_id = experiment['id']
|
@@ -53,7 +51,7 @@ module Optimizely
|
|
53
51
|
group = @config.group_key_map.fetch(group_id)
|
54
52
|
if Helpers::Group.random_policy?(group)
|
55
53
|
traffic_allocations = group.fetch('trafficAllocation')
|
56
|
-
bucketed_experiment_id = find_bucket(
|
54
|
+
bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
|
57
55
|
# return if the user is not bucketed into any experiment
|
58
56
|
unless bucketed_experiment_id
|
59
57
|
@config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
|
@@ -78,7 +76,7 @@ module Optimizely
|
|
78
76
|
end
|
79
77
|
|
80
78
|
traffic_allocations = experiment['trafficAllocation']
|
81
|
-
variation_id = find_bucket(
|
79
|
+
variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
|
82
80
|
if variation_id && variation_id != ''
|
83
81
|
variation = @config.get_variation_from_id(experiment_key, variation_id)
|
84
82
|
variation_key = variation ? variation['key'] : nil
|
@@ -98,18 +96,18 @@ module Optimizely
|
|
98
96
|
nil
|
99
97
|
end
|
100
98
|
|
101
|
-
def find_bucket(
|
99
|
+
def find_bucket(user_id, parent_id, traffic_allocations)
|
102
100
|
# Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
|
103
101
|
#
|
104
|
-
# bucketing_id - String A customer-assigned value user to generate bucketing key
|
105
102
|
# user_id - String ID for user
|
106
103
|
# parent_id - String entity ID to use for bucketing ID
|
107
104
|
# traffic_allocations - Array of traffic allocations
|
108
105
|
#
|
109
106
|
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.
|
110
|
-
|
111
|
-
|
112
|
-
|
107
|
+
|
108
|
+
bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: parent_id)
|
109
|
+
bucket_value = generate_bucket_value(bucketing_id)
|
110
|
+
@config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")
|
113
111
|
|
114
112
|
traffic_allocations.each do |traffic_allocation|
|
115
113
|
current_end_of_range = traffic_allocation['endOfRange']
|
@@ -124,25 +122,25 @@ module Optimizely
|
|
124
122
|
|
125
123
|
private
|
126
124
|
|
127
|
-
def generate_bucket_value(
|
125
|
+
def generate_bucket_value(bucketing_id)
|
128
126
|
# Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
|
129
127
|
#
|
130
|
-
#
|
128
|
+
# bucketing_id - String ID for bucketing.
|
131
129
|
#
|
132
|
-
# Returns bucket value corresponding to the provided bucketing
|
130
|
+
# Returns bucket value corresponding to the provided bucketing ID.
|
133
131
|
|
134
|
-
ratio = (generate_unsigned_hash_code_32_bit(
|
132
|
+
ratio = (generate_unsigned_hash_code_32_bit(bucketing_id)).to_f / MAX_HASH_VALUE
|
135
133
|
(ratio * MAX_TRAFFIC_VALUE).to_i
|
136
134
|
end
|
137
135
|
|
138
|
-
def generate_unsigned_hash_code_32_bit(
|
136
|
+
def generate_unsigned_hash_code_32_bit(bucketing_id)
|
139
137
|
# Helper function to retreive hash code
|
140
138
|
#
|
141
|
-
#
|
139
|
+
# bucketing_id - String ID for bucketing.
|
142
140
|
#
|
143
141
|
# Returns hash code which is a 32 bit unsigned integer.
|
144
142
|
|
145
|
-
MurmurHash3::V32.str_hash(
|
143
|
+
MurmurHash3::V32.str_hash(bucketing_id, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
|
146
144
|
end
|
147
145
|
end
|
148
146
|
end
|
@@ -16,31 +16,27 @@
|
|
16
16
|
require_relative './bucketer'
|
17
17
|
|
18
18
|
module Optimizely
|
19
|
-
|
20
|
-
RESERVED_ATTRIBUTE_KEY_BUCKETING_ID = "\$opt_bucketing_id".freeze
|
21
|
-
|
22
19
|
class DecisionService
|
23
20
|
# Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
|
24
21
|
#
|
25
22
|
# The decision service contains all logic relating to how a user bucketing decisions is made.
|
26
23
|
# This includes all of the following (in order):
|
27
24
|
#
|
28
|
-
# 1.
|
29
|
-
# 2.
|
30
|
-
# 3.
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
25
|
+
# 1. Checking experiment status
|
26
|
+
# 2. Checking whitelisting
|
27
|
+
# 3. Checking user profile service for past bucketing decisions (sticky bucketing)
|
28
|
+
# 3. Checking audience targeting
|
29
|
+
# 4. Using Murmurhash3 to bucket the user
|
30
|
+
|
35
31
|
attr_reader :bucketer
|
36
32
|
attr_reader :config
|
37
|
-
|
33
|
+
|
38
34
|
def initialize(config, user_profile_service = nil)
|
39
35
|
@config = config
|
40
36
|
@user_profile_service = user_profile_service
|
41
37
|
@bucketer = Bucketer.new(@config)
|
42
38
|
end
|
43
|
-
|
39
|
+
|
44
40
|
def get_variation(experiment_key, user_id, attributes = nil)
|
45
41
|
# Determines variation into which user will be bucketed.
|
46
42
|
#
|
@@ -49,65 +45,52 @@ module Optimizely
|
|
49
45
|
# attributes - Hash representing user attributes
|
50
46
|
#
|
51
47
|
# Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
|
52
|
-
|
53
|
-
# By default, the bucketing ID should be the user ID
|
54
|
-
bucketing_id = user_id;
|
55
|
-
|
56
|
-
# If the bucketing ID key is defined in attributes, then use that in place of the userID
|
57
|
-
if attributes and attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].is_a? String
|
58
|
-
unless attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].empty?
|
59
|
-
bucketing_id = attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]
|
60
|
-
@config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
48
|
+
|
64
49
|
# Check to make sure experiment is active
|
65
50
|
experiment = @config.get_experiment_from_key(experiment_key)
|
66
|
-
|
67
|
-
|
51
|
+
if experiment.nil?
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
68
55
|
experiment_id = experiment['id']
|
69
56
|
unless @config.experiment_running?(experiment)
|
70
57
|
@config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
|
71
58
|
return nil
|
72
59
|
end
|
73
|
-
|
74
|
-
# Check if
|
75
|
-
|
76
|
-
return
|
77
|
-
|
78
|
-
# Check if user is in a white-listed variation
|
79
|
-
whitelisted_variation_id = get_whitelisted_variation_id(experiment_key, user_id)
|
80
|
-
return whitelisted_variation_id if whitelisted_variation_id
|
81
|
-
|
60
|
+
|
61
|
+
# Check if user is in a forced variation
|
62
|
+
forced_variation_id = get_forced_variation_id(experiment_key, user_id)
|
63
|
+
return forced_variation_id if forced_variation_id
|
64
|
+
|
82
65
|
# Check for saved bucketing decisions
|
83
66
|
user_profile = get_user_profile(user_id)
|
84
67
|
saved_variation_id = get_saved_variation_id(experiment_id, user_profile)
|
85
68
|
if saved_variation_id
|
86
69
|
@config.logger.log(
|
87
|
-
|
88
|
-
|
70
|
+
Logger::INFO,
|
71
|
+
"Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
|
89
72
|
)
|
90
73
|
return saved_variation_id
|
91
74
|
end
|
92
|
-
|
75
|
+
|
93
76
|
# Check audience conditions
|
94
77
|
unless Audience.user_in_experiment?(@config, experiment, attributes)
|
95
78
|
@config.logger.log(
|
96
|
-
|
97
|
-
|
79
|
+
Logger::INFO,
|
80
|
+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
|
98
81
|
)
|
99
82
|
return nil
|
100
83
|
end
|
101
|
-
|
84
|
+
|
102
85
|
# Bucket normally
|
103
|
-
variation = @bucketer.bucket(experiment,
|
86
|
+
variation = @bucketer.bucket(experiment, user_id)
|
104
87
|
variation_id = variation ? variation['id'] : nil
|
105
|
-
|
88
|
+
|
106
89
|
# Persist bucketing decision
|
107
90
|
save_user_profile(user_profile, experiment_id, variation_id)
|
108
91
|
variation_id
|
109
92
|
end
|
110
|
-
|
93
|
+
|
111
94
|
def get_variation_for_feature(feature_flag, user_id, attributes = nil)
|
112
95
|
# Get the variation the user is bucketed into for the given FeatureFlag.
|
113
96
|
#
|
@@ -116,33 +99,35 @@ module Optimizely
|
|
116
99
|
# attributes - Hash representing user attributes
|
117
100
|
#
|
118
101
|
# Returns hash with the experiment and variation where visitor will be bucketed (nil if the user is not bucketed into any of the experiments on the feature)
|
119
|
-
|
102
|
+
|
120
103
|
# check if the feature is being experiment on and whether the user is bucketed into the experiment
|
121
104
|
decision = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
|
122
|
-
|
123
|
-
|
105
|
+
unless decision.nil?
|
106
|
+
return decision
|
107
|
+
end
|
108
|
+
|
124
109
|
feature_flag_key = feature_flag['key']
|
125
110
|
variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
|
126
111
|
if variation
|
127
112
|
@config.logger.log(
|
128
|
-
|
129
|
-
|
113
|
+
Logger::INFO,
|
114
|
+
"User '#{user_id}' is in the rollout for feature flag '#{feature_flag_key}'."
|
130
115
|
)
|
131
116
|
# return decision with nil experiment so we don't track impressions for it
|
132
117
|
return {
|
133
|
-
|
134
|
-
|
118
|
+
'experiment' => nil,
|
119
|
+
'variation' => variation
|
135
120
|
}
|
136
121
|
else
|
137
122
|
@config.logger.log(
|
138
|
-
|
139
|
-
|
123
|
+
Logger::INFO,
|
124
|
+
"User '#{user_id}' is not in the rollout for feature flag '#{feature_flag_key}'."
|
140
125
|
)
|
141
126
|
end
|
142
|
-
|
127
|
+
|
143
128
|
return nil
|
144
129
|
end
|
145
|
-
|
130
|
+
|
146
131
|
def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
|
147
132
|
# Gets the variation the user is bucketed into for the feature flag's experiment.
|
148
133
|
#
|
@@ -152,7 +137,7 @@ module Optimizely
|
|
152
137
|
#
|
153
138
|
# Returns a hash with the experiment and variation where visitor will be bucketed
|
154
139
|
# or nil if the user is not bucketed into any of the experiments on the feature
|
155
|
-
|
140
|
+
|
156
141
|
feature_flag_key = feature_flag['key']
|
157
142
|
unless feature_flag['experimentIds'].empty?
|
158
143
|
# check if experiment is part of mutex group
|
@@ -160,12 +145,12 @@ module Optimizely
|
|
160
145
|
experiment = @config.experiment_id_map[experiment_id]
|
161
146
|
unless experiment
|
162
147
|
@config.logger.log(
|
163
|
-
|
164
|
-
|
148
|
+
Logger::DEBUG,
|
149
|
+
"Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
|
165
150
|
)
|
166
151
|
return nil
|
167
152
|
end
|
168
|
-
|
153
|
+
|
169
154
|
group_id = experiment['groupId']
|
170
155
|
# if experiment is part of mutex group we first determine which experiment (if any) in the group the user is part of
|
171
156
|
if group_id and @config.group_key_map.has_key?(group_id)
|
@@ -173,15 +158,15 @@ module Optimizely
|
|
173
158
|
bucketed_experiment_id = @bucketer.find_bucket(user_id, group_id, group['trafficAllocation'])
|
174
159
|
if bucketed_experiment_id.nil?
|
175
160
|
@config.logger.log(
|
176
|
-
|
177
|
-
|
161
|
+
Logger::INFO,
|
162
|
+
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
|
178
163
|
)
|
179
164
|
return nil
|
180
165
|
end
|
181
166
|
else
|
182
167
|
bucketed_experiment_id = experiment_id
|
183
168
|
end
|
184
|
-
|
169
|
+
|
185
170
|
if feature_flag['experimentIds'].include?(bucketed_experiment_id)
|
186
171
|
experiment = @config.experiment_id_map[bucketed_experiment_id]
|
187
172
|
experiment_key = experiment['key']
|
@@ -189,30 +174,30 @@ module Optimizely
|
|
189
174
|
unless variation_id.nil?
|
190
175
|
variation = @config.variation_id_map[experiment_key][variation_id]
|
191
176
|
@config.logger.log(
|
192
|
-
|
193
|
-
|
177
|
+
Logger::INFO,
|
178
|
+
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
|
194
179
|
)
|
195
180
|
return {
|
196
|
-
|
197
|
-
|
181
|
+
'variation' => variation,
|
182
|
+
'experiment' => experiment
|
198
183
|
}
|
199
184
|
else
|
200
185
|
@config.logger.log(
|
201
|
-
|
202
|
-
|
186
|
+
Logger::INFO,
|
187
|
+
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
|
203
188
|
)
|
204
189
|
end
|
205
190
|
end
|
206
191
|
else
|
207
192
|
@config.logger.log(
|
208
|
-
|
209
|
-
|
193
|
+
Logger::DEBUG,
|
194
|
+
"The feature flag '#{feature_flag_key}' is not used in any experiments."
|
210
195
|
)
|
211
196
|
end
|
212
|
-
|
197
|
+
|
213
198
|
return nil
|
214
199
|
end
|
215
|
-
|
200
|
+
|
216
201
|
def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
|
217
202
|
# Determine which variation the user is in for a given rollout.
|
218
203
|
# Returns the variation of the first experiment the user qualifies for.
|
@@ -222,110 +207,110 @@ module Optimizely
|
|
222
207
|
# attributes - Hash representing user attributes
|
223
208
|
#
|
224
209
|
# Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
|
225
|
-
|
210
|
+
|
226
211
|
rollout_id = feature_flag['rolloutId']
|
212
|
+
feature_flag_key = feature_flag['key']
|
227
213
|
if rollout_id.nil? or rollout_id.empty?
|
228
|
-
feature_flag_key = feature_flag['key']
|
229
214
|
@config.logger.log(
|
230
|
-
|
231
|
-
|
215
|
+
Logger::DEBUG,
|
216
|
+
"Feature flag '#{feature_flag_key}' is not part of a rollout."
|
232
217
|
)
|
233
218
|
return nil
|
234
219
|
end
|
235
|
-
|
220
|
+
|
236
221
|
rollout = @config.get_rollout_from_id(rollout_id)
|
237
222
|
unless rollout.nil? or rollout['experiments'].empty?
|
238
223
|
rollout_experiments = rollout['experiments']
|
239
224
|
number_of_rules = rollout_experiments.length - 1
|
240
|
-
|
225
|
+
|
241
226
|
# Go through each experiment in order and try to get the variation for the user
|
242
227
|
for index in (0...number_of_rules)
|
243
228
|
experiment = rollout_experiments[index]
|
244
229
|
experiment_key = experiment['key']
|
245
|
-
|
230
|
+
|
246
231
|
# Check that user meets audience conditions for targeting rule
|
247
232
|
unless Audience.user_in_experiment?(@config, experiment, attributes)
|
248
233
|
@config.logger.log(
|
249
|
-
|
250
|
-
|
234
|
+
Logger::DEBUG,
|
235
|
+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}' of rollout with feature flag '#{feature_flag_key}'."
|
251
236
|
)
|
252
237
|
# move onto the next targeting rule
|
253
238
|
next
|
254
239
|
end
|
255
|
-
|
240
|
+
|
256
241
|
@config.logger.log(
|
257
|
-
|
258
|
-
|
242
|
+
Logger::DEBUG,
|
243
|
+
"User '#{user_id}' meets conditions for targeting rule '#{index + 1}'."
|
259
244
|
)
|
260
245
|
variation = @bucketer.bucket(experiment, user_id)
|
261
246
|
unless variation.nil?
|
262
247
|
variation_key = variation['key']
|
263
248
|
return variation
|
264
249
|
end
|
265
|
-
|
250
|
+
|
266
251
|
# User failed traffic allocation, jump to Everyone Else rule
|
267
252
|
@config.logger.log(
|
268
|
-
|
269
|
-
|
253
|
+
Logger::DEBUG,
|
254
|
+
"User '#{user_id}' is not in the traffic group for the targeting rule. Checking 'Eveyrone Else' rule now."
|
270
255
|
)
|
271
256
|
break
|
272
257
|
end
|
273
|
-
|
258
|
+
|
274
259
|
# Evalute the "Everyone Else" rule, which is the last rule.
|
275
260
|
everyone_else_experiment = rollout_experiments[number_of_rules]
|
276
261
|
variation = @bucketer.bucket(everyone_else_experiment, user_id)
|
277
262
|
unless variation.nil?
|
278
263
|
@config.logger.log(
|
279
|
-
|
280
|
-
|
264
|
+
Logger::DEBUG,
|
265
|
+
"User '#{user_id}' meets conditions for targeting rule 'Everyone Else' of rollout with feature flag '#{feature_flag_key}'."
|
281
266
|
)
|
282
267
|
return variation
|
283
268
|
end
|
284
|
-
|
269
|
+
|
285
270
|
@config.logger.log(
|
286
|
-
|
287
|
-
|
271
|
+
Logger::DEBUG,
|
272
|
+
"User '#{user_id}' does not meet conditions for targeting rule 'Everyone Else' of rollout with feature flag '#{feature_flag_key}'."
|
288
273
|
)
|
289
274
|
end
|
290
|
-
|
275
|
+
|
291
276
|
return nil
|
292
277
|
end
|
293
|
-
|
278
|
+
|
294
279
|
private
|
295
|
-
|
296
|
-
def
|
297
|
-
# Determine if a user is
|
280
|
+
|
281
|
+
def get_forced_variation_id(experiment_key, user_id)
|
282
|
+
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
|
298
283
|
#
|
299
284
|
# experiment_key - Key representing the experiment for which user is to be bucketed
|
300
285
|
# user_id - ID for the user
|
301
286
|
#
|
302
|
-
# Returns variation ID into which user_id is
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
return nil unless
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
return nil unless
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
unless
|
287
|
+
# Returns variation ID into which user_id is forced (nil if no variation)
|
288
|
+
|
289
|
+
forced_variations = @config.get_forced_variations(experiment_key)
|
290
|
+
|
291
|
+
return nil unless forced_variations
|
292
|
+
|
293
|
+
forced_variation_key = forced_variations[user_id]
|
294
|
+
|
295
|
+
return nil unless forced_variation_key
|
296
|
+
|
297
|
+
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
|
298
|
+
|
299
|
+
unless forced_variation_id
|
315
300
|
@config.logger.log(
|
316
|
-
|
317
|
-
|
301
|
+
Logger::INFO,
|
302
|
+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}', which is not in the datafile."
|
318
303
|
)
|
319
304
|
return nil
|
320
305
|
end
|
321
|
-
|
306
|
+
|
322
307
|
@config.logger.log(
|
323
|
-
|
324
|
-
|
308
|
+
Logger::INFO,
|
309
|
+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}' of experiment '#{experiment_key}'."
|
325
310
|
)
|
326
|
-
|
311
|
+
forced_variation_id
|
327
312
|
end
|
328
|
-
|
313
|
+
|
329
314
|
def get_saved_variation_id(experiment_id, user_profile)
|
330
315
|
# Retrieve variation ID of stored bucketing decision for a given experiment from a given user profile
|
331
316
|
#
|
@@ -334,56 +319,56 @@ module Optimizely
|
|
334
319
|
#
|
335
320
|
# Returns string variation ID (nil if no decision is found)
|
336
321
|
return nil unless user_profile[:experiment_bucket_map]
|
337
|
-
|
322
|
+
|
338
323
|
decision = user_profile[:experiment_bucket_map][experiment_id]
|
339
324
|
return nil unless decision
|
340
325
|
variation_id = decision[:variation_id]
|
341
326
|
return variation_id if @config.variation_id_exists?(experiment_id, variation_id)
|
342
|
-
|
327
|
+
|
343
328
|
@config.logger.log(
|
344
|
-
|
345
|
-
|
329
|
+
Logger::INFO,
|
330
|
+
"User '#{user_profile['user_id']}' was previously bucketed into variation ID '#{variation_id}' for experiment '#{experiment_id}', but no matching variation was found. Re-bucketing user."
|
346
331
|
)
|
347
332
|
nil
|
348
333
|
end
|
349
|
-
|
334
|
+
|
350
335
|
def get_user_profile(user_id)
|
351
336
|
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
|
352
337
|
#
|
353
338
|
# user_id - String ID for the user
|
354
339
|
#
|
355
340
|
# Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
|
356
|
-
|
341
|
+
|
357
342
|
user_profile = {
|
358
|
-
|
359
|
-
|
343
|
+
:user_id => user_id,
|
344
|
+
:experiment_bucket_map => {}
|
360
345
|
}
|
361
|
-
|
346
|
+
|
362
347
|
return user_profile unless @user_profile_service
|
363
|
-
|
348
|
+
|
364
349
|
begin
|
365
350
|
user_profile = @user_profile_service.lookup(user_id) || user_profile
|
366
351
|
rescue => e
|
367
352
|
@config.logger.log(Logger::ERROR, "Error while looking up user profile for user ID '#{user_id}': #{e}.")
|
368
353
|
end
|
369
|
-
|
354
|
+
|
370
355
|
user_profile
|
371
356
|
end
|
372
|
-
|
373
|
-
|
357
|
+
|
358
|
+
|
374
359
|
def save_user_profile(user_profile, experiment_id, variation_id)
|
375
360
|
# Save a given bucketing decision to a given user profile
|
376
361
|
#
|
377
362
|
# user_profile - Hash user profile
|
378
363
|
# experiment_id - String experiment ID
|
379
364
|
# variation_id - String variation ID
|
380
|
-
|
365
|
+
|
381
366
|
return unless @user_profile_service
|
382
|
-
|
367
|
+
|
383
368
|
user_id = user_profile[:user_id]
|
384
369
|
begin
|
385
370
|
user_profile[:experiment_bucket_map][experiment_id] = {
|
386
|
-
|
371
|
+
:variation_id => variation_id
|
387
372
|
}
|
388
373
|
@user_profile_service.save(user_profile)
|
389
374
|
@config.logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
|
@@ -392,4 +377,4 @@ module Optimizely
|
|
392
377
|
end
|
393
378
|
end
|
394
379
|
end
|
395
|
-
end
|
380
|
+
end
|