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 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