optimizely-sdk 3.6.0 → 3.9.0
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/audience.rb +18 -39
- data/lib/optimizely/bucketer.rb +35 -27
- data/lib/optimizely/condition_tree_evaluator.rb +2 -0
- data/lib/optimizely/config/datafile_project_config.rb +97 -14
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -0
- data/lib/optimizely/decide/optimizely_decision.rb +60 -0
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -0
- data/lib/optimizely/decision_service.rb +164 -141
- data/lib/optimizely/event/entity/decision.rb +6 -4
- data/lib/optimizely/event/entity/impression_event.rb +4 -2
- data/lib/optimizely/event/event_factory.rb +4 -3
- data/lib/optimizely/event/user_event_factory.rb +4 -3
- data/lib/optimizely/helpers/constants.rb +1 -0
- data/lib/optimizely/optimizely_config.rb +180 -25
- data/lib/optimizely/optimizely_user_context.rb +107 -0
- data/lib/optimizely/project_config.rb +14 -2
- data/lib/optimizely/version.rb +1 -1
- data/lib/optimizely.rb +245 -18
- metadata +7 -3
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2020, Optimizely and contributors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'json'
|
19
|
+
|
20
|
+
module Optimizely
|
21
|
+
module Decide
|
22
|
+
class OptimizelyDecision
|
23
|
+
attr_reader :variation_key, :enabled, :variables, :rule_key, :flag_key, :user_context, :reasons
|
24
|
+
|
25
|
+
def initialize(
|
26
|
+
variation_key: nil,
|
27
|
+
enabled: nil,
|
28
|
+
variables: nil,
|
29
|
+
rule_key: nil,
|
30
|
+
flag_key: nil,
|
31
|
+
user_context: nil,
|
32
|
+
reasons: nil
|
33
|
+
)
|
34
|
+
@variation_key = variation_key
|
35
|
+
@enabled = enabled || false
|
36
|
+
@variables = variables || {}
|
37
|
+
@rule_key = rule_key
|
38
|
+
@flag_key = flag_key
|
39
|
+
@user_context = user_context
|
40
|
+
@reasons = reasons || []
|
41
|
+
end
|
42
|
+
|
43
|
+
def as_json
|
44
|
+
{
|
45
|
+
variation_key: @variation_key,
|
46
|
+
enabled: @enabled,
|
47
|
+
variables: @variables,
|
48
|
+
rule_key: @rule_key,
|
49
|
+
flag_key: @flag_key,
|
50
|
+
user_context: @user_context.as_json,
|
51
|
+
reasons: @reasons
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_json(*args)
|
56
|
+
as_json.to_json(*args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2020, Optimizely and contributors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Optimizely
|
19
|
+
module Decide
|
20
|
+
module OptimizelyDecisionMessage
|
21
|
+
SDK_NOT_READY = 'Optimizely SDK not configured properly yet.'
|
22
|
+
FLAG_KEY_INVALID = 'No flag was found for key "%s".'
|
23
|
+
VARIABLE_VALUE_INVALID = 'Variable value for key "%s" is invalid or wrong type.'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2017-
|
4
|
+
# Copyright 2017-2021, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -40,6 +40,7 @@ module Optimizely
|
|
40
40
|
Decision = Struct.new(:experiment, :variation, :source)
|
41
41
|
|
42
42
|
DECISION_SOURCES = {
|
43
|
+
'EXPERIMENT' => 'experiment',
|
43
44
|
'FEATURE_TEST' => 'feature-test',
|
44
45
|
'ROLLOUT' => 'rollout'
|
45
46
|
}.freeze
|
@@ -51,77 +52,89 @@ module Optimizely
|
|
51
52
|
@forced_variation_map = {}
|
52
53
|
end
|
53
54
|
|
54
|
-
def get_variation(project_config,
|
55
|
+
def get_variation(project_config, experiment_id, user_id, attributes = nil, decide_options = [])
|
55
56
|
# Determines variation into which user will be bucketed.
|
56
57
|
#
|
57
58
|
# project_config - project_config - Instance of ProjectConfig
|
58
|
-
#
|
59
|
+
# experiment_id - Experiment for which visitor variation needs to be determined
|
59
60
|
# user_id - String ID for user
|
60
61
|
# attributes - Hash representing user attributes
|
61
62
|
#
|
62
63
|
# Returns variation ID where visitor will be bucketed
|
63
64
|
# (nil if experiment is inactive or user does not meet audience conditions)
|
64
65
|
|
66
|
+
decide_reasons = []
|
65
67
|
# By default, the bucketing ID should be the user ID
|
66
|
-
bucketing_id = get_bucketing_id(user_id, attributes)
|
68
|
+
bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
|
69
|
+
decide_reasons.push(*bucketing_id_reasons)
|
67
70
|
# Check to make sure experiment is active
|
68
|
-
experiment = project_config.
|
69
|
-
return nil if experiment.nil?
|
71
|
+
experiment = project_config.get_experiment_from_id(experiment_id)
|
72
|
+
return nil, decide_reasons if experiment.nil?
|
70
73
|
|
71
|
-
|
74
|
+
experiment_key = experiment['key']
|
72
75
|
unless project_config.experiment_running?(experiment)
|
73
|
-
|
74
|
-
|
76
|
+
message = "Experiment '#{experiment_key}' is not running."
|
77
|
+
@logger.log(Logger::INFO, message)
|
78
|
+
decide_reasons.push(message)
|
79
|
+
return nil, decide_reasons
|
75
80
|
end
|
76
81
|
|
77
82
|
# Check if a forced variation is set for the user
|
78
|
-
forced_variation = get_forced_variation(project_config,
|
79
|
-
|
83
|
+
forced_variation, reasons_received = get_forced_variation(project_config, experiment['key'], user_id)
|
84
|
+
decide_reasons.push(*reasons_received)
|
85
|
+
return forced_variation['id'], decide_reasons if forced_variation
|
80
86
|
|
81
87
|
# Check if user is in a white-listed variation
|
82
|
-
whitelisted_variation_id = get_whitelisted_variation_id(project_config,
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
)
|
93
|
-
|
88
|
+
whitelisted_variation_id, reasons_received = get_whitelisted_variation_id(project_config, experiment_id, user_id)
|
89
|
+
decide_reasons.push(*reasons_received)
|
90
|
+
return whitelisted_variation_id, decide_reasons if whitelisted_variation_id
|
91
|
+
|
92
|
+
should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
|
93
|
+
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
|
94
|
+
unless should_ignore_user_profile_service
|
95
|
+
user_profile, reasons_received = get_user_profile(user_id)
|
96
|
+
decide_reasons.push(*reasons_received)
|
97
|
+
saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile)
|
98
|
+
decide_reasons.push(*reasons_received)
|
99
|
+
if saved_variation_id
|
100
|
+
message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
|
101
|
+
@logger.log(Logger::INFO, message)
|
102
|
+
decide_reasons.push(message)
|
103
|
+
return saved_variation_id, decide_reasons
|
104
|
+
end
|
94
105
|
end
|
95
106
|
|
96
107
|
# Check audience conditions
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
)
|
102
|
-
|
108
|
+
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
|
109
|
+
decide_reasons.push(*reasons_received)
|
110
|
+
unless user_meets_audience_conditions
|
111
|
+
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
|
112
|
+
@logger.log(Logger::INFO, message)
|
113
|
+
decide_reasons.push(message)
|
114
|
+
return nil, decide_reasons
|
103
115
|
end
|
104
116
|
|
105
117
|
# Bucket normally
|
106
|
-
variation = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
|
118
|
+
variation, bucket_reasons = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
|
119
|
+
decide_reasons.push(*bucket_reasons)
|
107
120
|
variation_id = variation ? variation['id'] : nil
|
108
121
|
|
122
|
+
message = ''
|
109
123
|
if variation_id
|
110
124
|
variation_key = variation['key']
|
111
|
-
|
112
|
-
Logger::INFO,
|
113
|
-
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
|
114
|
-
)
|
125
|
+
message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_id}'."
|
115
126
|
else
|
116
|
-
|
127
|
+
message = "User '#{user_id}' is in no variation."
|
117
128
|
end
|
129
|
+
@logger.log(Logger::INFO, message)
|
130
|
+
decide_reasons.push(message)
|
118
131
|
|
119
132
|
# Persist bucketing decision
|
120
|
-
save_user_profile(user_profile, experiment_id, variation_id)
|
121
|
-
variation_id
|
133
|
+
save_user_profile(user_profile, experiment_id, variation_id) unless should_ignore_user_profile_service
|
134
|
+
[variation_id, decide_reasons]
|
122
135
|
end
|
123
136
|
|
124
|
-
def get_variation_for_feature(project_config, feature_flag, user_id, attributes = nil)
|
137
|
+
def get_variation_for_feature(project_config, feature_flag, user_id, attributes = nil, decide_options = [])
|
125
138
|
# Get the variation the user is bucketed into for the given FeatureFlag.
|
126
139
|
#
|
127
140
|
# project_config - project_config - Instance of ProjectConfig
|
@@ -131,16 +144,20 @@ module Optimizely
|
|
131
144
|
#
|
132
145
|
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
|
133
146
|
|
147
|
+
decide_reasons = []
|
148
|
+
|
134
149
|
# check if the feature is being experiment on and whether the user is bucketed into the experiment
|
135
|
-
decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes)
|
136
|
-
|
150
|
+
decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes, decide_options)
|
151
|
+
decide_reasons.push(*reasons_received)
|
152
|
+
return decision, decide_reasons unless decision.nil?
|
137
153
|
|
138
|
-
decision = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)
|
154
|
+
decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)
|
155
|
+
decide_reasons.push(*reasons_received)
|
139
156
|
|
140
|
-
decision
|
157
|
+
[decision, decide_reasons]
|
141
158
|
end
|
142
159
|
|
143
|
-
def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil)
|
160
|
+
def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil, decide_options = [])
|
144
161
|
# Gets the variation the user is bucketed into for the feature flag's experiment.
|
145
162
|
#
|
146
163
|
# project_config - project_config - Instance of ProjectConfig
|
@@ -150,42 +167,41 @@ module Optimizely
|
|
150
167
|
#
|
151
168
|
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
|
152
169
|
# or nil if the user is not bucketed into any of the experiments on the feature
|
170
|
+
decide_reasons = []
|
153
171
|
feature_flag_key = feature_flag['key']
|
154
172
|
if feature_flag['experimentIds'].empty?
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
return nil
|
173
|
+
message = "The feature flag '#{feature_flag_key}' is not used in any experiments."
|
174
|
+
@logger.log(Logger::DEBUG, message)
|
175
|
+
decide_reasons.push(message)
|
176
|
+
return nil, decide_reasons
|
160
177
|
end
|
161
178
|
|
162
179
|
# Evaluate each experiment and return the first bucketed experiment variation
|
163
180
|
feature_flag['experimentIds'].each do |experiment_id|
|
164
181
|
experiment = project_config.experiment_id_map[experiment_id]
|
165
182
|
unless experiment
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
return nil
|
183
|
+
message = "Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
|
184
|
+
@logger.log(Logger::DEBUG, message)
|
185
|
+
decide_reasons.push(message)
|
186
|
+
return nil, decide_reasons
|
171
187
|
end
|
172
188
|
|
173
|
-
|
174
|
-
variation_id = get_variation(project_config,
|
189
|
+
experiment_id = experiment['id']
|
190
|
+
variation_id, reasons_received = get_variation(project_config, experiment_id, user_id, attributes, decide_options)
|
191
|
+
decide_reasons.push(*reasons_received)
|
175
192
|
|
176
193
|
next unless variation_id
|
177
194
|
|
178
|
-
variation = project_config.
|
195
|
+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
179
196
|
|
180
|
-
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'])
|
197
|
+
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST']), decide_reasons
|
181
198
|
end
|
182
199
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
)
|
200
|
+
message = "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
|
201
|
+
@logger.log(Logger::INFO, message)
|
202
|
+
decide_reasons.push(message)
|
187
203
|
|
188
|
-
nil
|
204
|
+
[nil, decide_reasons]
|
189
205
|
end
|
190
206
|
|
191
207
|
def get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes = nil)
|
@@ -198,27 +214,27 @@ module Optimizely
|
|
198
214
|
# attributes - Hash representing user attributes
|
199
215
|
#
|
200
216
|
# Returns the Decision struct or nil if not bucketed into any of the targeting rules
|
201
|
-
|
217
|
+
decide_reasons = []
|
218
|
+
bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
|
219
|
+
decide_reasons.push(*bucketing_id_reasons)
|
202
220
|
rollout_id = feature_flag['rolloutId']
|
203
221
|
if rollout_id.nil? || rollout_id.empty?
|
204
222
|
feature_flag_key = feature_flag['key']
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
return nil
|
223
|
+
message = "Feature flag '#{feature_flag_key}' is not used in a rollout."
|
224
|
+
@logger.log(Logger::DEBUG, message)
|
225
|
+
decide_reasons.push(message)
|
226
|
+
return nil, decide_reasons
|
210
227
|
end
|
211
228
|
|
212
229
|
rollout = project_config.get_rollout_from_id(rollout_id)
|
213
230
|
if rollout.nil?
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
return nil
|
231
|
+
message = "Rollout with ID '#{rollout_id}' is not in the datafile '#{feature_flag['key']}'"
|
232
|
+
@logger.log(Logger::DEBUG, message)
|
233
|
+
decide_reasons.push(message)
|
234
|
+
return nil, decide_reasons
|
219
235
|
end
|
220
236
|
|
221
|
-
return nil if rollout['experiments'].empty?
|
237
|
+
return nil, decide_reasons if rollout['experiments'].empty?
|
222
238
|
|
223
239
|
rollout_rules = rollout['experiments']
|
224
240
|
number_of_rules = rollout_rules.length - 1
|
@@ -228,24 +244,25 @@ module Optimizely
|
|
228
244
|
rollout_rule = rollout_rules[index]
|
229
245
|
logging_key = index + 1
|
230
246
|
|
247
|
+
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, rollout_rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
|
248
|
+
decide_reasons.push(*reasons_received)
|
231
249
|
# Check that user meets audience conditions for targeting rule
|
232
|
-
unless
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
)
|
250
|
+
unless user_meets_audience_conditions
|
251
|
+
message = "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
|
252
|
+
@logger.log(Logger::DEBUG, message)
|
253
|
+
decide_reasons.push(message)
|
237
254
|
# move onto the next targeting rule
|
238
255
|
next
|
239
256
|
end
|
240
257
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
)
|
258
|
+
message = "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
|
259
|
+
@logger.log(Logger::DEBUG, message)
|
260
|
+
decide_reasons.push(message)
|
245
261
|
|
246
262
|
# Evaluate if user satisfies the traffic allocation for this rollout rule
|
247
|
-
variation = @bucketer.bucket(project_config, rollout_rule, bucketing_id, user_id)
|
248
|
-
|
263
|
+
variation, bucket_reasons = @bucketer.bucket(project_config, rollout_rule, bucketing_id, user_id)
|
264
|
+
decide_reasons.push(*bucket_reasons)
|
265
|
+
return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']), decide_reasons unless variation.nil?
|
249
266
|
|
250
267
|
break
|
251
268
|
end
|
@@ -253,23 +270,26 @@ module Optimizely
|
|
253
270
|
# get last rule which is the everyone else rule
|
254
271
|
everyone_else_experiment = rollout_rules[number_of_rules]
|
255
272
|
logging_key = 'Everyone Else'
|
273
|
+
|
274
|
+
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, everyone_else_experiment, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
|
275
|
+
decide_reasons.push(*reasons_received)
|
256
276
|
# Check that user meets audience conditions for last rule
|
257
|
-
unless
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
return nil
|
277
|
+
unless user_meets_audience_conditions
|
278
|
+
message = "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
|
279
|
+
@logger.log(Logger::DEBUG, message)
|
280
|
+
decide_reasons.push(message)
|
281
|
+
return nil, decide_reasons
|
263
282
|
end
|
264
283
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
)
|
269
|
-
variation = @bucketer.bucket(project_config, everyone_else_experiment, bucketing_id, user_id)
|
270
|
-
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
|
284
|
+
message = "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
|
285
|
+
@logger.log(Logger::DEBUG, message)
|
286
|
+
decide_reasons.push(message)
|
271
287
|
|
272
|
-
|
288
|
+
variation, bucket_reasons = @bucketer.bucket(project_config, everyone_else_experiment, bucketing_id, user_id)
|
289
|
+
decide_reasons.push(*bucket_reasons)
|
290
|
+
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']), decide_reasons unless variation.nil?
|
291
|
+
|
292
|
+
[nil, decide_reasons]
|
273
293
|
end
|
274
294
|
|
275
295
|
def set_forced_variation(project_config, experiment_key, user_id, variation_key)
|
@@ -295,7 +315,7 @@ module Optimizely
|
|
295
315
|
return true
|
296
316
|
end
|
297
317
|
|
298
|
-
variation_id = project_config.
|
318
|
+
variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
|
299
319
|
|
300
320
|
# check if the variation exists in the datafile
|
301
321
|
unless variation_id
|
@@ -314,14 +334,16 @@ module Optimizely
|
|
314
334
|
# Gets the forced variation for the given user and experiment.
|
315
335
|
#
|
316
336
|
# project_config - Instance of ProjectConfig
|
317
|
-
# experiment_key - String
|
337
|
+
# experiment_key - String key for experiment
|
318
338
|
# user_id - String ID for user
|
319
339
|
#
|
320
340
|
# Returns Variation The variation which the given user and experiment should be forced into
|
321
341
|
|
342
|
+
decide_reasons = []
|
322
343
|
unless @forced_variation_map.key? user_id
|
323
|
-
|
324
|
-
|
344
|
+
message = "User '#{user_id}' is not in the forced variation map."
|
345
|
+
@logger.log(Logger::DEBUG, message)
|
346
|
+
return nil, decide_reasons
|
325
347
|
end
|
326
348
|
|
327
349
|
experiment_to_variation_map = @forced_variation_map[user_id]
|
@@ -329,32 +351,34 @@ module Optimizely
|
|
329
351
|
experiment_id = experiment['id'] if experiment
|
330
352
|
# check for nil and empty string experiment ID
|
331
353
|
# this case is logged in get_experiment_from_key
|
332
|
-
return nil if experiment_id.nil? || experiment_id.empty?
|
354
|
+
return nil, decide_reasons if experiment_id.nil? || experiment_id.empty?
|
333
355
|
|
334
356
|
unless experiment_to_variation_map.key? experiment_id
|
335
|
-
|
336
|
-
|
337
|
-
|
357
|
+
message = "No experiment '#{experiment_id}' mapped to user '#{user_id}' in the forced variation map."
|
358
|
+
@logger.log(Logger::DEBUG, message)
|
359
|
+
decide_reasons.push(message)
|
360
|
+
return nil, decide_reasons
|
338
361
|
end
|
339
362
|
|
340
363
|
variation_id = experiment_to_variation_map[experiment_id]
|
341
364
|
variation_key = ''
|
342
|
-
variation = project_config.
|
365
|
+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
343
366
|
variation_key = variation['key'] if variation
|
344
367
|
|
345
368
|
# check if the variation exists in the datafile
|
346
369
|
# this case is logged in get_variation_from_id
|
347
|
-
return nil if variation_key.empty?
|
370
|
+
return nil, decide_reasons if variation_key.empty?
|
348
371
|
|
349
|
-
|
350
|
-
|
372
|
+
message = "Variation '#{variation_key}' is mapped to experiment '#{experiment_id}' and user '#{user_id}' in the forced variation map"
|
373
|
+
@logger.log(Logger::DEBUG, message)
|
374
|
+
decide_reasons.push(message)
|
351
375
|
|
352
|
-
variation
|
376
|
+
[variation, decide_reasons]
|
353
377
|
end
|
354
378
|
|
355
379
|
private
|
356
380
|
|
357
|
-
def get_whitelisted_variation_id(project_config,
|
381
|
+
def get_whitelisted_variation_id(project_config, experiment_id, user_id)
|
358
382
|
# Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
|
359
383
|
#
|
360
384
|
# project_config - project_config - Instance of ProjectConfig
|
@@ -363,29 +387,26 @@ module Optimizely
|
|
363
387
|
#
|
364
388
|
# Returns variation ID into which user_id is whitelisted (nil if no variation)
|
365
389
|
|
366
|
-
whitelisted_variations = project_config.get_whitelisted_variations(
|
390
|
+
whitelisted_variations = project_config.get_whitelisted_variations(experiment_id)
|
367
391
|
|
368
|
-
return nil unless whitelisted_variations
|
392
|
+
return nil, nil unless whitelisted_variations
|
369
393
|
|
370
394
|
whitelisted_variation_key = whitelisted_variations[user_id]
|
371
395
|
|
372
|
-
return nil unless whitelisted_variation_key
|
396
|
+
return nil, nil unless whitelisted_variation_key
|
373
397
|
|
374
|
-
whitelisted_variation_id = project_config.
|
398
|
+
whitelisted_variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, whitelisted_variation_key)
|
375
399
|
|
376
400
|
unless whitelisted_variation_id
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
)
|
381
|
-
return nil
|
401
|
+
message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
|
402
|
+
@logger.log(Logger::INFO, message)
|
403
|
+
return nil, message
|
382
404
|
end
|
383
405
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
whitelisted_variation_id
|
406
|
+
message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_id}'."
|
407
|
+
@logger.log(Logger::INFO, message)
|
408
|
+
|
409
|
+
[whitelisted_variation_id, message]
|
389
410
|
end
|
390
411
|
|
391
412
|
def get_saved_variation_id(project_config, experiment_id, user_profile)
|
@@ -396,19 +417,18 @@ module Optimizely
|
|
396
417
|
# user_profile - Hash user profile
|
397
418
|
#
|
398
419
|
# Returns string variation ID (nil if no decision is found)
|
399
|
-
return nil unless user_profile[:experiment_bucket_map]
|
420
|
+
return nil, nil unless user_profile[:experiment_bucket_map]
|
400
421
|
|
401
422
|
decision = user_profile[:experiment_bucket_map][experiment_id]
|
402
|
-
return nil unless decision
|
423
|
+
return nil, nil unless decision
|
403
424
|
|
404
425
|
variation_id = decision[:variation_id]
|
405
|
-
return variation_id if project_config.variation_id_exists?(experiment_id, variation_id)
|
426
|
+
return variation_id, nil if project_config.variation_id_exists?(experiment_id, variation_id)
|
427
|
+
|
428
|
+
message = "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."
|
429
|
+
@logger.log(Logger::INFO, message)
|
406
430
|
|
407
|
-
|
408
|
-
Logger::INFO,
|
409
|
-
"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."
|
410
|
-
)
|
411
|
-
nil
|
431
|
+
[nil, message]
|
412
432
|
end
|
413
433
|
|
414
434
|
def get_user_profile(user_id)
|
@@ -423,15 +443,17 @@ module Optimizely
|
|
423
443
|
experiment_bucket_map: {}
|
424
444
|
}
|
425
445
|
|
426
|
-
return user_profile unless @user_profile_service
|
446
|
+
return user_profile, nil unless @user_profile_service
|
427
447
|
|
448
|
+
message = nil
|
428
449
|
begin
|
429
450
|
user_profile = @user_profile_service.lookup(user_id) || user_profile
|
430
451
|
rescue => e
|
431
|
-
|
452
|
+
message = "Error while looking up user profile for user ID '#{user_id}': #{e}."
|
453
|
+
@logger.log(Logger::ERROR, message)
|
432
454
|
end
|
433
455
|
|
434
|
-
user_profile
|
456
|
+
[user_profile, message]
|
435
457
|
end
|
436
458
|
|
437
459
|
def save_user_profile(user_profile, experiment_id, variation_id)
|
@@ -462,16 +484,17 @@ module Optimizely
|
|
462
484
|
# attributes - Hash user attributes
|
463
485
|
# Returns String representing bucketing ID if it is a String type in attributes else return user ID
|
464
486
|
|
465
|
-
return user_id unless attributes
|
487
|
+
return user_id, nil unless attributes
|
466
488
|
|
467
489
|
bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
|
468
490
|
|
469
491
|
if bucketing_id
|
470
|
-
return bucketing_id if bucketing_id.is_a?(String)
|
492
|
+
return bucketing_id, nil if bucketing_id.is_a?(String)
|
471
493
|
|
472
|
-
|
494
|
+
message = 'Bucketing ID attribute is not a string. Defaulted to user ID.'
|
495
|
+
@logger.log(Logger::WARN, message)
|
473
496
|
end
|
474
|
-
user_id
|
497
|
+
[user_id, message]
|
475
498
|
end
|
476
499
|
end
|
477
500
|
end
|