optimizely-sdk 3.8.1 → 3.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/optimizely/condition_tree_evaluator.rb +2 -0
- data/lib/optimizely/config/datafile_project_config.rb +59 -1
- data/lib/optimizely/config_manager/http_project_config_manager.rb +12 -4
- data/lib/optimizely/config_manager/static_project_config_manager.rb +8 -3
- data/lib/optimizely/decision_service.rb +120 -57
- data/lib/optimizely/optimizely_config.rb +180 -25
- data/lib/optimizely/optimizely_user_context.rb +74 -2
- data/lib/optimizely/project_config.rb +5 -1
- data/lib/optimizely/version.rb +1 -1
- data/lib/optimizely.rb +40 -12
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6a2a4abf7984fbba2d78c4c8cf39c39b66d3cd9e2d381b5d0e8ab0cc9ea0324
|
4
|
+
data.tar.gz: 62e9a6d0a44a5d83fcb4f574905cd64873c53d052503d135df6eb83b93c91b41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 807fe5ee0c7ab987ca2e273ea340aace86170295b0746f69b4ca5fb8e4d389093a43f1a8d625efd2e55c29960baa7dbc3b747111bd98bcf7e1e011da753cafff
|
7
|
+
data.tar.gz: 44fa786ddcc047608c8b3cac34c01c7f15822f9f33db0fd9cadedc2b4440a3b6b52177e94708223ae5e16d4ae5b46553e3c1820bb9b083dfcaee889e64ea3e8f
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2019-
|
3
|
+
# Copyright 2019-2021, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -38,6 +38,8 @@ module Optimizely
|
|
38
38
|
attr_reader :anonymize_ip
|
39
39
|
attr_reader :bot_filtering
|
40
40
|
attr_reader :revision
|
41
|
+
attr_reader :sdk_key
|
42
|
+
attr_reader :environment_key
|
41
43
|
attr_reader :rollouts
|
42
44
|
attr_reader :version
|
43
45
|
attr_reader :send_flag_decisions
|
@@ -58,6 +60,7 @@ module Optimizely
|
|
58
60
|
attr_reader :variation_key_map
|
59
61
|
attr_reader :variation_id_map_by_experiment_id
|
60
62
|
attr_reader :variation_key_map_by_experiment_id
|
63
|
+
attr_reader :flag_variation_map
|
61
64
|
|
62
65
|
def initialize(datafile, logger, error_handler)
|
63
66
|
# ProjectConfig init method to fetch and set project config data
|
@@ -85,6 +88,8 @@ module Optimizely
|
|
85
88
|
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
|
86
89
|
@bot_filtering = config['botFiltering']
|
87
90
|
@revision = config['revision']
|
91
|
+
@sdk_key = config.fetch('sdkKey', '')
|
92
|
+
@environment_key = config.fetch('environmentKey', '')
|
88
93
|
@rollouts = config.fetch('rollouts', [])
|
89
94
|
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
|
90
95
|
|
@@ -119,6 +124,8 @@ module Optimizely
|
|
119
124
|
@variation_key_map_by_experiment_id = {}
|
120
125
|
@variation_id_to_variable_usage_map = {}
|
121
126
|
@variation_id_to_experiment_map = {}
|
127
|
+
@flag_variation_map = {}
|
128
|
+
|
122
129
|
@experiment_id_map.each_value do |exp|
|
123
130
|
# Excludes experiments from rollouts
|
124
131
|
variations = exp.fetch('variations')
|
@@ -134,6 +141,8 @@ module Optimizely
|
|
134
141
|
exps = rollout.fetch('experiments')
|
135
142
|
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
|
136
143
|
end
|
144
|
+
|
145
|
+
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
137
146
|
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
138
147
|
@all_experiments.each do |id, exp|
|
139
148
|
variations = exp.fetch('variations')
|
@@ -161,6 +170,24 @@ module Optimizely
|
|
161
170
|
end
|
162
171
|
end
|
163
172
|
|
173
|
+
def get_rules_for_flag(feature_flag)
|
174
|
+
# Retrieves rules for a given feature flag
|
175
|
+
#
|
176
|
+
# feature_flag - String key representing the feature_flag
|
177
|
+
#
|
178
|
+
# Returns rules in feature flag
|
179
|
+
rules = feature_flag['experimentIds'].map { |exp_id| @experiment_id_map[exp_id] }
|
180
|
+
rollout = feature_flag['rolloutId'].empty? ? nil : @rollout_id_map[feature_flag['rolloutId']]
|
181
|
+
|
182
|
+
if rollout
|
183
|
+
rollout_experiments = rollout.fetch('experiments')
|
184
|
+
rollout_experiments.each do |exp|
|
185
|
+
rules.push(exp)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
rules
|
189
|
+
end
|
190
|
+
|
164
191
|
def self.create(datafile, logger, error_handler, skip_json_validation)
|
165
192
|
# Looks up and sets datafile and config based on response body.
|
166
193
|
#
|
@@ -275,6 +302,13 @@ module Optimizely
|
|
275
302
|
nil
|
276
303
|
end
|
277
304
|
|
305
|
+
def get_variation_from_flag(flag_key, target_value, attribute)
|
306
|
+
variations = @flag_variation_map[flag_key]
|
307
|
+
return variations.select { |variation| variation[attribute] == target_value }.first if variations
|
308
|
+
|
309
|
+
nil
|
310
|
+
end
|
311
|
+
|
278
312
|
def get_variation_from_id(experiment_key, variation_id)
|
279
313
|
# Get variation given experiment key and variation ID
|
280
314
|
#
|
@@ -478,8 +512,32 @@ module Optimizely
|
|
478
512
|
@experiment_feature_map.key?(experiment_id)
|
479
513
|
end
|
480
514
|
|
515
|
+
def rollout_experiment?(experiment_id)
|
516
|
+
# Determines if given experiment is a rollout test.
|
517
|
+
#
|
518
|
+
# experiment_id - String experiment ID
|
519
|
+
#
|
520
|
+
# Returns true if experiment belongs to any rollout,
|
521
|
+
# false otherwise.
|
522
|
+
@rollout_experiment_id_map.key?(experiment_id)
|
523
|
+
end
|
524
|
+
|
481
525
|
private
|
482
526
|
|
527
|
+
def generate_feature_variation_map(feature_flags)
|
528
|
+
flag_variation_map = {}
|
529
|
+
feature_flags.each do |flag|
|
530
|
+
variations = []
|
531
|
+
get_rules_for_flag(flag).each do |rule|
|
532
|
+
rule['variations'].each do |rule_variation|
|
533
|
+
variations.push(rule_variation) if variations.select { |variation| variation['id'] == rule_variation['id'] }.empty?
|
534
|
+
end
|
535
|
+
end
|
536
|
+
flag_variation_map[flag['key']] = variations
|
537
|
+
end
|
538
|
+
flag_variation_map
|
539
|
+
end
|
540
|
+
|
483
541
|
def generate_key_map(array, key)
|
484
542
|
# Helper method to generate map from key to hash in array of hashes
|
485
543
|
#
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2019-2020, Optimizely and contributors
|
4
|
+
# Copyright 2019-2020, 2022, 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.
|
@@ -33,7 +33,7 @@ module Optimizely
|
|
33
33
|
class HTTPProjectConfigManager < ProjectConfigManager
|
34
34
|
# Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
|
35
35
|
|
36
|
-
attr_reader :stopped
|
36
|
+
attr_reader :stopped
|
37
37
|
|
38
38
|
# Initialize config manager. One of sdk_key or url has to be set to be able to use.
|
39
39
|
#
|
@@ -80,8 +80,8 @@ module Optimizely
|
|
80
80
|
@last_modified = nil
|
81
81
|
@skip_json_validation = skip_json_validation
|
82
82
|
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
83
|
+
@optimizely_config = nil
|
83
84
|
@config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
|
84
|
-
@optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
|
85
85
|
@mutex = Mutex.new
|
86
86
|
@resource = ConditionVariable.new
|
87
87
|
@async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
|
@@ -140,6 +140,12 @@ module Optimizely
|
|
140
140
|
@config
|
141
141
|
end
|
142
142
|
|
143
|
+
def optimizely_config
|
144
|
+
@optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
|
145
|
+
|
146
|
+
@optimizely_config
|
147
|
+
end
|
148
|
+
|
143
149
|
private
|
144
150
|
|
145
151
|
def fetch_datafile_config
|
@@ -209,7 +215,9 @@ module Optimizely
|
|
209
215
|
end
|
210
216
|
|
211
217
|
@config = config
|
212
|
-
|
218
|
+
|
219
|
+
# clearing old optimizely config so that a fresh one is generated on the next api call.
|
220
|
+
@optimizely_config = nil
|
213
221
|
|
214
222
|
@notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
|
215
223
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2019-2020, Optimizely and contributors
|
4
|
+
# Copyright 2019-2020, 2022, 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.
|
@@ -23,7 +23,7 @@ require_relative 'project_config_manager'
|
|
23
23
|
module Optimizely
|
24
24
|
class StaticProjectConfigManager < ProjectConfigManager
|
25
25
|
# Implementation of ProjectConfigManager interface.
|
26
|
-
attr_reader :config
|
26
|
+
attr_reader :config
|
27
27
|
|
28
28
|
def initialize(datafile, logger, error_handler, skip_json_validation)
|
29
29
|
# Looks up and sets datafile and config based on response body.
|
@@ -40,8 +40,13 @@ module Optimizely
|
|
40
40
|
error_handler,
|
41
41
|
skip_json_validation
|
42
42
|
)
|
43
|
+
@optimizely_config = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def optimizely_config
|
47
|
+
@optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
|
43
48
|
|
44
|
-
@optimizely_config
|
49
|
+
@optimizely_config
|
45
50
|
end
|
46
51
|
end
|
47
52
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2017-
|
4
|
+
# Copyright 2017-2022, 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.
|
@@ -52,18 +52,19 @@ module Optimizely
|
|
52
52
|
@forced_variation_map = {}
|
53
53
|
end
|
54
54
|
|
55
|
-
def get_variation(project_config, experiment_id,
|
55
|
+
def get_variation(project_config, experiment_id, user_context, decide_options = [])
|
56
56
|
# Determines variation into which user will be bucketed.
|
57
57
|
#
|
58
58
|
# project_config - project_config - Instance of ProjectConfig
|
59
59
|
# experiment_id - Experiment for which visitor variation needs to be determined
|
60
|
-
#
|
61
|
-
# attributes - Hash representing user attributes
|
60
|
+
# user_context - Optimizely user context instance
|
62
61
|
#
|
63
62
|
# Returns variation ID where visitor will be bucketed
|
64
63
|
# (nil if experiment is inactive or user does not meet audience conditions)
|
65
64
|
|
66
65
|
decide_reasons = []
|
66
|
+
user_id = user_context.user_id
|
67
|
+
attributes = user_context.user_attributes
|
67
68
|
# By default, the bucketing ID should be the user ID
|
68
69
|
bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
|
69
70
|
decide_reasons.push(*bucketing_id_reasons)
|
@@ -134,40 +135,39 @@ module Optimizely
|
|
134
135
|
[variation_id, decide_reasons]
|
135
136
|
end
|
136
137
|
|
137
|
-
def get_variation_for_feature(project_config, feature_flag,
|
138
|
+
def get_variation_for_feature(project_config, feature_flag, user_context, decide_options = [])
|
138
139
|
# Get the variation the user is bucketed into for the given FeatureFlag.
|
139
140
|
#
|
140
141
|
# project_config - project_config - Instance of ProjectConfig
|
141
142
|
# feature_flag - The feature flag the user wants to access
|
142
|
-
#
|
143
|
-
# attributes - Hash representing user attributes
|
143
|
+
# user_context - Optimizely user context instance
|
144
144
|
#
|
145
145
|
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
|
146
146
|
|
147
147
|
decide_reasons = []
|
148
148
|
|
149
149
|
# check if the feature is being experiment on and whether the user is bucketed into the experiment
|
150
|
-
decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag,
|
150
|
+
decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options)
|
151
151
|
decide_reasons.push(*reasons_received)
|
152
152
|
return decision, decide_reasons unless decision.nil?
|
153
153
|
|
154
|
-
decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag,
|
154
|
+
decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
|
155
155
|
decide_reasons.push(*reasons_received)
|
156
156
|
|
157
157
|
[decision, decide_reasons]
|
158
158
|
end
|
159
159
|
|
160
|
-
def get_variation_for_feature_experiment(project_config, feature_flag,
|
160
|
+
def get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options = [])
|
161
161
|
# Gets the variation the user is bucketed into for the feature flag's experiment.
|
162
162
|
#
|
163
163
|
# project_config - project_config - Instance of ProjectConfig
|
164
164
|
# feature_flag - The feature flag the user wants to access
|
165
|
-
#
|
166
|
-
# attributes - Hash representing user attributes
|
165
|
+
# user_context - Optimizely user context instance
|
167
166
|
#
|
168
167
|
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
|
169
168
|
# or nil if the user is not bucketed into any of the experiments on the feature
|
170
169
|
decide_reasons = []
|
170
|
+
user_id = user_context.user_id
|
171
171
|
feature_flag_key = feature_flag['key']
|
172
172
|
if feature_flag['experimentIds'].empty?
|
173
173
|
message = "The feature flag '#{feature_flag_key}' is not used in any experiments."
|
@@ -187,12 +187,13 @@ module Optimizely
|
|
187
187
|
end
|
188
188
|
|
189
189
|
experiment_id = experiment['id']
|
190
|
-
variation_id, reasons_received =
|
190
|
+
variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, decide_options)
|
191
191
|
decide_reasons.push(*reasons_received)
|
192
192
|
|
193
193
|
next unless variation_id
|
194
194
|
|
195
195
|
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
196
|
+
variation = project_config.get_variation_from_flag(feature_flag['key'], variation_id, 'id') if variation.nil?
|
196
197
|
|
197
198
|
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST']), decide_reasons
|
198
199
|
end
|
@@ -204,22 +205,20 @@ module Optimizely
|
|
204
205
|
[nil, decide_reasons]
|
205
206
|
end
|
206
207
|
|
207
|
-
def get_variation_for_feature_rollout(project_config, feature_flag,
|
208
|
+
def get_variation_for_feature_rollout(project_config, feature_flag, user_context)
|
208
209
|
# Determine which variation the user is in for a given rollout.
|
209
210
|
# Returns the variation of the first experiment the user qualifies for.
|
210
211
|
#
|
211
212
|
# project_config - project_config - Instance of ProjectConfig
|
212
213
|
# feature_flag - The feature flag the user wants to access
|
213
|
-
#
|
214
|
-
# attributes - Hash representing user attributes
|
214
|
+
# user_context - Optimizely user context instance
|
215
215
|
#
|
216
216
|
# Returns the Decision struct or nil if not bucketed into any of the targeting rules
|
217
217
|
decide_reasons = []
|
218
|
-
|
219
|
-
decide_reasons.push(*bucketing_id_reasons)
|
218
|
+
|
220
219
|
rollout_id = feature_flag['rolloutId']
|
220
|
+
feature_flag_key = feature_flag['key']
|
221
221
|
if rollout_id.nil? || rollout_id.empty?
|
222
|
-
feature_flag_key = feature_flag['key']
|
223
222
|
message = "Feature flag '#{feature_flag_key}' is not used in a rollout."
|
224
223
|
@logger.log(Logger::DEBUG, message)
|
225
224
|
decide_reasons.push(message)
|
@@ -236,60 +235,102 @@ module Optimizely
|
|
236
235
|
|
237
236
|
return nil, decide_reasons if rollout['experiments'].empty?
|
238
237
|
|
238
|
+
index = 0
|
239
239
|
rollout_rules = rollout['experiments']
|
240
|
-
|
241
|
-
|
242
|
-
# Go through each experiment in order and try to get the variation for the user
|
243
|
-
number_of_rules.times do |index|
|
244
|
-
rollout_rule = rollout_rules[index]
|
245
|
-
logging_key = index + 1
|
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)
|
240
|
+
while index < rollout_rules.length
|
241
|
+
variation, skip_to_everyone_else, reasons_received = get_variation_from_delivery_rule(project_config, feature_flag_key, rollout_rules, index, user_context)
|
248
242
|
decide_reasons.push(*reasons_received)
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
decide_reasons.push(message)
|
254
|
-
# move onto the next targeting rule
|
255
|
-
next
|
243
|
+
if variation
|
244
|
+
rule = rollout_rules[index]
|
245
|
+
feature_decision = Decision.new(rule, variation, DECISION_SOURCES['ROLLOUT'])
|
246
|
+
return [feature_decision, decide_reasons]
|
256
247
|
end
|
257
248
|
|
258
|
-
|
259
|
-
|
260
|
-
decide_reasons.push(message)
|
249
|
+
index = skip_to_everyone_else ? (rollout_rules.length - 1) : (index + 1)
|
250
|
+
end
|
261
251
|
|
262
|
-
|
263
|
-
|
264
|
-
decide_reasons.push(*bucket_reasons)
|
265
|
-
return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']), decide_reasons unless variation.nil?
|
252
|
+
[nil, decide_reasons]
|
253
|
+
end
|
266
254
|
|
267
|
-
|
268
|
-
|
255
|
+
def get_variation_from_experiment_rule(project_config, flag_key, rule, user, options = [])
|
256
|
+
# Determine which variation the user is in for a given rollout.
|
257
|
+
# Returns the variation from experiment rules.
|
258
|
+
#
|
259
|
+
# project_config - project_config - Instance of ProjectConfig
|
260
|
+
# flag_key - The feature flag the user wants to access
|
261
|
+
# rule - An experiment rule key
|
262
|
+
# user - Optimizely user context instance
|
263
|
+
#
|
264
|
+
# Returns variation_id and reasons
|
265
|
+
reasons = []
|
269
266
|
|
270
|
-
|
271
|
-
|
272
|
-
|
267
|
+
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(flag_key, rule['key'])
|
268
|
+
variation, forced_reasons = validated_forced_decision(project_config, context, user)
|
269
|
+
reasons.push(*forced_reasons)
|
273
270
|
|
274
|
-
|
275
|
-
|
276
|
-
|
271
|
+
return [variation['id'], reasons] if variation
|
272
|
+
|
273
|
+
variation_id, response_reasons = get_variation(project_config, rule['id'], user, options)
|
274
|
+
reasons.push(*response_reasons)
|
275
|
+
|
276
|
+
[variation_id, reasons]
|
277
|
+
end
|
278
|
+
|
279
|
+
def get_variation_from_delivery_rule(project_config, flag_key, rules, rule_index, user)
|
280
|
+
# Determine which variation the user is in for a given rollout.
|
281
|
+
# Returns the variation from delivery rules.
|
282
|
+
#
|
283
|
+
# project_config - project_config - Instance of ProjectConfig
|
284
|
+
# flag_key - The feature flag the user wants to access
|
285
|
+
# rule - An experiment rule key
|
286
|
+
# user - Optimizely user context instance
|
287
|
+
#
|
288
|
+
# Returns variation, boolean to skip for eveyone else rule and reasons
|
289
|
+
reasons = []
|
290
|
+
skip_to_everyone_else = false
|
291
|
+
rule = rules[rule_index]
|
292
|
+
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(flag_key, rule['key'])
|
293
|
+
variation, forced_reasons = validated_forced_decision(project_config, context, user)
|
294
|
+
reasons.push(*forced_reasons)
|
295
|
+
|
296
|
+
return [variation, skip_to_everyone_else, reasons] if variation
|
297
|
+
|
298
|
+
user_id = user.user_id
|
299
|
+
attributes = user.user_attributes
|
300
|
+
bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
|
301
|
+
reasons.push(*bucketing_id_reasons)
|
302
|
+
|
303
|
+
everyone_else = (rule_index == rules.length - 1)
|
304
|
+
|
305
|
+
logging_key = everyone_else ? 'Everyone Else' : (rule_index + 1).to_s
|
306
|
+
|
307
|
+
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
|
308
|
+
reasons.push(*reasons_received)
|
277
309
|
unless user_meets_audience_conditions
|
278
|
-
message = "User '#{user_id}' does not meet the
|
310
|
+
message = "User '#{user_id}' does not meet the conditions for targeting rule '#{logging_key}'."
|
279
311
|
@logger.log(Logger::DEBUG, message)
|
280
|
-
|
281
|
-
return nil,
|
312
|
+
reasons.push(message)
|
313
|
+
return [nil, skip_to_everyone_else, reasons]
|
282
314
|
end
|
283
315
|
|
284
316
|
message = "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
|
285
317
|
@logger.log(Logger::DEBUG, message)
|
286
|
-
|
318
|
+
reasons.push(message)
|
319
|
+
bucket_variation, bucket_reasons = @bucketer.bucket(project_config, rule, bucketing_id, user_id)
|
287
320
|
|
288
|
-
|
289
|
-
decide_reasons.push(*bucket_reasons)
|
290
|
-
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']), decide_reasons unless variation.nil?
|
321
|
+
reasons.push(*bucket_reasons)
|
291
322
|
|
292
|
-
|
323
|
+
if bucket_variation
|
324
|
+
message = "User '#{user_id}' is in the traffic group of targeting rule '#{logging_key}'."
|
325
|
+
@logger.log(Logger::DEBUG, message)
|
326
|
+
reasons.push(message)
|
327
|
+
elsif !everyone_else
|
328
|
+
message = "User '#{user_id}' is not in the traffic group for targeting rule '#{logging_key}'."
|
329
|
+
@logger.log(Logger::DEBUG, message)
|
330
|
+
reasons.push(message)
|
331
|
+
skip_to_everyone_else = true
|
332
|
+
end
|
333
|
+
[bucket_variation, skip_to_everyone_else, reasons]
|
293
334
|
end
|
294
335
|
|
295
336
|
def set_forced_variation(project_config, experiment_key, user_id, variation_key)
|
@@ -376,6 +417,28 @@ module Optimizely
|
|
376
417
|
[variation, decide_reasons]
|
377
418
|
end
|
378
419
|
|
420
|
+
def validated_forced_decision(project_config, context, user_context)
|
421
|
+
decision = user_context.get_forced_decision(context)
|
422
|
+
flag_key = context[:flag_key]
|
423
|
+
rule_key = context[:rule_key]
|
424
|
+
variation_key = decision ? decision[:variation_key] : decision
|
425
|
+
reasons = []
|
426
|
+
target = rule_key ? "flag (#{flag_key}), rule (#{rule_key})" : "flag (#{flag_key})"
|
427
|
+
if variation_key
|
428
|
+
variation = project_config.get_variation_from_flag(flag_key, variation_key, 'key')
|
429
|
+
if variation
|
430
|
+
reason = "Variation (#{variation_key}) is mapped to #{target} and user (#{user_context.user_id}) in the forced decision map."
|
431
|
+
reasons.push(reason)
|
432
|
+
return variation, reasons
|
433
|
+
else
|
434
|
+
reason = "Invalid variation is mapped to #{target} and user (#{user_context.user_id}) in the forced decision map."
|
435
|
+
reasons.push(reason)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
[nil, reasons]
|
440
|
+
end
|
441
|
+
|
379
442
|
private
|
380
443
|
|
381
444
|
def get_whitelisted_variation_id(project_config, experiment_id, user_id)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2019-
|
3
|
+
# Copyright 2019-2021, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -16,53 +16,109 @@
|
|
16
16
|
#
|
17
17
|
|
18
18
|
module Optimizely
|
19
|
+
require 'json'
|
19
20
|
class OptimizelyConfig
|
21
|
+
include Optimizely::ConditionTreeEvaluator
|
20
22
|
def initialize(project_config)
|
21
23
|
@project_config = project_config
|
24
|
+
@rollouts = @project_config.rollouts
|
25
|
+
@audiences = []
|
26
|
+
audience_id_lookup_dict = {}
|
27
|
+
|
28
|
+
@project_config.typed_audiences.each do |typed_audience|
|
29
|
+
@audiences.push(
|
30
|
+
'id' => typed_audience['id'],
|
31
|
+
'name' => typed_audience['name'],
|
32
|
+
'conditions' => typed_audience['conditions'].to_json
|
33
|
+
)
|
34
|
+
audience_id_lookup_dict[typed_audience['id']] = typed_audience['id']
|
35
|
+
end
|
36
|
+
|
37
|
+
@project_config.audiences.each do |audience|
|
38
|
+
next unless !audience_id_lookup_dict.key?(audience['id']) && (audience['id'] != '$opt_dummy_audience')
|
39
|
+
|
40
|
+
@audiences.push(
|
41
|
+
'id' => audience['id'],
|
42
|
+
'name' => audience['name'],
|
43
|
+
'conditions' => audience['conditions']
|
44
|
+
)
|
45
|
+
end
|
22
46
|
end
|
23
47
|
|
24
48
|
def config
|
25
49
|
experiments_map_object = experiments_map
|
26
|
-
features_map = get_features_map(
|
27
|
-
{
|
50
|
+
features_map = get_features_map(experiments_id_map)
|
51
|
+
config = {
|
52
|
+
'sdkKey' => @project_config.sdk_key,
|
28
53
|
'datafile' => @project_config.datafile,
|
54
|
+
# This experimentsMap is for experiments of legacy projects only.
|
55
|
+
# For flag projects, experiment keys are not guaranteed to be unique
|
56
|
+
# across multiple flags, so this map may not include all experiments
|
57
|
+
# when keys conflict. Use experimentRules and deliveryRules instead.
|
29
58
|
'experimentsMap' => experiments_map_object,
|
30
59
|
'featuresMap' => features_map,
|
31
|
-
'revision' => @project_config.revision
|
60
|
+
'revision' => @project_config.revision,
|
61
|
+
'attributes' => get_attributes_list(@project_config.attributes),
|
62
|
+
'audiences' => @audiences,
|
63
|
+
'events' => get_events_list(@project_config.events),
|
64
|
+
'environmentKey' => @project_config.environment_key
|
32
65
|
}
|
66
|
+
config
|
33
67
|
end
|
34
68
|
|
35
69
|
private
|
36
70
|
|
37
|
-
def
|
38
|
-
feature_variables_map =
|
39
|
-
|
40
|
-
end
|
71
|
+
def experiments_id_map
|
72
|
+
feature_variables_map = feature_variable_map
|
73
|
+
audiences_id_map = audiences_map
|
41
74
|
@project_config.experiments.reduce({}) do |experiments_map, experiment|
|
75
|
+
feature_id = @project_config.experiment_feature_map.fetch(experiment['id'], []).first
|
42
76
|
experiments_map.update(
|
43
|
-
experiment['
|
77
|
+
experiment['id'] => {
|
44
78
|
'id' => experiment['id'],
|
45
79
|
'key' => experiment['key'],
|
46
|
-
'variationsMap' =>
|
47
|
-
|
48
|
-
'id' => variation['id'],
|
49
|
-
'key' => variation['key'],
|
50
|
-
'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map)
|
51
|
-
}
|
52
|
-
variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id'])
|
53
|
-
variations_map.update(variation['key'] => variation_object)
|
54
|
-
end
|
80
|
+
'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map),
|
81
|
+
'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || ''
|
55
82
|
}
|
56
83
|
)
|
57
84
|
end
|
58
85
|
end
|
59
86
|
|
87
|
+
def audiences_map
|
88
|
+
@audiences.reduce({}) do |audiences_map, optly_audience|
|
89
|
+
audiences_map.update(optly_audience['id'] => optly_audience['name'])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def experiments_map
|
94
|
+
experiments_id_map.values.reduce({}) do |experiments_key_map, experiment|
|
95
|
+
experiments_key_map.update(experiment['key'] => experiment)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def feature_variable_map
|
100
|
+
@project_config.feature_flags.reduce({}) do |result_map, feature|
|
101
|
+
result_map.update(feature['id'] => feature['variables'])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_variation_map(feature_id, experiment, feature_variables_map)
|
106
|
+
experiment['variations'].reduce({}) do |variations_map, variation|
|
107
|
+
variation_object = {
|
108
|
+
'id' => variation['id'],
|
109
|
+
'key' => variation['key'],
|
110
|
+
'featureEnabled' => variation['featureEnabled'],
|
111
|
+
'variablesMap' => get_merged_variables_map(variation, feature_id, feature_variables_map)
|
112
|
+
}
|
113
|
+
variations_map.update(variation['key'] => variation_object)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
60
117
|
# Merges feature key and type from feature variables to variation variables.
|
61
|
-
def get_merged_variables_map(variation,
|
62
|
-
|
63
|
-
return {} unless feature_ids
|
118
|
+
def get_merged_variables_map(variation, feature_id, feature_variables_map)
|
119
|
+
return {} unless feature_id
|
64
120
|
|
65
|
-
|
121
|
+
feature_variables = feature_variables_map[feature_id]
|
66
122
|
# temporary variation variables map to get values to merge.
|
67
123
|
temp_variables_id_map = {}
|
68
124
|
if variation['variables']
|
@@ -75,7 +131,7 @@ module Optimizely
|
|
75
131
|
)
|
76
132
|
end
|
77
133
|
end
|
78
|
-
|
134
|
+
feature_variables.reduce({}) do |variables_map, feature_variable|
|
79
135
|
variation_variable = temp_variables_id_map[feature_variable['id']]
|
80
136
|
variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue']
|
81
137
|
variables_map.update(
|
@@ -91,13 +147,15 @@ module Optimizely
|
|
91
147
|
|
92
148
|
def get_features_map(all_experiments_map)
|
93
149
|
@project_config.feature_flags.reduce({}) do |features_map, feature|
|
150
|
+
delivery_rules = get_delivery_rules(@rollouts, feature['rolloutId'], feature['id'])
|
94
151
|
features_map.update(
|
95
152
|
feature['key'] => {
|
96
153
|
'id' => feature['id'],
|
97
154
|
'key' => feature['key'],
|
155
|
+
# This experimentsMap is deprecated. Use experimentRules and deliveryRules instead.
|
98
156
|
'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id|
|
99
157
|
experiment_key = @project_config.experiment_id_map[experiment_id]['key']
|
100
|
-
experiments_map.update(experiment_key =>
|
158
|
+
experiments_map.update(experiment_key => experiments_id_map[experiment_id])
|
101
159
|
end,
|
102
160
|
'variablesMap' => feature['variables'].reduce({}) do |variables, variable|
|
103
161
|
variables.update(
|
@@ -108,10 +166,107 @@ module Optimizely
|
|
108
166
|
'value' => variable['defaultValue']
|
109
167
|
}
|
110
168
|
)
|
111
|
-
end
|
169
|
+
end,
|
170
|
+
'experimentRules' => feature['experimentIds'].reduce([]) do |experiments_map, experiment_id|
|
171
|
+
experiments_map.push(all_experiments_map[experiment_id])
|
172
|
+
end,
|
173
|
+
'deliveryRules' => delivery_rules
|
112
174
|
}
|
113
175
|
)
|
114
176
|
end
|
115
177
|
end
|
178
|
+
|
179
|
+
def get_attributes_list(attributes)
|
180
|
+
attributes.map do |attribute|
|
181
|
+
{
|
182
|
+
'id' => attribute['id'],
|
183
|
+
'key' => attribute['key']
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_events_list(events)
|
189
|
+
events.map do |event|
|
190
|
+
{
|
191
|
+
'id' => event['id'],
|
192
|
+
'key' => event['key'],
|
193
|
+
'experimentIds' => event['experimentIds']
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def lookup_name_from_id(audience_id, audiences_map)
|
199
|
+
audiences_map[audience_id] || audience_id
|
200
|
+
end
|
201
|
+
|
202
|
+
def stringify_conditions(conditions, audiences_map)
|
203
|
+
operand = 'OR'
|
204
|
+
conditions_str = ''
|
205
|
+
length = conditions.length()
|
206
|
+
return '' if length.zero?
|
207
|
+
return '"' + lookup_name_from_id(conditions[0], audiences_map) + '"' if length == 1 && !OPERATORS.include?(conditions[0])
|
208
|
+
|
209
|
+
# Edge cases for lengths 0, 1 or 2
|
210
|
+
if length == 2 && OPERATORS.include?(conditions[0]) && !conditions[1].is_a?(Array) && !OPERATORS.include?(conditions[1])
|
211
|
+
return '"' + lookup_name_from_id(conditions[1], audiences_map) + '"' if conditions[0] != 'not'
|
212
|
+
|
213
|
+
return conditions[0].upcase + ' "' + lookup_name_from_id(conditions[1], audiences_map) + '"'
|
214
|
+
|
215
|
+
end
|
216
|
+
if length > 1
|
217
|
+
(0..length - 1).each do |n|
|
218
|
+
# Operand is handled here and made Upper Case
|
219
|
+
if OPERATORS.include?(conditions[n])
|
220
|
+
operand = conditions[n].upcase
|
221
|
+
# Check if element is a list or not
|
222
|
+
elsif conditions[n].is_a?(Array)
|
223
|
+
# Check if at the end or not to determine where to add the operand
|
224
|
+
# Recursive call to call stringify on embedded list
|
225
|
+
conditions_str += if n + 1 < length
|
226
|
+
'(' + stringify_conditions(conditions[n], audiences_map) + ') '
|
227
|
+
else
|
228
|
+
operand + ' (' + stringify_conditions(conditions[n], audiences_map) + ')'
|
229
|
+
end
|
230
|
+
# If the item is not a list, we process as an audience ID and retrieve the name
|
231
|
+
else
|
232
|
+
audience_name = lookup_name_from_id(conditions[n], audiences_map)
|
233
|
+
unless audience_name.nil?
|
234
|
+
# Below handles all cases for one ID or greater
|
235
|
+
conditions_str += if n + 1 < length - 1
|
236
|
+
'"' + audience_name + '" ' + operand + ' '
|
237
|
+
elsif n + 1 == length
|
238
|
+
operand + ' "' + audience_name + '"'
|
239
|
+
else
|
240
|
+
'"' + audience_name + '" '
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
conditions_str || ''
|
247
|
+
end
|
248
|
+
|
249
|
+
def replace_ids_with_names(conditions, audiences_map)
|
250
|
+
!conditions.empty? ? stringify_conditions(conditions, audiences_map) : ''
|
251
|
+
end
|
252
|
+
|
253
|
+
def get_delivery_rules(rollouts, rollout_id, feature_id)
|
254
|
+
audiences_id_map = audiences_map
|
255
|
+
feature_variables_map = feature_variable_map
|
256
|
+
rollout = rollouts.select { |selected_rollout| selected_rollout['id'] == rollout_id }
|
257
|
+
if rollout.any?
|
258
|
+
rollout = rollout[0]
|
259
|
+
experiments = rollout['experiments']
|
260
|
+
return experiments.map do |experiment|
|
261
|
+
{
|
262
|
+
'id' => experiment['id'],
|
263
|
+
'key' => experiment['key'],
|
264
|
+
'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map),
|
265
|
+
'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || ''
|
266
|
+
}
|
267
|
+
end
|
268
|
+
end
|
269
|
+
[]
|
270
|
+
end
|
116
271
|
end
|
117
272
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2020, Optimizely and contributors
|
4
|
+
# Copyright 2020-2022, 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.
|
@@ -23,16 +23,26 @@ module Optimizely
|
|
23
23
|
# Representation of an Optimizely User Context using which APIs are to be called.
|
24
24
|
|
25
25
|
attr_reader :user_id
|
26
|
+
attr_reader :forced_decisions
|
27
|
+
attr_reader :OptimizelyDecisionContext
|
28
|
+
attr_reader :OptimizelyForcedDecision
|
29
|
+
attr_reader :optimizely_client
|
26
30
|
|
31
|
+
OptimizelyDecisionContext = Struct.new(:flag_key, :rule_key)
|
32
|
+
OptimizelyForcedDecision = Struct.new(:variation_key)
|
27
33
|
def initialize(optimizely_client, user_id, user_attributes)
|
28
34
|
@attr_mutex = Mutex.new
|
35
|
+
@forced_decision_mutex = Mutex.new
|
29
36
|
@optimizely_client = optimizely_client
|
30
37
|
@user_id = user_id
|
31
38
|
@user_attributes = user_attributes.nil? ? {} : user_attributes.clone
|
39
|
+
@forced_decisions = {}
|
32
40
|
end
|
33
41
|
|
34
42
|
def clone
|
35
|
-
OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes)
|
43
|
+
user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes)
|
44
|
+
@forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? }
|
45
|
+
user_context
|
36
46
|
end
|
37
47
|
|
38
48
|
def user_attributes
|
@@ -85,6 +95,68 @@ module Optimizely
|
|
85
95
|
@optimizely_client&.decide_all(clone, options)
|
86
96
|
end
|
87
97
|
|
98
|
+
# Sets the forced decision (variation key) for a given flag and an optional rule.
|
99
|
+
#
|
100
|
+
# @param context - An OptimizelyDecisionContext object containg flag key and rule key.
|
101
|
+
# @param decision - An OptimizelyForcedDecision object containing variation key
|
102
|
+
#
|
103
|
+
# @return - true if the forced decision has been set successfully.
|
104
|
+
|
105
|
+
def set_forced_decision(context, decision)
|
106
|
+
flag_key = context[:flag_key]
|
107
|
+
return false if flag_key.nil?
|
108
|
+
|
109
|
+
@forced_decision_mutex.synchronize { @forced_decisions[context] = decision }
|
110
|
+
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def find_forced_decision(context)
|
115
|
+
return nil if @forced_decisions.empty?
|
116
|
+
|
117
|
+
decision = nil
|
118
|
+
@forced_decision_mutex.synchronize { decision = @forced_decisions[context] }
|
119
|
+
decision
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the forced decision for a given flag and an optional rule.
|
123
|
+
#
|
124
|
+
# @param context - An OptimizelyDecisionContext object containg flag key and rule key.
|
125
|
+
#
|
126
|
+
# @return - A variation key or nil if forced decisions are not set for the parameters.
|
127
|
+
|
128
|
+
def get_forced_decision(context)
|
129
|
+
find_forced_decision(context)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Removes the forced decision for a given flag and an optional rule.
|
133
|
+
#
|
134
|
+
# @param context - An OptimizelyDecisionContext object containg flag key and rule key.
|
135
|
+
#
|
136
|
+
# @return - true if the forced decision has been removed successfully.
|
137
|
+
|
138
|
+
def remove_forced_decision(context)
|
139
|
+
deleted = false
|
140
|
+
@forced_decision_mutex.synchronize do
|
141
|
+
if @forced_decisions.key?(context)
|
142
|
+
@forced_decisions.delete(context)
|
143
|
+
deleted = true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
deleted
|
147
|
+
end
|
148
|
+
|
149
|
+
# Removes all forced decisions bound to this user context.
|
150
|
+
#
|
151
|
+
# @return - true if forced decisions have been removed successfully.
|
152
|
+
|
153
|
+
def remove_all_forced_decisions
|
154
|
+
return false if @optimizely_client&.get_optimizely_config.nil?
|
155
|
+
|
156
|
+
@forced_decision_mutex.synchronize { @forced_decisions.clear }
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
88
160
|
# Track an event
|
89
161
|
#
|
90
162
|
# @param event_key - Event key representing the event which needs to be recorded.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2016-
|
3
|
+
# Copyright 2016-2021, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -46,6 +46,10 @@ module Optimizely
|
|
46
46
|
|
47
47
|
def revision; end
|
48
48
|
|
49
|
+
def sdk_key; end
|
50
|
+
|
51
|
+
def environment_key; end
|
52
|
+
|
49
53
|
def send_flag_decisions; end
|
50
54
|
|
51
55
|
def rollouts; end
|
data/lib/optimizely/version.rb
CHANGED
data/lib/optimizely.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2022, 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.
|
@@ -198,17 +198,24 @@ module Optimizely
|
|
198
198
|
decision_event_dispatched = false
|
199
199
|
experiment = nil
|
200
200
|
decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
201
|
-
|
202
|
-
|
201
|
+
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
|
202
|
+
variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
|
203
203
|
reasons.push(*reasons_received)
|
204
204
|
|
205
|
+
if variation
|
206
|
+
decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
|
207
|
+
else
|
208
|
+
decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_context, decide_options)
|
209
|
+
reasons.push(*reasons_received)
|
210
|
+
end
|
211
|
+
|
205
212
|
# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
|
206
213
|
if decision.is_a?(Optimizely::DecisionService::Decision)
|
207
214
|
experiment = decision.experiment
|
208
|
-
rule_key = experiment['key']
|
215
|
+
rule_key = experiment ? experiment['key'] : nil
|
209
216
|
variation = decision['variation']
|
210
|
-
variation_key = variation['key']
|
211
|
-
feature_enabled = variation['featureEnabled']
|
217
|
+
variation_key = variation ? variation['key'] : nil
|
218
|
+
feature_enabled = variation ? variation['featureEnabled'] : false
|
212
219
|
decision_source = decision.source
|
213
220
|
end
|
214
221
|
|
@@ -291,6 +298,19 @@ module Optimizely
|
|
291
298
|
decisions
|
292
299
|
end
|
293
300
|
|
301
|
+
# Gets variation using variation key or id and flag key.
|
302
|
+
#
|
303
|
+
# @param flag_key - flag key from which the variation is required.
|
304
|
+
# @param target_value - variation value either id or key that will be matched.
|
305
|
+
# @param attribute - string representing variation attribute.
|
306
|
+
#
|
307
|
+
# @return [variation]
|
308
|
+
# @return [nil] if no variation found in flag_variation_map.
|
309
|
+
|
310
|
+
def get_flag_variation(flag_key, target_value, attribute)
|
311
|
+
project_config.get_variation_from_flag(flag_key, target_value, attribute)
|
312
|
+
end
|
313
|
+
|
294
314
|
# Buckets visitor and sends impression event to Optimizely.
|
295
315
|
#
|
296
316
|
# @param experiment_key - Experiment which needs to be activated.
|
@@ -490,7 +510,8 @@ module Optimizely
|
|
490
510
|
return false
|
491
511
|
end
|
492
512
|
|
493
|
-
|
513
|
+
user_context = create_user_context(user_id, attributes)
|
514
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
494
515
|
|
495
516
|
feature_enabled = false
|
496
517
|
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
@@ -739,7 +760,8 @@ module Optimizely
|
|
739
760
|
return nil
|
740
761
|
end
|
741
762
|
|
742
|
-
|
763
|
+
user_context = create_user_context(user_id, attributes)
|
764
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
743
765
|
variation = decision ? decision['variation'] : nil
|
744
766
|
feature_enabled = variation ? variation['featureEnabled'] : false
|
745
767
|
all_variables = {}
|
@@ -881,7 +903,8 @@ module Optimizely
|
|
881
903
|
|
882
904
|
return nil unless user_inputs_valid?(attributes)
|
883
905
|
|
884
|
-
|
906
|
+
user_context = create_user_context(user_id, attributes)
|
907
|
+
variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
|
885
908
|
variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
|
886
909
|
variation_key = variation['key'] if variation
|
887
910
|
decision_notification_type = if config.feature_experiment?(experiment_id)
|
@@ -947,7 +970,8 @@ module Optimizely
|
|
947
970
|
return nil
|
948
971
|
end
|
949
972
|
|
950
|
-
|
973
|
+
user_context = create_user_context(user_id, attributes)
|
974
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
951
975
|
variation = decision ? decision['variation'] : nil
|
952
976
|
feature_enabled = variation ? variation['featureEnabled'] : false
|
953
977
|
|
@@ -1083,8 +1107,12 @@ module Optimizely
|
|
1083
1107
|
experiment_id = experiment['id']
|
1084
1108
|
experiment_key = experiment['key']
|
1085
1109
|
|
1086
|
-
variation_id =
|
1087
|
-
|
1110
|
+
variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) unless experiment_id.empty?
|
1111
|
+
|
1112
|
+
unless variation_id
|
1113
|
+
variation = !flag_key.empty? ? get_flag_variation(flag_key, variation_key, 'key') : nil
|
1114
|
+
variation_id = variation ? variation['id'] : ''
|
1115
|
+
end
|
1088
1116
|
|
1089
1117
|
metadata = {
|
1090
1118
|
flag_key: flag_key,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: optimizely-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Optimizely
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|