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