optimizely-sdk 3.3.2 → 3.7.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 2017-2019, Optimizely and contributors
4
+ # Copyright 2017-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.
@@ -40,6 +40,7 @@ module Optimizely
40
40
  Decision = Struct.new(:experiment, :variation, :source)
41
41
 
42
42
  DECISION_SOURCES = {
43
+ 'EXPERIMENT' => 'experiment',
43
44
  'FEATURE_TEST' => 'feature-test',
44
45
  'ROLLOUT' => 'rollout'
45
46
  }.freeze
@@ -94,7 +95,7 @@ module Optimizely
94
95
  end
95
96
 
96
97
  # Check audience conditions
97
- unless Audience.user_in_experiment?(project_config, experiment, attributes, @logger)
98
+ unless Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
98
99
  @logger.log(
99
100
  Logger::INFO,
100
101
  "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
@@ -106,6 +107,16 @@ module Optimizely
106
107
  variation = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
107
108
  variation_id = variation ? variation['id'] : nil
108
109
 
110
+ if variation_id
111
+ variation_key = variation['key']
112
+ @logger.log(
113
+ Logger::INFO,
114
+ "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
115
+ )
116
+ else
117
+ @logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
118
+ end
119
+
109
120
  # Persist bucketing decision
110
121
  save_user_profile(user_profile, experiment_id, variation_id)
111
122
  variation_id
@@ -125,21 +136,9 @@ module Optimizely
125
136
  decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes)
126
137
  return decision unless decision.nil?
127
138
 
128
- feature_flag_key = feature_flag['key']
129
139
  decision = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)
130
- if decision
131
- @logger.log(
132
- Logger::INFO,
133
- "User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
134
- )
135
- return decision
136
- end
137
- @logger.log(
138
- Logger::INFO,
139
- "User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
140
- )
141
140
 
142
- nil
141
+ decision
143
142
  end
144
143
 
145
144
  def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil)
@@ -178,10 +177,7 @@ module Optimizely
178
177
  next unless variation_id
179
178
 
180
179
  variation = project_config.variation_id_map[experiment_key][variation_id]
181
- @logger.log(
182
- Logger::INFO,
183
- "The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
184
- )
180
+
185
181
  return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'])
186
182
  end
187
183
 
@@ -231,20 +227,23 @@ module Optimizely
231
227
  # Go through each experiment in order and try to get the variation for the user
232
228
  number_of_rules.times do |index|
233
229
  rollout_rule = rollout_rules[index]
234
- audience_id = rollout_rule['audienceIds'][0]
235
- audience = project_config.get_audience_from_id(audience_id)
236
- audience_name = audience['name']
230
+ logging_key = index + 1
237
231
 
238
232
  # Check that user meets audience conditions for targeting rule
239
- unless Audience.user_in_experiment?(project_config, rollout_rule, attributes, @logger)
233
+ unless Audience.user_meets_audience_conditions?(project_config, rollout_rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
240
234
  @logger.log(
241
235
  Logger::DEBUG,
242
- "User '#{user_id}' does not meet the conditions to be in rollout rule for audience '#{audience_name}'."
236
+ "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
243
237
  )
244
238
  # move onto the next targeting rule
245
239
  next
246
240
  end
247
241
 
242
+ @logger.log(
243
+ Logger::DEBUG,
244
+ "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
245
+ )
246
+
248
247
  # Evaluate if user satisfies the traffic allocation for this rollout rule
249
248
  variation = @bucketer.bucket(project_config, rollout_rule, bucketing_id, user_id)
250
249
  return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
@@ -254,17 +253,20 @@ module Optimizely
254
253
 
255
254
  # get last rule which is the everyone else rule
256
255
  everyone_else_experiment = rollout_rules[number_of_rules]
256
+ logging_key = 'Everyone Else'
257
257
  # Check that user meets audience conditions for last rule
258
- unless Audience.user_in_experiment?(project_config, everyone_else_experiment, attributes, @logger)
259
- audience_id = everyone_else_experiment['audienceIds'][0]
260
- audience = project_config.get_audience_from_id(audience_id)
261
- audience_name = audience['name']
258
+ unless Audience.user_meets_audience_conditions?(project_config, everyone_else_experiment, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
262
259
  @logger.log(
263
260
  Logger::DEBUG,
264
- "User '#{user_id}' does not meet the conditions to be in rollout rule for audience '#{audience_name}'."
261
+ "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
265
262
  )
266
263
  return nil
267
264
  end
265
+
266
+ @logger.log(
267
+ Logger::DEBUG,
268
+ "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
269
+ )
268
270
  variation = @bucketer.bucket(project_config, everyone_else_experiment, bucketing_id, user_id)
269
271
  return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
270
272
 
@@ -31,7 +31,6 @@ module Optimizely
31
31
  DEFAULT_BATCH_INTERVAL = 30_000 # interval in milliseconds
32
32
  DEFAULT_QUEUE_CAPACITY = 1000
33
33
  DEFAULT_TIMEOUT_INTERVAL = 5 # interval in seconds
34
- MAX_NIL_COUNT = 3
35
34
 
36
35
  FLUSH_SIGNAL = 'FLUSH_SIGNAL'
37
36
  SHUTDOWN_SIGNAL = 'SHUTDOWN_SIGNAL'
@@ -62,7 +61,7 @@ module Optimizely
62
61
  @notification_center = notification_center
63
62
  @current_batch = []
64
63
  @started = false
65
- start!
64
+ @stopped = false
66
65
  end
67
66
 
68
67
  def start!
@@ -72,26 +71,37 @@ module Optimizely
72
71
  end
73
72
  @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
74
73
  @logger.log(Logger::INFO, 'Starting scheduler.')
75
- @thread = Thread.new { run }
74
+ if @wait_mutex.nil?
75
+ @wait_mutex = Mutex.new
76
+ @resource = ConditionVariable.new
77
+ end
78
+ @thread = Thread.new { run_queue }
76
79
  @started = true
80
+ @stopped = false
77
81
  end
78
82
 
79
83
  def flush
80
84
  @event_queue << FLUSH_SIGNAL
85
+ @wait_mutex.synchronize { @resource.signal }
81
86
  end
82
87
 
83
88
  def process(user_event)
84
89
  @logger.log(Logger::DEBUG, "Received userEvent: #{user_event}")
85
90
 
86
- if !@started || !@thread.alive?
91
+ # if the processor has been explicitly stopped. Don't accept tasks
92
+ if @stopped
87
93
  @logger.log(Logger::WARN, 'Executor shutdown, not accepting tasks.')
88
94
  return
89
95
  end
90
96
 
97
+ # start if the processor hasn't been started
98
+ start! unless @started
99
+
91
100
  begin
92
101
  @event_queue.push(user_event, true)
93
- rescue Exception
94
- @logger.log(Logger::WARN, 'Payload not accepted by the queue.')
102
+ @wait_mutex.synchronize { @resource.signal }
103
+ rescue => e
104
+ @logger.log(Logger::WARN, 'Payload not accepted by the queue: ' + e.message)
95
105
  return
96
106
  end
97
107
  end
@@ -101,42 +111,20 @@ module Optimizely
101
111
 
102
112
  @logger.log(Logger::INFO, 'Stopping scheduler.')
103
113
  @event_queue << SHUTDOWN_SIGNAL
114
+ @wait_mutex.synchronize { @resource.signal }
104
115
  @thread.join(DEFAULT_TIMEOUT_INTERVAL)
105
116
  @started = false
117
+ @stopped = true
106
118
  end
107
119
 
108
120
  private
109
121
 
110
- def run
111
- # if we receive a number of item nils that reach MAX_NIL_COUNT,
112
- # then we hang on the pop via setting use_pop to false
113
- @nil_count = 0
114
- # hang on pop if true
115
- @use_pop = false
116
- loop do
117
- if Helpers::DateTimeUtils.create_timestamp >= @flushing_interval_deadline
118
- @logger.log(Logger::DEBUG, 'Deadline exceeded flushing current batch.')
119
- flush_queue!
120
- @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
121
- @use_pop = true if @nil_count > MAX_NIL_COUNT
122
- end
123
-
124
- item = @event_queue.pop if @event_queue.length.positive? || @use_pop
125
-
126
- if item.nil?
127
- # when nil count is greater than MAX_NIL_COUNT, we hang on the pop until there is an item available.
128
- # this avoids to much spinning of the loop.
129
- @nil_count += 1
130
- next
131
- end
132
-
133
- # reset nil_count and use_pop if we have received an item.
134
- @nil_count = 0
135
- @use_pop = false
136
-
122
+ def process_queue
123
+ while @event_queue.length.positive?
124
+ item = @event_queue.pop
137
125
  if item == SHUTDOWN_SIGNAL
138
126
  @logger.log(Logger::DEBUG, 'Received shutdown signal.')
139
- break
127
+ return false
140
128
  end
141
129
 
142
130
  if item == FLUSH_SIGNAL
@@ -147,15 +135,35 @@ module Optimizely
147
135
 
148
136
  add_to_batch(item) if item.is_a? Optimizely::UserEvent
149
137
  end
138
+ true
139
+ end
140
+
141
+ def run_queue
142
+ loop do
143
+ if Helpers::DateTimeUtils.create_timestamp >= @flushing_interval_deadline
144
+ @logger.log(Logger::DEBUG, 'Deadline exceeded flushing current batch.')
145
+
146
+ break unless process_queue
147
+
148
+ flush_queue!
149
+ @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
150
+ end
151
+
152
+ break unless process_queue
153
+
154
+ # what is the current interval to flush in seconds
155
+ interval = (@flushing_interval_deadline - Helpers::DateTimeUtils.create_timestamp) * 0.001
156
+
157
+ next unless interval.positive?
158
+
159
+ @wait_mutex.synchronize { @resource.wait(@wait_mutex, interval) }
160
+ end
150
161
  rescue SignalException
151
162
  @logger.log(Logger::ERROR, 'Interrupted while processing buffer.')
152
- rescue Exception => e
163
+ rescue => e
153
164
  @logger.log(Logger::ERROR, "Uncaught exception processing buffer. #{e.message}")
154
165
  ensure
155
- @logger.log(
156
- Logger::INFO,
157
- 'Exiting processing loop. Attempting to flush pending events.'
158
- )
166
+ @logger.log(Logger::INFO, 'Exiting processing loop. Attempting to flush pending events.')
159
167
  flush_queue!
160
168
  end
161
169
 
@@ -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
  )
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2017, 2019, Optimizely and contributors
4
+ # Copyright 2016-2017, 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.
@@ -16,8 +16,7 @@
16
16
  # limitations under the License.
17
17
  #
18
18
  require_relative 'exceptions'
19
-
20
- require 'httparty'
19
+ require_relative 'helpers/http_utils'
21
20
 
22
21
  module Optimizely
23
22
  class NoOpEventDispatcher
@@ -30,28 +29,23 @@ module Optimizely
30
29
  # @api constants
31
30
  REQUEST_TIMEOUT = 10
32
31
 
33
- def initialize(logger: nil, error_handler: nil)
32
+ def initialize(logger: nil, error_handler: nil, proxy_config: nil)
34
33
  @logger = logger || NoOpLogger.new
35
34
  @error_handler = error_handler || NoOpErrorHandler.new
35
+ @proxy_config = proxy_config
36
36
  end
37
37
 
38
38
  # Dispatch the event being represented by the Event object.
39
39
  #
40
40
  # @param event - Event object
41
41
  def dispatch_event(event)
42
- if event.http_verb == :get
43
- response = HTTParty.get(event.url, headers: event.headers, query: event.params, timeout: REQUEST_TIMEOUT)
44
-
45
- elsif event.http_verb == :post
46
- response = HTTParty.post(event.url,
47
- body: event.params.to_json,
48
- headers: event.headers,
49
- timeout: REQUEST_TIMEOUT)
50
- end
42
+ response = Helpers::HttpUtils.make_request(
43
+ event.url, event.http_verb, event.params.to_json, event.headers, REQUEST_TIMEOUT, @proxy_config
44
+ )
51
45
 
52
46
  error_msg = "Event failed to dispatch with response code: #{response.code}"
53
47
 
54
- case response.code
48
+ case response.code.to_i
55
49
  when 400...500
56
50
  @logger.log(Logger::ERROR, error_msg)
57
51
  @error_handler.handle_error(HTTPCallError.new("HTTP Client Error: #{response.code}"))