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 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.
@@ -39,7 +39,7 @@ module Optimizely
39
39
  # Determines ID of variation to be shown for a given experiment key and user ID.
40
40
  #
41
41
  # project_config - Instance of ProjectConfig
42
- # experiment - Experiment for which visitor is to be bucketed.
42
+ # experiment - Experiment or Rollout rule for which visitor is to be bucketed.
43
43
  # bucketing_id - String A customer-assigned value used to generate the bucketing key
44
44
  # user_id - String ID for user.
45
45
  #
@@ -47,6 +47,7 @@ module Optimizely
47
47
  return nil if experiment.nil?
48
48
 
49
49
  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
50
+ # this will not affect evaluation of rollout rules.
50
51
  experiment_id = experiment['id']
51
52
  experiment_key = experiment['key']
52
53
  group_id = experiment['groupId']
@@ -82,11 +83,6 @@ module Optimizely
82
83
  variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
83
84
  if variation_id && variation_id != ''
84
85
  variation = project_config.get_variation_from_id(experiment_key, variation_id)
85
- variation_key = variation ? variation['key'] : nil
86
- @logger.log(
87
- Logger::INFO,
88
- "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
89
- )
90
86
  return variation
91
87
  end
92
88
 
@@ -98,7 +94,6 @@ module Optimizely
98
94
  )
99
95
  end
100
96
 
101
- @logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
102
97
  nil
103
98
  end
104
99
 
@@ -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.
@@ -24,6 +24,7 @@ module Optimizely
24
24
  RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
25
25
  RESERVED_ATTRIBUTE_PREFIX = '$opt_'
26
26
 
27
+ attr_reader :datafile
27
28
  attr_reader :account_id
28
29
  attr_reader :attributes
29
30
  attr_reader :audiences
@@ -39,6 +40,7 @@ module Optimizely
39
40
  attr_reader :revision
40
41
  attr_reader :rollouts
41
42
  attr_reader :version
43
+ attr_reader :send_flag_decisions
42
44
 
43
45
  attr_reader :attribute_key_map
44
46
  attr_reader :audience_id_map
@@ -62,6 +64,7 @@ module Optimizely
62
64
 
63
65
  config = JSON.parse(datafile)
64
66
 
67
+ @datafile = datafile
65
68
  @error_handler = error_handler
66
69
  @logger = logger
67
70
  @version = config['version']
@@ -81,6 +84,18 @@ module Optimizely
81
84
  @bot_filtering = config['botFiltering']
82
85
  @revision = config['revision']
83
86
  @rollouts = config.fetch('rollouts', [])
87
+ @send_flag_decisions = config.fetch('sendFlagDecisions', false)
88
+
89
+ # Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
90
+ # Converting it to a first-class json type while creating Project Config
91
+ @feature_flags.each do |feature_flag|
92
+ feature_flag['variables'].each do |variable|
93
+ if variable['type'] == 'string' && variable['subType'] == 'json'
94
+ variable['type'] = 'json'
95
+ variable.delete('subType')
96
+ end
97
+ end
98
+ end
84
99
 
85
100
  # Utility maps for quick lookup
86
101
  @attribute_key_map = generate_key_map(@attributes, 'key')
@@ -159,8 +174,9 @@ module Optimizely
159
174
  config = new(datafile, logger, error_handler)
160
175
  rescue StandardError => e
161
176
  default_logger = SimpleLogger.new
162
- error_msg = e.class == InvalidDatafileVersionError ? e.message : InvalidInputError.new('datafile').message
163
- error_to_handle = e.class == InvalidDatafileVersionError ? InvalidDatafileVersionError : InvalidInputError
177
+ error_to_handle = e.class == InvalidDatafileVersionError ? e : InvalidInputError.new('datafile')
178
+ error_msg = error_to_handle.message
179
+
164
180
  default_logger.log(Logger::ERROR, error_msg)
165
181
  error_handler.handle_error error_to_handle
166
182
  return nil
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020, Optimizely and contributors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ #
18
+
19
+ module Optimizely
20
+ class ProxyConfig
21
+ attr_reader :host, :port, :username, :password
22
+
23
+ def initialize(host, port = nil, username = nil, password = nil)
24
+ # host - DNS name or IP address of proxy
25
+ # port - port to use to acess the proxy
26
+ # username - username if authorization is required
27
+ # password - password if authorization is required
28
+ @host = host
29
+ @port = port
30
+ @username = username
31
+ @password = password
32
+ end
33
+ end
34
+ 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,13 +19,14 @@ module Optimizely
19
19
  class AsyncScheduler
20
20
  attr_reader :running
21
21
 
22
- def initialize(callback, interval, auto_update, logger = nil)
22
+ def initialize(callback, interval, auto_update, logger = nil, error_handler = nil)
23
23
  # Sets up AsyncScheduler to execute a callback periodically.
24
24
  #
25
25
  # callback - Main function to be executed periodically.
26
26
  # interval - How many seconds to wait between executions.
27
27
  # auto_update - boolean indicates to run infinitely or only once.
28
28
  # logger - Optional Provides a logger instance.
29
+ # error_handler - Optional Provides a handle_error method to handle exceptions.
29
30
 
30
31
  @callback = callback
31
32
  @interval = interval
@@ -33,6 +34,7 @@ module Optimizely
33
34
  @running = false
34
35
  @thread = nil
35
36
  @logger = logger || NoOpLogger.new
37
+ @error_handler = error_handler || NoOpErrorHandler.new
36
38
  end
37
39
 
38
40
  def start!
@@ -54,6 +56,7 @@ module Optimizely
54
56
  Logger::ERROR,
55
57
  "Couldn't create a new thread for async scheduler. #{e.message}"
56
58
  )
59
+ @error_handler.handle_error(e)
57
60
  end
58
61
  end
59
62
 
@@ -80,6 +83,7 @@ module Optimizely
80
83
  Logger::ERROR,
81
84
  "Something went wrong when executing passed callback. #{e.message}"
82
85
  )
86
+ @error_handler.handle_error(e)
83
87
  stop!
84
88
  end
85
89
  break unless @auto_update
@@ -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,18 +19,21 @@ require_relative '../config/datafile_project_config'
19
19
  require_relative '../error_handler'
20
20
  require_relative '../exceptions'
21
21
  require_relative '../helpers/constants'
22
+ require_relative '../helpers/http_utils'
22
23
  require_relative '../logger'
23
24
  require_relative '../notification_center'
24
25
  require_relative '../project_config'
26
+ require_relative '../optimizely_config'
25
27
  require_relative 'project_config_manager'
26
28
  require_relative 'async_scheduler'
27
- require 'httparty'
29
+
28
30
  require 'json'
31
+
29
32
  module Optimizely
30
33
  class HTTPProjectConfigManager < ProjectConfigManager
31
34
  # Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
32
35
 
33
- attr_reader :stopped
36
+ attr_reader :stopped, :optimizely_config
34
37
 
35
38
  # Initialize config manager. One of sdk_key or url has to be set to be able to use.
36
39
  #
@@ -48,6 +51,8 @@ module Optimizely
48
51
  # error_handler - Provides a handle_error method to handle exceptions.
49
52
  # skip_json_validation - Optional boolean param which allows skipping JSON schema
50
53
  # validation upon object invocation. By default JSON schema validation will be performed.
54
+ # datafile_access_token - access token used to fetch private datafiles
55
+ # proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.
51
56
  def initialize(
52
57
  sdk_key: nil,
53
58
  url: nil,
@@ -60,10 +65,13 @@ module Optimizely
60
65
  logger: nil,
61
66
  error_handler: nil,
62
67
  skip_json_validation: false,
63
- notification_center: nil
68
+ notification_center: nil,
69
+ datafile_access_token: nil,
70
+ proxy_config: nil
64
71
  )
65
72
  @logger = logger || NoOpLogger.new
66
73
  @error_handler = error_handler || NoOpErrorHandler.new
74
+ @access_token = datafile_access_token
67
75
  @datafile_url = get_datafile_url(sdk_key, url, url_template)
68
76
  @polling_interval = nil
69
77
  polling_interval(polling_interval)
@@ -73,12 +81,14 @@ module Optimizely
73
81
  @skip_json_validation = skip_json_validation
74
82
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
75
83
  @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
84
+ @optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
76
85
  @mutex = Mutex.new
77
86
  @resource = ConditionVariable.new
78
87
  @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
79
88
  # Start async scheduler in the end to avoid race condition where scheduler executes
80
89
  # callback which makes use of variables not yet initialized by the main thread.
81
90
  @async_scheduler.start! if start_by_default == true
91
+ @proxy_config = proxy_config
82
92
  @stopped = false
83
93
  end
84
94
 
@@ -141,21 +151,20 @@ module Optimizely
141
151
  end
142
152
 
143
153
  def request_config
144
- @logger.log(
145
- Logger::DEBUG,
146
- "Fetching datafile from #{@datafile_url}"
147
- )
148
- begin
149
- headers = {
150
- 'Content-Type' => 'application/json'
151
- }
154
+ @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
155
+ headers = {}
156
+ headers['Content-Type'] = 'application/json'
157
+ headers['If-Modified-Since'] = @last_modified if @last_modified
158
+ headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
152
159
 
153
- headers[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']] = @last_modified if @last_modified
160
+ # Cleaning headers before logging to avoid exposing authorization token
161
+ cleansed_headers = {}
162
+ headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
163
+ @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
154
164
 
155
- response = HTTParty.get(
156
- @datafile_url,
157
- headers: headers,
158
- timeout: Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT']
165
+ begin
166
+ response = Helpers::HttpUtils.make_request(
167
+ @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
159
168
  )
160
169
  rescue StandardError => e
161
170
  @logger.log(
@@ -165,6 +174,9 @@ module Optimizely
165
174
  return nil
166
175
  end
167
176
 
177
+ response_code = response.code.to_i
178
+ @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
179
+
168
180
  # Leave datafile and config unchanged if it has not been modified.
169
181
  if response.code == '304'
170
182
  @logger.log(
@@ -174,9 +186,14 @@ module Optimizely
174
186
  return
175
187
  end
176
188
 
177
- @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
178
-
179
- config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation) if response.body
189
+ if response_code >= 200 && response_code < 400
190
+ @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
191
+ config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
192
+ @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
193
+ @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
194
+ else
195
+ @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
196
+ end
180
197
 
181
198
  config
182
199
  end
@@ -192,6 +209,7 @@ module Optimizely
192
209
  end
193
210
 
194
211
  @config = config
212
+ @optimizely_config = OptimizelyConfig.new(config).config
195
213
 
196
214
  @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
197
215
 
@@ -281,17 +299,19 @@ module Optimizely
281
299
  # SDK key to determine URL from which to fetch the datafile.
282
300
  # Returns String representing URL to fetch datafile from.
283
301
  if sdk_key.nil? && url.nil?
284
- @logger.log(Logger::ERROR, 'Must provide at least one of sdk_key or url.')
285
- @error_handler.handle_error(InvalidInputsError)
302
+ error_msg = 'Must provide at least one of sdk_key or url.'
303
+ @logger.log(Logger::ERROR, error_msg)
304
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
286
305
  end
287
306
 
288
307
  unless url
289
- url_template ||= Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE']
308
+ url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
290
309
  begin
291
310
  return (url_template % sdk_key)
292
311
  rescue
293
- @logger.log(Logger::ERROR, "Invalid url_template #{url_template} provided.")
294
- @error_handler.handle_error(InvalidInputsError)
312
+ error_msg = "Invalid url_template #{url_template} provided."
313
+ @logger.log(Logger::ERROR, error_msg)
314
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
295
315
  end
296
316
  end
297
317
 
@@ -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,11 +17,13 @@
17
17
  #
18
18
 
19
19
  require_relative '../config/datafile_project_config'
20
+ require_relative '../optimizely_config'
20
21
  require_relative 'project_config_manager'
22
+
21
23
  module Optimizely
22
24
  class StaticProjectConfigManager < ProjectConfigManager
23
25
  # Implementation of ProjectConfigManager interface.
24
- attr_reader :config
26
+ attr_reader :config, :optimizely_config
25
27
 
26
28
  def initialize(datafile, logger, error_handler, skip_json_validation)
27
29
  # Looks up and sets datafile and config based on response body.
@@ -38,6 +40,8 @@ module Optimizely
38
40
  error_handler,
39
41
  skip_json_validation
40
42
  )
43
+
44
+ @optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
41
45
  end
42
46
  end
43
47
  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.
@@ -15,8 +15,10 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require_relative 'exceptions'
18
19
  require_relative 'helpers/constants'
19
20
  require_relative 'helpers/validator'
21
+ require_relative 'semantic_version'
20
22
 
21
23
  module Optimizely
22
24
  class CustomAttributeConditionEvaluator
@@ -26,15 +28,29 @@ module Optimizely
26
28
  EXACT_MATCH_TYPE = 'exact'
27
29
  EXISTS_MATCH_TYPE = 'exists'
28
30
  GREATER_THAN_MATCH_TYPE = 'gt'
31
+ GREATER_EQUAL_MATCH_TYPE = 'ge'
29
32
  LESS_THAN_MATCH_TYPE = 'lt'
33
+ LESS_EQUAL_MATCH_TYPE = 'le'
30
34
  SUBSTRING_MATCH_TYPE = 'substring'
35
+ SEMVER_EQ = 'semver_eq'
36
+ SEMVER_GE = 'semver_ge'
37
+ SEMVER_GT = 'semver_gt'
38
+ SEMVER_LE = 'semver_le'
39
+ SEMVER_LT = 'semver_lt'
31
40
 
32
41
  EVALUATORS_BY_MATCH_TYPE = {
33
42
  EXACT_MATCH_TYPE => :exact_evaluator,
34
43
  EXISTS_MATCH_TYPE => :exists_evaluator,
35
44
  GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
45
+ GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
36
46
  LESS_THAN_MATCH_TYPE => :less_than_evaluator,
37
- SUBSTRING_MATCH_TYPE => :substring_evaluator
47
+ LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
48
+ SUBSTRING_MATCH_TYPE => :substring_evaluator,
49
+ SEMVER_EQ => :semver_equal_evaluator,
50
+ SEMVER_GE => :semver_greater_than_or_equal_evaluator,
51
+ SEMVER_GT => :semver_greater_than_evaluator,
52
+ SEMVER_LE => :semver_less_than_or_equal_evaluator,
53
+ SEMVER_LT => :semver_less_than_evaluator
38
54
  }.freeze
39
55
 
40
56
  attr_reader :user_attributes
@@ -95,7 +111,35 @@ module Optimizely
95
111
  return nil
96
112
  end
97
113
 
98
- send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
114
+ begin
115
+ send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
116
+ rescue InvalidAttributeType
117
+ condition_name = leaf_condition['name']
118
+ user_value = @user_attributes[condition_name]
119
+
120
+ @logger.log(
121
+ Logger::WARN,
122
+ format(
123
+ Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
124
+ leaf_condition,
125
+ user_value.class,
126
+ condition_name
127
+ )
128
+ )
129
+ return nil
130
+ rescue InvalidSemanticVersion
131
+ condition_name = leaf_condition['name']
132
+
133
+ @logger.log(
134
+ Logger::WARN,
135
+ format(
136
+ Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
137
+ leaf_condition,
138
+ condition_name
139
+ )
140
+ )
141
+ return nil
142
+ end
99
143
  end
100
144
 
101
145
  def exact_evaluator(condition)
@@ -122,16 +166,7 @@ module Optimizely
122
166
 
123
167
  if !value_type_valid_for_exact_conditions?(user_provided_value) ||
124
168
  !Helpers::Validator.same_types?(condition_value, user_provided_value)
125
- @logger.log(
126
- Logger::WARN,
127
- format(
128
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
129
- condition,
130
- user_provided_value.class,
131
- condition['name']
132
- )
133
- )
134
- return nil
169
+ raise InvalidAttributeType
135
170
  end
136
171
 
137
172
  if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
@@ -173,6 +208,20 @@ module Optimizely
173
208
  user_provided_value > condition_value
174
209
  end
175
210
 
211
+ def greater_than_or_equal_evaluator(condition)
212
+ # Evaluate the given greater than or equal match condition for the given user attributes.
213
+ # Returns boolean true if the user attribute value is greater than or equal to the condition value,
214
+ # false if the user attribute value is less than the condition value,
215
+ # nil if the condition value isn't a number or the user attribute value isn't a number.
216
+
217
+ condition_value = condition['value']
218
+ user_provided_value = @user_attributes[condition['name']]
219
+
220
+ return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
221
+
222
+ user_provided_value >= condition_value
223
+ end
224
+
176
225
  def less_than_evaluator(condition)
177
226
  # Evaluate the given less than match condition for the given user attributes.
178
227
  # Returns boolean true if the user attribute value is less than the condition value,
@@ -187,6 +236,20 @@ module Optimizely
187
236
  user_provided_value < condition_value
188
237
  end
189
238
 
239
+ def less_than_or_equal_evaluator(condition)
240
+ # Evaluate the given less than or equal match condition for the given user attributes.
241
+ # Returns boolean true if the user attribute value is less than or equal to the condition value,
242
+ # false if the user attribute value is greater than the condition value,
243
+ # nil if the condition value isn't a number or the user attribute value isn't a number.
244
+
245
+ condition_value = condition['value']
246
+ user_provided_value = @user_attributes[condition['name']]
247
+
248
+ return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
249
+
250
+ user_provided_value <= condition_value
251
+ end
252
+
190
253
  def substring_evaluator(condition)
191
254
  # Evaluate the given substring match condition for the given user attributes.
192
255
  # Returns boolean true if the condition value is a substring of the user attribute value,
@@ -204,22 +267,66 @@ module Optimizely
204
267
  return nil
205
268
  end
206
269
 
207
- unless user_provided_value.is_a?(String)
208
- @logger.log(
209
- Logger::WARN,
210
- format(
211
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
212
- condition,
213
- user_provided_value.class,
214
- condition['name']
215
- )
216
- )
217
- return nil
218
- end
270
+ raise InvalidAttributeType unless user_provided_value.is_a?(String)
219
271
 
220
272
  user_provided_value.include? condition_value
221
273
  end
222
274
 
275
+ def semver_equal_evaluator(condition)
276
+ # Evaluate the given semantic version equal match target version for the user version.
277
+ # Returns boolean true if the user version is equal to the target version,
278
+ # false if the user version is not equal to the target version
279
+
280
+ target_version = condition['value']
281
+ user_version = @user_attributes[condition['name']]
282
+
283
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
284
+ end
285
+
286
+ def semver_greater_than_evaluator(condition)
287
+ # Evaluate the given semantic version greater than match target version for the user version.
288
+ # Returns boolean true if the user version is greater than the target version,
289
+ # false if the user version is less than or equal to the target version
290
+
291
+ target_version = condition['value']
292
+ user_version = @user_attributes[condition['name']]
293
+
294
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
295
+ end
296
+
297
+ def semver_greater_than_or_equal_evaluator(condition)
298
+ # Evaluate the given semantic version greater than or equal to match target version for the user version.
299
+ # Returns boolean true if the user version is greater than or equal to the target version,
300
+ # false if the user version is less than the target version
301
+
302
+ target_version = condition['value']
303
+ user_version = @user_attributes[condition['name']]
304
+
305
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
306
+ end
307
+
308
+ def semver_less_than_evaluator(condition)
309
+ # Evaluate the given semantic version less than match target version for the user version.
310
+ # Returns boolean true if the user version is less than the target version,
311
+ # false if the user version is greater than or equal to the target version
312
+
313
+ target_version = condition['value']
314
+ user_version = @user_attributes[condition['name']]
315
+
316
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
317
+ end
318
+
319
+ def semver_less_than_or_equal_evaluator(condition)
320
+ # Evaluate the given semantic version less than or equal to match target version for the user version.
321
+ # Returns boolean true if the user version is less than or equal to the target version,
322
+ # false if the user version is greater than the target version
323
+
324
+ target_version = condition['value']
325
+ user_version = @user_attributes[condition['name']]
326
+
327
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
328
+ end
329
+
223
330
  private
224
331
 
225
332
  def valid_numeric_values?(user_value, condition_value, condition)
@@ -234,18 +341,7 @@ module Optimizely
234
341
  return false
235
342
  end
236
343
 
237
- unless user_value.is_a?(Numeric)
238
- @logger.log(
239
- Logger::WARN,
240
- format(
241
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
242
- condition,
243
- user_value.class,
244
- condition['name']
245
- )
246
- )
247
- return false
248
- end
344
+ raise InvalidAttributeType unless user_value.is_a?(Numeric)
249
345
 
250
346
  unless Helpers::Validator.finite_number?(user_value)
251
347
  @logger.log(