optimizely-sdk 3.6.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d8204dce32c85bea56820607c0268b363fb86e7506ce685a2675645b4ef581b
4
- data.tar.gz: dce7cd63fde5e24e179a74f8f14c6a22ed389028faac38a373af105683b596f0
3
+ metadata.gz: 3502b43c8fae064fda3e1b237786a0285103d4ce9383a9d64cfc7b34f3f8c849
4
+ data.tar.gz: 9b843fc3a7999e640f329812aeeb8e26474b5b85f29d27c0647ba7c3685cb73f
5
5
  SHA512:
6
- metadata.gz: 5cc88f9802b38a2eb29354507303c6dcdcf6a402f721d24689f1e42a52a79319568974cf3584c6bf94398ff5f1410aacf49a1adf7aa27ef9105f3e7b23d39e3d
7
- data.tar.gz: a6f0a7f052bb851ce760c4c88e0fe0dea18c14b42d5bcf457ac9cfefe3eaa2027f1b847c2ab0c133233618502cfe9207b1e82a5b1d65d0d4a678b58f5ea0ff88
6
+ metadata.gz: a244307de60cf4a7e9b4e90bd0282e071e310701c09b4314a5f011f9f97a1f638205dc9e3de59611c247315314bfd58a0521690bbc3fa876575f3ef8fa917306
7
+ data.tar.gz: b68df42eef86c3a4e1fd22f2b1b4cd3060df4ade0c9125862443164735778a16415af958a4002fba7adcdf078d1552a5ffef446bac22e7a269a5984445d1fd50
@@ -38,6 +38,7 @@ module Optimizely
38
38
  # This defaults to experiment['key'].
39
39
  #
40
40
  # Returns boolean representing if user satisfies audience conditions for the audiences or not.
41
+ decide_reasons = []
41
42
  logging_hash ||= 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'
42
43
  logging_key ||= experiment['key']
43
44
 
@@ -45,26 +46,15 @@ module Optimizely
45
46
 
46
47
  audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
47
48
 
48
- logger.log(
49
- Logger::DEBUG,
50
- format(
51
- logs_hash['EVALUATING_AUDIENCES_COMBINED'],
52
- logging_key,
53
- audience_conditions
54
- )
55
- )
49
+ message = format(logs_hash['EVALUATING_AUDIENCES_COMBINED'], logging_key, audience_conditions)
50
+ logger.log(Logger::DEBUG, message)
56
51
 
57
52
  # Return true if there are no audiences
58
53
  if audience_conditions.empty?
59
- logger.log(
60
- Logger::INFO,
61
- format(
62
- logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
63
- logging_key,
64
- 'TRUE'
65
- )
66
- )
67
- return true
54
+ message = format(logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'], logging_key, 'TRUE')
55
+ logger.log(Logger::INFO, message)
56
+ decide_reasons.push(message)
57
+ return true, decide_reasons
68
58
  end
69
59
 
70
60
  attributes ||= {}
@@ -80,39 +70,28 @@ module Optimizely
80
70
  return nil unless audience
81
71
 
82
72
  audience_conditions = audience['conditions']
83
- logger.log(
84
- Logger::DEBUG,
85
- format(
86
- logs_hash['EVALUATING_AUDIENCE'],
87
- audience_id,
88
- audience_conditions
89
- )
90
- )
73
+ message = format(logs_hash['EVALUATING_AUDIENCE'], audience_id, audience_conditions)
74
+ logger.log(Logger::DEBUG, message)
75
+ decide_reasons.push(message)
91
76
 
92
77
  audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
93
78
  result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
94
79
  result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
95
- logger.log(
96
- Logger::DEBUG,
97
- format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
98
- )
80
+ message = format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
81
+ logger.log(Logger::DEBUG, message)
82
+ decide_reasons.push(message)
83
+
99
84
  result
100
85
  end
101
86
 
102
87
  eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
103
-
104
88
  eval_result ||= false
105
89
 
106
- logger.log(
107
- Logger::INFO,
108
- format(
109
- logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
110
- logging_key,
111
- eval_result.to_s.upcase
112
- )
113
- )
90
+ message = format(logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'], logging_key, eval_result.to_s.upcase)
91
+ logger.log(Logger::INFO, message)
92
+ decide_reasons.push(message)
114
93
 
115
- eval_result
94
+ [eval_result, decide_reasons]
116
95
  end
117
96
  end
118
97
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2017, 2019-2020 Optimizely and contributors
4
+ # Copyright 2016-2017, 2019-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.
@@ -44,7 +44,9 @@ module Optimizely
44
44
  # user_id - String ID for user.
45
45
  #
46
46
  # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
47
- return nil if experiment.nil?
47
+ return nil, [] if experiment.nil?
48
+
49
+ decide_reasons = []
48
50
 
49
51
  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
50
52
  # this will not affect evaluation of rollout rules.
@@ -55,46 +57,49 @@ module Optimizely
55
57
  group = project_config.group_id_map.fetch(group_id)
56
58
  if Helpers::Group.random_policy?(group)
57
59
  traffic_allocations = group.fetch('trafficAllocation')
58
- bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
60
+ bucketed_experiment_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
61
+ decide_reasons.push(*find_bucket_reasons)
62
+
59
63
  # return if the user is not bucketed into any experiment
60
64
  unless bucketed_experiment_id
61
- @logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
62
- return nil
65
+ message = "User '#{user_id}' is in no experiment."
66
+ @logger.log(Logger::INFO, message)
67
+ decide_reasons.push(message)
68
+ return nil, decide_reasons
63
69
  end
64
70
 
65
71
  # return if the user is bucketed into a different experiment than the one specified
66
72
  if bucketed_experiment_id != experiment_id
67
- @logger.log(
68
- Logger::INFO,
69
- "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
70
- )
71
- return nil
73
+ message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
74
+ @logger.log(Logger::INFO, message)
75
+ decide_reasons.push(message)
76
+ return nil, decide_reasons
72
77
  end
73
78
 
74
79
  # continue bucketing if the user is bucketed into the experiment specified
75
- @logger.log(
76
- Logger::INFO,
77
- "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
78
- )
80
+ message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
81
+ @logger.log(Logger::INFO, message)
82
+ decide_reasons.push(message)
79
83
  end
80
84
  end
81
85
 
82
86
  traffic_allocations = experiment['trafficAllocation']
83
- variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
87
+ variation_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
88
+ decide_reasons.push(*find_bucket_reasons)
89
+
84
90
  if variation_id && variation_id != ''
85
- variation = project_config.get_variation_from_id(experiment_key, variation_id)
86
- return variation
91
+ variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
92
+ return variation, decide_reasons
87
93
  end
88
94
 
89
95
  # Handle the case when the traffic range is empty due to sticky bucketing
90
96
  if variation_id == ''
91
- @logger.log(
92
- Logger::DEBUG,
93
- 'Bucketed into an empty traffic range. Returning nil.'
94
- )
97
+ message = 'Bucketed into an empty traffic range. Returning nil.'
98
+ @logger.log(Logger::DEBUG, message)
99
+ decide_reasons.push(message)
95
100
  end
96
101
 
97
- nil
102
+ [nil, decide_reasons]
98
103
  end
99
104
 
100
105
  def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
@@ -105,21 +110,24 @@ module Optimizely
105
110
  # parent_id - String entity ID to use for bucketing ID
106
111
  # traffic_allocations - Array of traffic allocations
107
112
  #
108
- # Returns entity ID corresponding to the provided bucket value or nil if no match is found.
113
+ # Returns and array of two values where first value is the entity ID corresponding to the provided bucket value
114
+ # or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken
115
+ decide_reasons = []
109
116
  bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
110
117
  bucket_value = generate_bucket_value(bucketing_key)
111
- @logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
112
- "with bucketing ID: '#{bucketing_id}'.")
118
+
119
+ message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
120
+ @logger.log(Logger::DEBUG, message)
113
121
 
114
122
  traffic_allocations.each do |traffic_allocation|
115
123
  current_end_of_range = traffic_allocation['endOfRange']
116
124
  if bucket_value < current_end_of_range
117
125
  entity_id = traffic_allocation['entityId']
118
- return entity_id
126
+ return entity_id, decide_reasons
119
127
  end
120
128
  end
121
129
 
122
- nil
130
+ [nil, decide_reasons]
123
131
  end
124
132
 
125
133
  private
@@ -28,6 +28,8 @@ module Optimizely
28
28
  NOT_CONDITION => :not_evaluator
29
29
  }.freeze
30
30
 
31
+ OPERATORS = [AND_CONDITION, OR_CONDITION, NOT_CONDITION].freeze
32
+
31
33
  module_function
32
34
 
33
35
  def evaluate(conditions, leaf_evaluator)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019-2020, Optimizely and contributors
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,8 +38,11 @@ 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
45
+ attr_reader :send_flag_decisions
43
46
 
44
47
  attr_reader :attribute_key_map
45
48
  attr_reader :audience_id_map
@@ -51,10 +54,12 @@ module Optimizely
51
54
  attr_reader :feature_variable_key_map
52
55
  attr_reader :group_id_map
53
56
  attr_reader :rollout_id_map
54
- attr_reader :rollout_experiment_key_map
57
+ attr_reader :rollout_experiment_id_map
55
58
  attr_reader :variation_id_map
56
59
  attr_reader :variation_id_to_variable_usage_map
57
60
  attr_reader :variation_key_map
61
+ attr_reader :variation_id_map_by_experiment_id
62
+ attr_reader :variation_key_map_by_experiment_id
58
63
 
59
64
  def initialize(datafile, logger, error_handler)
60
65
  # ProjectConfig init method to fetch and set project config data
@@ -82,7 +87,10 @@ module Optimizely
82
87
  @anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
83
88
  @bot_filtering = config['botFiltering']
84
89
  @revision = config['revision']
90
+ @sdk_key = config.fetch('sdkKey', '')
91
+ @environment_key = config.fetch('environmentKey', '')
85
92
  @rollouts = config.fetch('rollouts', [])
93
+ @send_flag_decisions = config.fetch('sendFlagDecisions', false)
86
94
 
87
95
  # Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
88
96
  # Converting it to a first-class json type while creating Project Config
@@ -111,9 +119,11 @@ module Optimizely
111
119
  @audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
112
120
  @variation_id_map = {}
113
121
  @variation_key_map = {}
122
+ @variation_id_map_by_experiment_id = {}
123
+ @variation_key_map_by_experiment_id = {}
114
124
  @variation_id_to_variable_usage_map = {}
115
125
  @variation_id_to_experiment_map = {}
116
- @experiment_key_map.each_value do |exp|
126
+ @experiment_id_map.each_value do |exp|
117
127
  # Excludes experiments from rollouts
118
128
  variations = exp.fetch('variations')
119
129
  variations.each do |variation|
@@ -123,13 +133,13 @@ module Optimizely
123
133
  end
124
134
  @rollout_id_map = generate_key_map(@rollouts, 'id')
125
135
  # split out the experiment key map for rollouts
126
- @rollout_experiment_key_map = {}
136
+ @rollout_experiment_id_map = {}
127
137
  @rollout_id_map.each_value do |rollout|
128
138
  exps = rollout.fetch('experiments')
129
- @rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'key'))
139
+ @rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
130
140
  end
131
- @all_experiments = @experiment_key_map.merge(@rollout_experiment_key_map)
132
- @all_experiments.each do |key, exp|
141
+ @all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
142
+ @all_experiments.each do |id, exp|
133
143
  variations = exp.fetch('variations')
134
144
  variations.each do |variation|
135
145
  variation_id = variation['id']
@@ -139,8 +149,10 @@ module Optimizely
139
149
 
140
150
  @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
141
151
  end
142
- @variation_id_map[key] = generate_key_map(variations, 'id')
143
- @variation_key_map[key] = generate_key_map(variations, 'key')
152
+ @variation_id_map[exp['key']] = generate_key_map(variations, 'id')
153
+ @variation_key_map[exp['key']] = generate_key_map(variations, 'key')
154
+ @variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
155
+ @variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
144
156
  end
145
157
  @feature_flag_key_map = generate_key_map(@feature_flags, 'key')
146
158
  @experiment_feature_map = {}
@@ -207,6 +219,21 @@ module Optimizely
207
219
  nil
208
220
  end
209
221
 
222
+ def get_experiment_from_id(experiment_id)
223
+ # Retrieves experiment ID for a given key
224
+ #
225
+ # experiment_id - String id representing the experiment
226
+ #
227
+ # Returns Experiment or nil if not found
228
+
229
+ experiment = @experiment_id_map[experiment_id]
230
+ return experiment if experiment
231
+
232
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
233
+ @error_handler.handle_error InvalidExperimentError
234
+ nil
235
+ end
236
+
210
237
  def get_experiment_key(experiment_id)
211
238
  # Retrieves experiment key for a given ID.
212
239
  #
@@ -275,6 +302,52 @@ module Optimizely
275
302
  nil
276
303
  end
277
304
 
305
+ def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
306
+ # Get variation given experiment ID and variation ID
307
+ #
308
+ # experiment_id - ID representing parent experiment of variation
309
+ # variation_id - ID of the variation
310
+ #
311
+ # Returns the variation or nil if not found
312
+
313
+ variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
314
+ if variation_id_map_by_experiment_id
315
+ variation = variation_id_map_by_experiment_id[variation_id]
316
+ return variation if variation
317
+
318
+ @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
319
+ @error_handler.handle_error InvalidVariationError
320
+ return nil
321
+ end
322
+
323
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
324
+ @error_handler.handle_error InvalidExperimentError
325
+ nil
326
+ end
327
+
328
+ def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
329
+ # Get variation given experiment ID and variation key
330
+ #
331
+ # experiment_id - ID representing parent experiment of variation
332
+ # variation_key - Key of the variation
333
+ #
334
+ # Returns the variation or nil if not found
335
+
336
+ variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
337
+ if variation_key_map
338
+ variation = variation_key_map[variation_key]
339
+ return variation['id'] if variation
340
+
341
+ @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
342
+ @error_handler.handle_error InvalidVariationError
343
+ return nil
344
+ end
345
+
346
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
347
+ @error_handler.handle_error InvalidExperimentError
348
+ nil
349
+ end
350
+
278
351
  def get_variation_id_from_key(experiment_key, variation_key)
279
352
  # Get variation ID given experiment key and variation key
280
353
  #
@@ -298,17 +371,17 @@ module Optimizely
298
371
  nil
299
372
  end
300
373
 
301
- def get_whitelisted_variations(experiment_key)
302
- # Retrieves whitelisted variations for a given experiment Key
374
+ def get_whitelisted_variations(experiment_id)
375
+ # Retrieves whitelisted variations for a given experiment id
303
376
  #
304
- # experiment_key - String Key representing the experiment
377
+ # experiment_id - String id representing the experiment
305
378
  #
306
379
  # Returns whitelisted variations for the experiment or nil
307
380
 
308
- experiment = @experiment_key_map[experiment_key]
381
+ experiment = @experiment_id_map[experiment_id]
309
382
  return experiment['forcedVariations'] if experiment
310
383
 
311
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
384
+ @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
312
385
  @error_handler.handle_error InvalidExperimentError
313
386
  end
314
387
 
@@ -409,6 +482,16 @@ module Optimizely
409
482
  @experiment_feature_map.key?(experiment_id)
410
483
  end
411
484
 
485
+ def rollout_experiment?(experiment_id)
486
+ # Determines if given experiment is a rollout test.
487
+ #
488
+ # experiment_id - String experiment ID
489
+ #
490
+ # Returns true if experiment belongs to any rollout,
491
+ # false otherwise.
492
+ @rollout_experiment_id_map.key?(experiment_id)
493
+ end
494
+
412
495
  private
413
496
 
414
497
  def generate_key_map(array, key)
@@ -0,0 +1,28 @@
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 OptimizelyDecideOption
21
+ DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT'
22
+ ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY'
23
+ IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE'
24
+ INCLUDE_REASONS = 'INCLUDE_REASONS'
25
+ EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES'
26
+ end
27
+ end
28
+ end