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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2016-2020, Optimizely and contributors
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,12 +46,20 @@ module Optimizely
46
46
 
47
47
  def revision; end
48
48
 
49
+ def sdk_key; end
50
+
51
+ def environment_key; end
52
+
53
+ def send_flag_decisions; end
54
+
49
55
  def rollouts; end
50
56
 
51
57
  def experiment_running?(experiment); end
52
58
 
53
59
  def get_experiment_from_key(experiment_key); end
54
60
 
61
+ def get_experiment_from_id(experiment_id); end
62
+
55
63
  def get_experiment_key(experiment_id); end
56
64
 
57
65
  def get_event_from_key(event_key); end
@@ -60,9 +68,13 @@ module Optimizely
60
68
 
61
69
  def get_variation_from_id(experiment_key, variation_id); end
62
70
 
71
+ def get_variation_from_id_by_experiment_id(experiment_id, variation_id); end
72
+
73
+ def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key); end
74
+
63
75
  def get_variation_id_from_key(experiment_key, variation_key); end
64
76
 
65
- def get_whitelisted_variations(experiment_key); end
77
+ def get_whitelisted_variations(experiment_id); end
66
78
 
67
79
  def get_attribute_id(attribute_key); end
68
80
 
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '3.6.0'
20
+ VERSION = '3.9.0'
21
21
  end
data/lib/optimizely.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2020, Optimizely and contributors
4
+ # Copyright 2016-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.
@@ -19,6 +19,9 @@ require_relative 'optimizely/audience'
19
19
  require_relative 'optimizely/config/datafile_project_config'
20
20
  require_relative 'optimizely/config_manager/http_project_config_manager'
21
21
  require_relative 'optimizely/config_manager/static_project_config_manager'
22
+ require_relative 'optimizely/decide/optimizely_decide_option'
23
+ require_relative 'optimizely/decide/optimizely_decision'
24
+ require_relative 'optimizely/decide/optimizely_decision_message'
22
25
  require_relative 'optimizely/decision_service'
23
26
  require_relative 'optimizely/error_handler'
24
27
  require_relative 'optimizely/event_builder'
@@ -34,9 +37,12 @@ require_relative 'optimizely/helpers/variable_type'
34
37
  require_relative 'optimizely/logger'
35
38
  require_relative 'optimizely/notification_center'
36
39
  require_relative 'optimizely/optimizely_config'
40
+ require_relative 'optimizely/optimizely_user_context'
37
41
 
38
42
  module Optimizely
39
43
  class Project
44
+ include Optimizely::Decide
45
+
40
46
  attr_reader :notification_center
41
47
  # @api no-doc
42
48
  attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
@@ -67,12 +73,21 @@ module Optimizely
67
73
  sdk_key = nil,
68
74
  config_manager = nil,
69
75
  notification_center = nil,
70
- event_processor = nil
76
+ event_processor = nil,
77
+ default_decide_options = []
71
78
  )
72
79
  @logger = logger || NoOpLogger.new
73
80
  @error_handler = error_handler || NoOpErrorHandler.new
74
81
  @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
75
82
  @user_profile_service = user_profile_service
83
+ @default_decide_options = []
84
+
85
+ if default_decide_options.is_a? Array
86
+ @default_decide_options = default_decide_options.clone
87
+ else
88
+ @logger.log(Logger::DEBUG, 'Provided default decide options is not an array.')
89
+ @default_decide_options = []
90
+ end
76
91
 
77
92
  begin
78
93
  validate_instantiation_options
@@ -107,6 +122,175 @@ module Optimizely
107
122
  end
108
123
  end
109
124
 
125
+ # Create a context of the user for which decision APIs will be called.
126
+ #
127
+ # A user context will be created successfully even when the SDK is not fully configured yet.
128
+ #
129
+ # @param user_id - The user ID to be used for bucketing.
130
+ # @param attributes - A Hash representing user attribute names and values.
131
+ #
132
+ # @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient.
133
+ # @return [nil] If user attributes are not in valid format.
134
+
135
+ def create_user_context(user_id, attributes = nil)
136
+ # We do not check for is_valid here as a user context can be created successfully
137
+ # even when the SDK is not fully configured.
138
+
139
+ # validate user_id
140
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
141
+ {
142
+ user_id: user_id
143
+ }, @logger, Logger::ERROR
144
+ )
145
+
146
+ # validate attributes
147
+ return nil unless user_inputs_valid?(attributes)
148
+
149
+ user_context = OptimizelyUserContext.new(self, user_id, attributes)
150
+ user_context
151
+ end
152
+
153
+ def decide(user_context, key, decide_options = [])
154
+ # raising on user context as it is internal and not provided directly by the user.
155
+ raise if user_context.class != OptimizelyUserContext
156
+
157
+ reasons = []
158
+
159
+ # check if SDK is ready
160
+ unless is_valid
161
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
162
+ reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
163
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
164
+ end
165
+
166
+ # validate that key is a string
167
+ unless key.is_a?(String)
168
+ @logger.log(Logger::ERROR, 'Provided key is invalid')
169
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
170
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
171
+ end
172
+
173
+ # validate that key maps to a feature flag
174
+ config = project_config
175
+ feature_flag = config.get_feature_flag_from_key(key)
176
+ unless feature_flag
177
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
178
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
179
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
180
+ end
181
+
182
+ # merge decide_options and default_decide_options
183
+ if decide_options.is_a? Array
184
+ decide_options += @default_decide_options
185
+ else
186
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
187
+ decide_options = @default_decide_options
188
+ end
189
+
190
+ # Create Optimizely Decision Result.
191
+ user_id = user_context.user_id
192
+ attributes = user_context.user_attributes
193
+ variation_key = nil
194
+ feature_enabled = false
195
+ rule_key = nil
196
+ flag_key = key
197
+ all_variables = {}
198
+ decision_event_dispatched = false
199
+ experiment = nil
200
+ decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
201
+
202
+ decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes, decide_options)
203
+ reasons.push(*reasons_received)
204
+
205
+ # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
206
+ if decision.is_a?(Optimizely::DecisionService::Decision)
207
+ experiment = decision.experiment
208
+ rule_key = experiment['key']
209
+ variation = decision['variation']
210
+ variation_key = variation['key']
211
+ feature_enabled = variation['featureEnabled']
212
+ decision_source = decision.source
213
+ end
214
+
215
+ unless decide_options.include? OptimizelyDecideOption::DISABLE_DECISION_EVENT
216
+ if decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions
217
+ send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes)
218
+ decision_event_dispatched = true
219
+ end
220
+ end
221
+
222
+ # Generate all variables map if decide options doesn't include excludeVariables
223
+ unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
224
+ feature_flag['variables'].each do |variable|
225
+ variable_value = get_feature_variable_for_variation(key, feature_enabled, variation, variable, user_id)
226
+ all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
227
+ end
228
+ end
229
+
230
+ should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS
231
+
232
+ # Send notification
233
+ @notification_center.send_notifications(
234
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
235
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
236
+ user_id, (attributes || {}),
237
+ flag_key: flag_key,
238
+ enabled: feature_enabled,
239
+ variables: all_variables,
240
+ variation_key: variation_key,
241
+ rule_key: rule_key,
242
+ reasons: should_include_reasons ? reasons : [],
243
+ decision_event_dispatched: decision_event_dispatched
244
+ )
245
+
246
+ OptimizelyDecision.new(
247
+ variation_key: variation_key,
248
+ enabled: feature_enabled,
249
+ variables: all_variables,
250
+ rule_key: rule_key,
251
+ flag_key: flag_key,
252
+ user_context: user_context,
253
+ reasons: should_include_reasons ? reasons : []
254
+ )
255
+ end
256
+
257
+ def decide_all(user_context, decide_options = [])
258
+ # raising on user context as it is internal and not provided directly by the user.
259
+ raise if user_context.class != OptimizelyUserContext
260
+
261
+ # check if SDK is ready
262
+ unless is_valid
263
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_all').message)
264
+ return {}
265
+ end
266
+
267
+ keys = []
268
+ project_config.feature_flags.each do |feature_flag|
269
+ keys.push(feature_flag['key'])
270
+ end
271
+ decide_for_keys(user_context, keys, decide_options)
272
+ end
273
+
274
+ def decide_for_keys(user_context, keys, decide_options = [])
275
+ # raising on user context as it is internal and not provided directly by the user.
276
+ raise if user_context.class != OptimizelyUserContext
277
+
278
+ # check if SDK is ready
279
+ unless is_valid
280
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_for_keys').message)
281
+ return {}
282
+ end
283
+
284
+ enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
285
+
286
+ decisions = {}
287
+ keys.each do |key|
288
+ decision = decide(user_context, key, decide_options)
289
+ decisions[key] = decision unless enabled_flags_only && !decision.enabled
290
+ end
291
+ decisions
292
+ end
293
+
110
294
  # Buckets visitor and sends impression event to Optimizely.
111
295
  #
112
296
  # @param experiment_key - Experiment which needs to be activated.
@@ -140,7 +324,10 @@ module Optimizely
140
324
 
141
325
  # Create and dispatch impression event
142
326
  experiment = config.get_experiment_from_key(experiment_key)
143
- send_impression(config, experiment, variation_key, user_id, attributes)
327
+ send_impression(
328
+ config, experiment, variation_key, '', experiment_key, true,
329
+ Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes
330
+ )
144
331
 
145
332
  variation_key
146
333
  end
@@ -219,7 +406,7 @@ module Optimizely
219
406
  config = project_config
220
407
 
221
408
  forced_variation_key = nil
222
- forced_variation = @decision_service.get_forced_variation(config, experiment_key, user_id)
409
+ forced_variation, = @decision_service.get_forced_variation(config, experiment_key, user_id)
223
410
  forced_variation_key = forced_variation['key'] if forced_variation
224
411
 
225
412
  forced_variation_key
@@ -303,7 +490,7 @@ module Optimizely
303
490
  return false
304
491
  end
305
492
 
306
- decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
493
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
307
494
 
308
495
  feature_enabled = false
309
496
  source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
@@ -316,14 +503,23 @@ module Optimizely
316
503
  experiment_key: decision.experiment['key'],
317
504
  variation_key: variation['key']
318
505
  }
319
- # Send event if Decision came from an experiment.
320
- send_impression(config, decision.experiment, variation['key'], user_id, attributes)
321
- else
322
- @logger.log(Logger::DEBUG,
323
- "The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
506
+ # Send event if Decision came from a feature test.
507
+ send_impression(
508
+ config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
509
+ )
510
+ elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions
511
+ send_impression(
512
+ config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
513
+ )
324
514
  end
325
515
  end
326
516
 
517
+ if decision.nil? && config.send_flag_decisions
518
+ send_impression(
519
+ config, nil, '', feature_flag_key, '', feature_enabled, source_string, user_id, attributes
520
+ )
521
+ end
522
+
327
523
  @notification_center.send_notifications(
328
524
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
329
525
  Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
@@ -543,7 +739,7 @@ module Optimizely
543
739
  return nil
544
740
  end
545
741
 
546
- decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
742
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
547
743
  variation = decision ? decision['variation'] : nil
548
744
  feature_enabled = variation ? variation['featureEnabled'] : false
549
745
  all_variables = {}
@@ -681,12 +877,14 @@ module Optimizely
681
877
  experiment = config.get_experiment_from_key(experiment_key)
682
878
  return nil if experiment.nil?
683
879
 
880
+ experiment_id = experiment['id']
881
+
684
882
  return nil unless user_inputs_valid?(attributes)
685
883
 
686
- variation_id = @decision_service.get_variation(config, experiment_key, user_id, attributes)
884
+ variation_id, = @decision_service.get_variation(config, experiment_id, user_id, attributes)
687
885
  variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
688
886
  variation_key = variation['key'] if variation
689
- decision_notification_type = if config.feature_experiment?(experiment['id'])
887
+ decision_notification_type = if config.feature_experiment?(experiment_id)
690
888
  Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
691
889
  else
692
890
  Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
@@ -749,7 +947,7 @@ module Optimizely
749
947
  return nil
750
948
  end
751
949
 
752
- decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
950
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
753
951
  variation = decision ? decision['variation'] : nil
754
952
  feature_enabled = variation ? variation['featureEnabled'] : false
755
953
 
@@ -867,15 +1065,44 @@ module Optimizely
867
1065
  raise InvalidInputError, 'event_dispatcher'
868
1066
  end
869
1067
 
870
- def send_impression(config, experiment, variation_key, user_id, attributes = nil)
1068
+ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil)
1069
+ if experiment.nil?
1070
+ experiment = {
1071
+ 'id' => '',
1072
+ 'key' => '',
1073
+ 'layerId' => '',
1074
+ 'status' => '',
1075
+ 'variations' => [],
1076
+ 'trafficAllocation' => [],
1077
+ 'audienceIds' => [],
1078
+ 'audienceConditions' => [],
1079
+ 'forcedVariations' => {}
1080
+ }
1081
+ end
1082
+
1083
+ experiment_id = experiment['id']
871
1084
  experiment_key = experiment['key']
872
- variation_id = config.get_variation_id_from_key(experiment_key, variation_key)
873
- user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, user_id, attributes)
1085
+
1086
+ variation_id = ''
1087
+ variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) if experiment_id != ''
1088
+
1089
+ metadata = {
1090
+ flag_key: flag_key,
1091
+ rule_key: rule_key,
1092
+ rule_type: rule_type,
1093
+ variation_key: variation_key,
1094
+ enabled: enabled
1095
+ }
1096
+
1097
+ user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
874
1098
  @event_processor.process(user_event)
875
1099
  return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
876
1100
 
877
1101
  @logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
878
- variation = config.get_variation_from_id(experiment_key, variation_id)
1102
+
1103
+ experiment = nil if experiment_id == ''
1104
+ variation = nil
1105
+ variation = config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) unless experiment.nil?
879
1106
  log_event = EventFactory.create_log_event(user_event, @logger)
880
1107
  @notification_center.send_notifications(
881
1108
  NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
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.6.0
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-30 00:00:00.000000000 Z
11
+ date: 2021-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -141,6 +141,9 @@ files:
141
141
  - lib/optimizely/config_manager/project_config_manager.rb
142
142
  - lib/optimizely/config_manager/static_project_config_manager.rb
143
143
  - lib/optimizely/custom_attribute_condition_evaluator.rb
144
+ - lib/optimizely/decide/optimizely_decide_option.rb
145
+ - lib/optimizely/decide/optimizely_decision.rb
146
+ - lib/optimizely/decide/optimizely_decision_message.rb
144
147
  - lib/optimizely/decision_service.rb
145
148
  - lib/optimizely/error_handler.rb
146
149
  - lib/optimizely/event/batch_event_processor.rb
@@ -172,6 +175,7 @@ files:
172
175
  - lib/optimizely/notification_center.rb
173
176
  - lib/optimizely/optimizely_config.rb
174
177
  - lib/optimizely/optimizely_factory.rb
178
+ - lib/optimizely/optimizely_user_context.rb
175
179
  - lib/optimizely/params.rb
176
180
  - lib/optimizely/project_config.rb
177
181
  - lib/optimizely/semantic_version.rb
@@ -196,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
196
200
  - !ruby/object:Gem::Version
197
201
  version: '0'
198
202
  requirements: []
199
- rubygems_version: 3.0.3
203
+ rubygems_version: 3.0.1
200
204
  signing_key:
201
205
  specification_version: 4
202
206
  summary: Ruby SDK for Optimizely's testing framework