optimizely-sdk 1.5.0 → 2.0.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|