optimizely-sdk 3.3.2 → 3.7.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 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}"))