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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, 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.
@@ -17,19 +17,21 @@
17
17
  #
18
18
  module Optimizely
19
19
  class Decision
20
- attr_reader :campaign_id, :experiment_id, :variation_id
20
+ attr_reader :campaign_id, :experiment_id, :variation_id, :metadata
21
21
 
22
- def initialize(campaign_id:, experiment_id:, variation_id:)
22
+ def initialize(campaign_id:, experiment_id:, variation_id:, metadata:)
23
23
  @campaign_id = campaign_id
24
24
  @experiment_id = experiment_id
25
25
  @variation_id = variation_id
26
+ @metadata = metadata
26
27
  end
27
28
 
28
29
  def as_json
29
30
  {
30
31
  campaign_id: @campaign_id,
31
32
  experiment_id: @experiment_id,
32
- variation_id: @variation_id
33
+ variation_id: @variation_id,
34
+ metadata: @metadata
33
35
  }
34
36
  end
35
37
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, 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.
@@ -19,7 +19,7 @@ require_relative 'user_event'
19
19
  require 'optimizely/helpers/date_time_utils'
20
20
  module Optimizely
21
21
  class ImpressionEvent < UserEvent
22
- attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id,
22
+ attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id, :metadata,
23
23
  :visitor_attributes, :bot_filtering
24
24
 
25
25
  def initialize(
@@ -28,6 +28,7 @@ module Optimizely
28
28
  experiment_layer_id:,
29
29
  experiment_id:,
30
30
  variation_id:,
31
+ metadata:,
31
32
  visitor_attributes:,
32
33
  bot_filtering:
33
34
  )
@@ -38,6 +39,7 @@ module Optimizely
38
39
  @experiment_layer_id = experiment_layer_id
39
40
  @experiment_id = experiment_id
40
41
  @variation_id = variation_id
42
+ @metadata = metadata
41
43
  @visitor_attributes = visitor_attributes
42
44
  @bot_filtering = bot_filtering
43
45
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, 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.
@@ -101,10 +101,11 @@ module Optimizely
101
101
  private
102
102
 
103
103
  def create_impression_event_visitor(impression_event)
104
- decision = Optimizely::Decision.new(
104
+ decision = Decision.new(
105
105
  campaign_id: impression_event.experiment_layer_id,
106
106
  experiment_id: impression_event.experiment_id,
107
- variation_id: impression_event.variation_id
107
+ variation_id: impression_event.variation_id,
108
+ metadata: impression_event.metadata
108
109
  )
109
110
 
110
111
  snapshot_event = Optimizely::SnapshotEvent.new(
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, 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.
@@ -22,7 +22,7 @@ require_relative 'event_factory'
22
22
  module Optimizely
23
23
  class UserEventFactory
24
24
  # UserEventFactory builds ImpressionEvent and ConversionEvent objects from a given user_event.
25
- def self.create_impression_event(project_config, experiment, variation_id, user_id, user_attributes)
25
+ def self.create_impression_event(project_config, experiment, variation_id, metadata, user_id, user_attributes)
26
26
  # Create impression Event to be sent to the logging endpoint.
27
27
  #
28
28
  # project_config - Instance of ProjectConfig
@@ -42,13 +42,14 @@ module Optimizely
42
42
  ).as_json
43
43
 
44
44
  visitor_attributes = Optimizely::EventFactory.build_attribute_list(user_attributes, project_config)
45
- experiment_layer_id = project_config.experiment_key_map[experiment['key']]['layerId']
45
+ experiment_layer_id = experiment['layerId']
46
46
  Optimizely::ImpressionEvent.new(
47
47
  event_context: event_context,
48
48
  user_id: user_id,
49
49
  experiment_layer_id: experiment_layer_id,
50
50
  experiment_id: experiment['id'],
51
51
  variation_id: variation_id,
52
+ metadata: metadata,
52
53
  visitor_attributes: visitor_attributes,
53
54
  bot_filtering: project_config.bot_filtering
54
55
  )
@@ -369,6 +369,7 @@ module Optimizely
369
369
  'FEATURE' => 'feature',
370
370
  'FEATURE_TEST' => 'feature-test',
371
371
  'FEATURE_VARIABLE' => 'feature-variable',
372
+ 'FLAG' => 'flag',
372
373
  'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
373
374
  }.freeze
374
375
 
@@ -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.
@@ -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(experiments_map_object)
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 experiments_map
38
- feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature|
39
- result_map.update(feature['id'] => feature['variables'])
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['key'] => {
77
+ experiment['id'] => {
44
78
  'id' => experiment['id'],
45
79
  'key' => experiment['key'],
46
- 'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation|
47
- variation_object = {
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, experiment_id, feature_variables_map)
62
- feature_ids = @project_config.experiment_feature_map[experiment_id]
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
- experiment_feature_variables = feature_variables_map[feature_ids[0]]
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
- experiment_feature_variables.reduce({}) do |variables_map, feature_variable|
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 => all_experiments_map[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
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2020, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'json'
20
+
21
+ module Optimizely
22
+ class OptimizelyUserContext
23
+ # Representation of an Optimizely User Context using which APIs are to be called.
24
+
25
+ attr_reader :user_id
26
+
27
+ def initialize(optimizely_client, user_id, user_attributes)
28
+ @attr_mutex = Mutex.new
29
+ @optimizely_client = optimizely_client
30
+ @user_id = user_id
31
+ @user_attributes = user_attributes.nil? ? {} : user_attributes.clone
32
+ end
33
+
34
+ def clone
35
+ OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes)
36
+ end
37
+
38
+ def user_attributes
39
+ @attr_mutex.synchronize { @user_attributes.clone }
40
+ end
41
+
42
+ # Set an attribute for a given key
43
+ #
44
+ # @param key - An attribute key
45
+ # @param value - An attribute value
46
+
47
+ def set_attribute(attribute_key, attribute_value)
48
+ @attr_mutex.synchronize { @user_attributes[attribute_key] = attribute_value }
49
+ end
50
+
51
+ # Returns a decision result (OptimizelyDecision) for a given flag key and a user context, which contains all data required to deliver the flag.
52
+ #
53
+ # If the SDK finds an error, it'll return a `decision` with nil for `variation_key`. The decision will include an error message in `reasons`
54
+ #
55
+ # @param key -A flag key for which a decision will be made
56
+ # @param options - A list of options for decision making.
57
+ #
58
+ # @return [OptimizelyDecision] A decision result
59
+
60
+ def decide(key, options = nil)
61
+ @optimizely_client&.decide(clone, key, options)
62
+ end
63
+
64
+ # Returns a hash of decision results (OptimizelyDecision) for multiple flag keys and a user context.
65
+ #
66
+ # If the SDK finds an error for a key, the response will include a decision for the key showing `reasons` for the error.
67
+ # The SDK will always return hash of decisions. When it can not process requests, it'll return an empty hash after logging the errors.
68
+ #
69
+ # @param keys - A list of flag keys for which the decisions will be made.
70
+ # @param options - A list of options for decision making.
71
+ #
72
+ # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
73
+
74
+ def decide_for_keys(keys, options = nil)
75
+ @optimizely_client&.decide_for_keys(clone, keys, options)
76
+ end
77
+
78
+ # Returns a hash of decision results (OptimizelyDecision) for all active flag keys.
79
+ #
80
+ # @param options - A list of options for decision making.
81
+ #
82
+ # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
83
+
84
+ def decide_all(options = nil)
85
+ @optimizely_client&.decide_all(clone, options)
86
+ end
87
+
88
+ # Track an event
89
+ #
90
+ # @param event_key - Event key representing the event which needs to be recorded.
91
+
92
+ def track_event(event_key, event_tags = nil)
93
+ @optimizely_client&.track(event_key, @user_id, user_attributes, event_tags)
94
+ end
95
+
96
+ def as_json
97
+ {
98
+ user_id: @user_id,
99
+ attributes: @user_attributes
100
+ }
101
+ end
102
+
103
+ def to_json(*args)
104
+ as_json.to_json(*args)
105
+ end
106
+ end
107
+ end