optimizely-sdk 3.4.0 → 3.8.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
  )
@@ -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}"))
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, Optimizely and contributors
4
+ # Copyright 2016-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.
@@ -81,14 +81,6 @@ module Optimizely
81
81
  end
82
82
  end
83
83
 
84
- class InvalidDatafileError < Error
85
- # Raised when a public method fails due to an invalid datafile
86
-
87
- def initialize(aborted_method)
88
- super("Provided datafile is in an invalid format. Aborting #{aborted_method}.")
89
- end
90
- end
91
-
92
84
  class InvalidDatafileVersionError < Error
93
85
  # Raised when a datafile with an unsupported version is provided
94
86
 
@@ -128,4 +120,20 @@ module Optimizely
128
120
  super("Optimizely instance is not valid. Failing '#{aborted_method}'.")
129
121
  end
130
122
  end
123
+
124
+ class InvalidAttributeType < Error
125
+ # Raised when an attribute is not provided in expected type.
126
+
127
+ def initialize(msg = 'Provided attribute value is not in the expected data type.')
128
+ super
129
+ end
130
+ end
131
+
132
+ class InvalidSemanticVersion < Error
133
+ # Raised when an invalid value is provided as semantic version.
134
+
135
+ def initialize(msg = 'Provided semantic version is invalid.')
136
+ super
137
+ end
138
+ end
131
139
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, Optimizely and contributors
4
+ # Copyright 2016-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.
@@ -304,7 +304,8 @@ module Optimizely
304
304
  'BOOLEAN' => 'boolean',
305
305
  'DOUBLE' => 'double',
306
306
  'INTEGER' => 'integer',
307
- 'STRING' => 'string'
307
+ 'STRING' => 'string',
308
+ 'JSON' => 'json'
308
309
  }.freeze
309
310
 
310
311
  INPUT_VARIABLES = {
@@ -334,11 +335,11 @@ module Optimizely
334
335
 
335
336
  AUDIENCE_EVALUATION_LOGS = {
336
337
  'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
337
- 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
338
338
  'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
339
- 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
340
339
  'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
341
340
  "for user attribute '%s' is not in the range [-2^53, +2^53].",
341
+ 'INVALID_SEMANTIC_VERSION' => 'Audience condition %s evaluated as UNKNOWN because an invalid semantic version ' \
342
+ "was passed for user attribute '%s'.",
342
343
  'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
343
344
  "was passed for user attribute '%s'.",
344
345
  'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
@@ -353,15 +354,28 @@ module Optimizely
353
354
  'to upgrade to a newer release of the Optimizely SDK.'
354
355
  }.freeze
355
356
 
357
+ EXPERIMENT_AUDIENCE_EVALUATION_LOGS = {
358
+ 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
359
+ 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s."
360
+ }.merge(AUDIENCE_EVALUATION_LOGS).freeze
361
+
362
+ ROLLOUT_AUDIENCE_EVALUATION_LOGS = {
363
+ 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for rule '%s' collectively evaluated to %s.",
364
+ 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for rule '%s': %s."
365
+ }.merge(AUDIENCE_EVALUATION_LOGS).freeze
366
+
356
367
  DECISION_NOTIFICATION_TYPES = {
357
368
  'AB_TEST' => 'ab-test',
358
369
  'FEATURE' => 'feature',
359
370
  'FEATURE_TEST' => 'feature-test',
360
- 'FEATURE_VARIABLE' => 'feature-variable'
371
+ 'FEATURE_VARIABLE' => 'feature-variable',
372
+ 'FLAG' => 'flag',
373
+ 'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
361
374
  }.freeze
362
375
 
363
376
  CONFIG_MANAGER = {
364
377
  'DATAFILE_URL_TEMPLATE' => 'https://cdn.optimizely.com/datafiles/%s.json',
378
+ 'AUTHENTICATED_DATAFILE_URL_TEMPLATE' => 'https://config.optimizely.com/datafiles/auth/%s.json',
365
379
  # Default time in seconds to block the 'config' method call until 'config' instance has been initialized.
366
380
  'DEFAULT_BLOCKING_TIMEOUT' => 15,
367
381
  # Default config update interval of 5 minutes
@@ -0,0 +1,64 @@
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 'net/http'
20
+
21
+ module Optimizely
22
+ module Helpers
23
+ module HttpUtils
24
+ module_function
25
+
26
+ def make_request(url, http_method, request_body = nil, headers = {}, read_timeout = nil, proxy_config = nil)
27
+ # makes http/https GET/POST request and returns response
28
+ #
29
+ uri = URI.parse(url)
30
+
31
+ if http_method == :get
32
+ request = Net::HTTP::Get.new(uri.request_uri)
33
+ elsif http_method == :post
34
+ request = Net::HTTP::Post.new(uri.request_uri)
35
+ request.body = request_body if request_body
36
+ else
37
+ return nil
38
+ end
39
+
40
+ # set headers
41
+ headers&.each do |key, val|
42
+ request[key] = val
43
+ end
44
+
45
+ # do not try to make request with proxy unless we have at least a host
46
+ http_class = if proxy_config&.host
47
+ Net::HTTP::Proxy(
48
+ proxy_config.host,
49
+ proxy_config.port,
50
+ proxy_config.username,
51
+ proxy_config.password
52
+ )
53
+ else
54
+ Net::HTTP
55
+ end
56
+
57
+ http = http_class.new(uri.host, uri.port)
58
+ http.read_timeout = read_timeout if read_timeout
59
+ http.use_ssl = uri.scheme == 'https'
60
+ http.request(request)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2017, 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.
@@ -48,6 +48,13 @@ module Optimizely
48
48
  logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
49
49
  "'#{variable_type}': #{e.message}.")
50
50
  end
51
+ when 'json'
52
+ begin
53
+ return_value = JSON.parse(value)
54
+ rescue => e
55
+ logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
56
+ "'#{variable_type}': #{e.message}.")
57
+ end
51
58
  else
52
59
  # default case is string
53
60
  return_value = value
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019, Optimizely and contributors
3
+ # Copyright 2019-2020, 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.
@@ -25,6 +25,7 @@ module Optimizely
25
25
  experiments_map_object = experiments_map
26
26
  features_map = get_features_map(experiments_map_object)
27
27
  {
28
+ 'datafile' => @project_config.datafile,
28
29
  'experimentsMap' => experiments_map_object,
29
30
  'featuresMap' => features_map,
30
31
  'revision' => @project_config.revision
@@ -17,16 +17,18 @@
17
17
  #
18
18
 
19
19
  require 'optimizely'
20
+ require 'optimizely/error_handler'
20
21
  require 'optimizely/event_dispatcher'
21
22
  require 'optimizely/event/batch_event_processor'
23
+ require 'optimizely/logger'
24
+ require 'optimizely/notification_center'
25
+
22
26
  module Optimizely
23
27
  class OptimizelyFactory
24
- attr_reader :max_event_batch_size, :max_event_flush_interval
25
-
26
28
  # Convenience method for setting the maximum number of events contained within a batch.
27
29
  # @param batch_size Integer - Sets size of EventQueue.
28
30
  # @param logger - Optional LoggerInterface Provides a log method to log messages.
29
- def self.max_event_batch_size(batch_size, logger)
31
+ def self.max_event_batch_size(batch_size, logger = NoOpLogger.new)
30
32
  unless batch_size.is_a? Integer
31
33
  logger.log(
32
34
  Logger::ERROR,
@@ -48,7 +50,7 @@ module Optimizely
48
50
  # Convenience method for setting the maximum time interval in milliseconds between event dispatches.
49
51
  # @param flush_interval Numeric - Time interval between event dispatches.
50
52
  # @param logger - Optional LoggerInterface Provides a log method to log messages.
51
- def self.max_event_flush_interval(flush_interval, logger)
53
+ def self.max_event_flush_interval(flush_interval, logger = NoOpLogger.new)
52
54
  unless flush_interval.is_a? Numeric
53
55
  logger.log(
54
56
  Logger::ERROR,
@@ -67,12 +69,42 @@ module Optimizely
67
69
  @max_event_flush_interval = flush_interval
68
70
  end
69
71
 
72
+ # Convenience method for setting frequency at which datafile has to be polled and ProjectConfig updated.
73
+ #
74
+ # @param polling_interval Numeric - Time in seconds after which to update datafile.
75
+ def self.polling_interval(polling_interval)
76
+ @polling_interval = polling_interval
77
+ end
78
+
79
+ # Convenience method for setting timeout to block the config call until config has been initialized.
80
+ #
81
+ # @param blocking_timeout Numeric - Time in seconds.
82
+ def self.blocking_timeout(blocking_timeout)
83
+ @blocking_timeout = blocking_timeout
84
+ end
85
+
70
86
  # Returns a new optimizely instance.
71
87
  #
72
88
  # @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
73
89
  # @param fallback datafile - Optional JSON string datafile.
74
90
  def self.default_instance(sdk_key, datafile = nil)
75
- Optimizely::Project.new(datafile, nil, nil, nil, nil, nil, sdk_key)
91
+ error_handler = NoOpErrorHandler.new
92
+ logger = NoOpLogger.new
93
+ notification_center = NotificationCenter.new(logger, error_handler)
94
+
95
+ config_manager = Optimizely::HTTPProjectConfigManager.new(
96
+ sdk_key: sdk_key,
97
+ polling_interval: @polling_interval,
98
+ blocking_timeout: @blocking_timeout,
99
+ datafile: datafile,
100
+ logger: logger,
101
+ error_handler: error_handler,
102
+ notification_center: notification_center
103
+ )
104
+
105
+ Optimizely::Project.new(
106
+ datafile, nil, logger, error_handler, nil, nil, sdk_key, config_manager, notification_center
107
+ )
76
108
  end
77
109
 
78
110
  # Returns a new optimizely instance.
@@ -108,10 +140,27 @@ module Optimizely
108
140
  config_manager = nil,
109
141
  notification_center = nil
110
142
  )
143
+
144
+ error_handler ||= NoOpErrorHandler.new
145
+ logger ||= NoOpLogger.new
146
+ notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
147
+
111
148
  event_processor = BatchEventProcessor.new(
112
149
  event_dispatcher: event_dispatcher || EventDispatcher.new,
113
150
  batch_size: @max_event_batch_size,
114
151
  flush_interval: @max_event_flush_interval,
152
+ logger: logger,
153
+ notification_center: notification_center
154
+ )
155
+
156
+ config_manager ||= Optimizely::HTTPProjectConfigManager.new(
157
+ sdk_key: sdk_key,
158
+ polling_interval: @polling_interval,
159
+ blocking_timeout: @blocking_timeout,
160
+ datafile: datafile,
161
+ logger: logger,
162
+ error_handler: error_handler,
163
+ skip_json_validation: skip_json_validation,
115
164
  notification_center: notification_center
116
165
  )
117
166