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