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.
- checksums.yaml +5 -5
- data/lib/optimizely.rb +248 -46
- data/lib/optimizely/audience.rb +22 -13
- data/lib/optimizely/bucketer.rb +3 -8
- data/lib/optimizely/config/datafile_project_config.rb +19 -3
- data/lib/optimizely/config/proxy_config.rb +34 -0
- data/lib/optimizely/config_manager/async_scheduler.rb +6 -2
- data/lib/optimizely/config_manager/http_project_config_manager.rb +45 -25
- data/lib/optimizely/config_manager/static_project_config_manager.rb +6 -2
- data/lib/optimizely/custom_attribute_condition_evaluator.rb +133 -37
- data/lib/optimizely/decision_service.rb +31 -29
- data/lib/optimizely/event/batch_event_processor.rb +47 -39
- data/lib/optimizely/event/entity/decision.rb +6 -4
- data/lib/optimizely/event/entity/impression_event.rb +4 -2
- data/lib/optimizely/event/event_factory.rb +4 -3
- data/lib/optimizely/event/user_event_factory.rb +4 -3
- data/lib/optimizely/event_dispatcher.rb +8 -14
- data/lib/optimizely/exceptions.rb +17 -9
- data/lib/optimizely/helpers/constants.rb +18 -5
- data/lib/optimizely/helpers/http_utils.rb +64 -0
- data/lib/optimizely/helpers/variable_type.rb +8 -1
- data/lib/optimizely/optimizely_config.rb +117 -0
- data/lib/optimizely/optimizely_factory.rb +54 -5
- data/lib/optimizely/project_config.rb +5 -1
- data/lib/optimizely/semantic_version.rb +166 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +7 -18
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2017-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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.
|
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
|
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
|
-
|
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
|
-
@
|
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
|
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
|
-
|
94
|
-
|
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
|
111
|
-
|
112
|
-
|
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
|
-
|
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
|
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 =
|
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 =
|
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
|
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
|
-
|
43
|
-
|
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}"))
|