analytics-ruby 2.2.3.pre → 2.2.4.pre
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/History.md +16 -0
- data/Makefile +17 -8
- data/README.md +2 -2
- data/RELEASING.md +2 -3
- data/Rakefile +17 -1
- data/analytics-ruby.gemspec +10 -2
- data/codecov.yml +2 -0
- data/lib/analytics-ruby.rb +1 -0
- data/lib/segment/analytics.rb +9 -2
- data/lib/segment/analytics/backoff_policy.rb +49 -0
- data/lib/segment/analytics/client.rb +148 -99
- data/lib/segment/analytics/defaults.rb +20 -4
- data/lib/segment/analytics/logging.rb +2 -4
- data/lib/segment/analytics/message.rb +26 -0
- data/lib/segment/analytics/message_batch.rb +58 -0
- data/lib/segment/analytics/request.rb +84 -32
- data/lib/segment/analytics/response.rb +0 -1
- data/lib/segment/analytics/utils.rb +19 -16
- data/lib/segment/analytics/version.rb +1 -1
- data/lib/segment/analytics/worker.rb +11 -10
- data/spec/helpers/runscope_client.rb +38 -0
- data/spec/segment/analytics/backoff_policy_spec.rb +92 -0
- data/spec/segment/analytics/client_spec.rb +61 -44
- data/spec/segment/analytics/e2e_spec.rb +48 -0
- data/spec/segment/analytics/message_batch_spec.rb +49 -0
- data/spec/segment/analytics/message_spec.rb +35 -0
- data/spec/segment/analytics/request_spec.rb +87 -34
- data/spec/segment/analytics/worker_spec.rb +24 -16
- data/spec/spec_helper.rb +32 -6
- metadata +73 -17
- data/Gemfile.lock +0 -43
- data/analytics-ruby-2.0.13.gem +0 -0
- data/analytics-ruby-2.1.0.gem +0 -0
- data/analytics-ruby-2.2.2.gem +0 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Segment
|
4
|
+
class Analytics
|
5
|
+
describe BackoffPolicy do
|
6
|
+
describe '#initialize' do
|
7
|
+
context 'no options are given' do
|
8
|
+
it 'sets default min_timeout_ms' do
|
9
|
+
actual = subject.instance_variable_get(:@min_timeout_ms)
|
10
|
+
expect(actual).to eq(described_class::MIN_TIMEOUT_MS)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'sets default max_timeout_ms' do
|
14
|
+
actual = subject.instance_variable_get(:@max_timeout_ms)
|
15
|
+
expect(actual).to eq(described_class::MAX_TIMEOUT_MS)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'sets default multiplier' do
|
19
|
+
actual = subject.instance_variable_get(:@multiplier)
|
20
|
+
expect(actual).to eq(described_class::MULTIPLIER)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'sets default randomization factor' do
|
24
|
+
actual = subject.instance_variable_get(:@randomization_factor)
|
25
|
+
expect(actual).to eq(described_class::RANDOMIZATION_FACTOR)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'options are given' do
|
30
|
+
let(:min_timeout_ms) { 1234 }
|
31
|
+
let(:max_timeout_ms) { 5678 }
|
32
|
+
let(:multiplier) { 24 }
|
33
|
+
let(:randomization_factor) { 0.4 }
|
34
|
+
|
35
|
+
let(:options) do
|
36
|
+
{
|
37
|
+
min_timeout_ms: min_timeout_ms,
|
38
|
+
max_timeout_ms: max_timeout_ms,
|
39
|
+
multiplier: multiplier,
|
40
|
+
randomization_factor: randomization_factor
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
subject { described_class.new(options) }
|
45
|
+
|
46
|
+
it 'sets passed in min_timeout_ms' do
|
47
|
+
actual = subject.instance_variable_get(:@min_timeout_ms)
|
48
|
+
expect(actual).to eq(min_timeout_ms)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sets passed in max_timeout_ms' do
|
52
|
+
actual = subject.instance_variable_get(:@max_timeout_ms)
|
53
|
+
expect(actual).to eq(max_timeout_ms)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'sets passed in multiplier' do
|
57
|
+
actual = subject.instance_variable_get(:@multiplier)
|
58
|
+
expect(actual).to eq(multiplier)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'sets passed in randomization_factor' do
|
62
|
+
actual = subject.instance_variable_get(:@randomization_factor)
|
63
|
+
expect(actual).to eq(randomization_factor)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#next_interval' do
|
69
|
+
subject {
|
70
|
+
described_class.new(
|
71
|
+
min_timeout_ms: 1000,
|
72
|
+
max_timeout_ms: 10000,
|
73
|
+
multiplier: 2,
|
74
|
+
randomization_factor: 0.5
|
75
|
+
)
|
76
|
+
}
|
77
|
+
|
78
|
+
it 'returns exponentially increasing durations' do
|
79
|
+
expect(subject.next_interval).to be_within(500).of(1000)
|
80
|
+
expect(subject.next_interval).to be_within(1000).of(2000)
|
81
|
+
expect(subject.next_interval).to be_within(2000).of(4000)
|
82
|
+
expect(subject.next_interval).to be_within(4000).of(8000)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'caps maximum duration at max_timeout_secs' do
|
86
|
+
10.times { subject.next_interval }
|
87
|
+
expect(subject.next_interval).to eq(10000)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -3,7 +3,12 @@ require 'spec_helper'
|
|
3
3
|
module Segment
|
4
4
|
class Analytics
|
5
5
|
describe Client do
|
6
|
-
let(:client)
|
6
|
+
let(:client) do
|
7
|
+
Client.new(:write_key => WRITE_KEY).tap { |client|
|
8
|
+
# Ensure that worker doesn't consume items from the queue
|
9
|
+
client.instance_variable_set(:@worker, NoopWorker.new)
|
10
|
+
}
|
11
|
+
end
|
7
12
|
let(:queue) { client.instance_variable_get :@queue }
|
8
13
|
|
9
14
|
describe '#initialize' do
|
@@ -38,13 +43,13 @@ module Segment
|
|
38
43
|
client.track({
|
39
44
|
:user_id => 'user',
|
40
45
|
:event => 'Event',
|
41
|
-
:properties => [1,2,3]
|
46
|
+
:properties => [1, 2, 3]
|
42
47
|
})
|
43
48
|
}.to raise_error(ArgumentError)
|
44
49
|
end
|
45
50
|
|
46
51
|
it 'uses the timestamp given' do
|
47
|
-
time = Time.parse(
|
52
|
+
time = Time.parse('1990-07-16 13:30:00.123 UTC')
|
48
53
|
|
49
54
|
client.track({
|
50
55
|
:event => 'testing the timestamp',
|
@@ -78,22 +83,22 @@ module Segment
|
|
78
83
|
:properties => {
|
79
84
|
:time => Time.utc(2013),
|
80
85
|
:time_with_zone => Time.zone.parse('2013-01-01'),
|
81
|
-
:date_time => DateTime.new(2013,1,1),
|
82
|
-
:date => Date.new(2013,1,1),
|
86
|
+
:date_time => DateTime.new(2013, 1, 1),
|
87
|
+
:date => Date.new(2013, 1, 1),
|
83
88
|
:nottime => 'x'
|
84
89
|
}
|
85
90
|
})
|
86
91
|
message = queue.pop
|
87
92
|
|
88
|
-
|
89
|
-
expect(
|
90
|
-
expect(
|
91
|
-
expect(
|
92
|
-
expect(
|
93
|
+
properties = message[:properties]
|
94
|
+
expect(properties[:time]).to eq('2013-01-01T00:00:00.000Z')
|
95
|
+
expect(properties[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
|
96
|
+
expect(properties[:date_time]).to eq('2013-01-01T00:00:00.000+00:00')
|
97
|
+
expect(properties[:date]).to eq('2013-01-01')
|
98
|
+
expect(properties[:nottime]).to eq('x')
|
93
99
|
end
|
94
100
|
end
|
95
101
|
|
96
|
-
|
97
102
|
describe '#identify' do
|
98
103
|
it 'errors without any user id' do
|
99
104
|
expect { client.identify({}) }.to raise_error(ArgumentError)
|
@@ -119,19 +124,20 @@ module Segment
|
|
119
124
|
:traits => {
|
120
125
|
:time => Time.utc(2013),
|
121
126
|
:time_with_zone => Time.zone.parse('2013-01-01'),
|
122
|
-
:date_time => DateTime.new(2013,1,1),
|
123
|
-
:date => Date.new(2013,1,1),
|
127
|
+
:date_time => DateTime.new(2013, 1, 1),
|
128
|
+
:date => Date.new(2013, 1, 1),
|
124
129
|
:nottime => 'x'
|
125
130
|
}
|
126
131
|
})
|
127
132
|
|
128
133
|
message = queue.pop
|
129
134
|
|
130
|
-
|
131
|
-
expect(
|
132
|
-
expect(
|
133
|
-
expect(
|
134
|
-
expect(
|
135
|
+
traits = message[:traits]
|
136
|
+
expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z')
|
137
|
+
expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
|
138
|
+
expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00')
|
139
|
+
expect(traits[:date]).to eq('2013-01-01')
|
140
|
+
expect(traits[:nottime]).to eq('x')
|
135
141
|
end
|
136
142
|
end
|
137
143
|
|
@@ -156,10 +162,6 @@ module Segment
|
|
156
162
|
end
|
157
163
|
|
158
164
|
describe '#group' do
|
159
|
-
after do
|
160
|
-
client.flush
|
161
|
-
end
|
162
|
-
|
163
165
|
it 'errors without group_id' do
|
164
166
|
expect { client.group :user_id => 'foo' }.to raise_error(ArgumentError)
|
165
167
|
end
|
@@ -183,19 +185,20 @@ module Segment
|
|
183
185
|
:traits => {
|
184
186
|
:time => Time.utc(2013),
|
185
187
|
:time_with_zone => Time.zone.parse('2013-01-01'),
|
186
|
-
:date_time => DateTime.new(2013,1,1),
|
187
|
-
:date => Date.new(2013,1,1),
|
188
|
+
:date_time => DateTime.new(2013, 1, 1),
|
189
|
+
:date => Date.new(2013, 1, 1),
|
188
190
|
:nottime => 'x'
|
189
191
|
}
|
190
192
|
})
|
191
193
|
|
192
194
|
message = queue.pop
|
193
195
|
|
194
|
-
|
195
|
-
expect(
|
196
|
-
expect(
|
197
|
-
expect(
|
198
|
-
expect(
|
196
|
+
traits = message[:traits]
|
197
|
+
expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z')
|
198
|
+
expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z')
|
199
|
+
expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00')
|
200
|
+
expect(traits[:date]).to eq('2013-01-01')
|
201
|
+
expect(traits[:nottime]).to eq('x')
|
199
202
|
end
|
200
203
|
end
|
201
204
|
|
@@ -232,31 +235,35 @@ module Segment
|
|
232
235
|
end
|
233
236
|
|
234
237
|
describe '#flush' do
|
238
|
+
let(:client_with_worker) { Client.new(:write_key => WRITE_KEY) }
|
239
|
+
|
235
240
|
it 'waits for the queue to finish on a flush' do
|
236
|
-
|
237
|
-
|
238
|
-
|
241
|
+
client_with_worker.identify Queued::IDENTIFY
|
242
|
+
client_with_worker.track Queued::TRACK
|
243
|
+
client_with_worker.flush
|
239
244
|
|
240
|
-
expect(
|
245
|
+
expect(client_with_worker.queued_messages).to eq(0)
|
241
246
|
end
|
242
247
|
|
243
|
-
|
244
|
-
|
248
|
+
unless defined? JRUBY_VERSION
|
249
|
+
it 'completes when the process forks' do
|
250
|
+
client_with_worker.identify Queued::IDENTIFY
|
245
251
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
252
|
+
Process.fork do
|
253
|
+
client_with_worker.track Queued::TRACK
|
254
|
+
client_with_worker.flush
|
255
|
+
expect(client_with_worker.queued_messages).to eq(0)
|
256
|
+
end
|
251
257
|
|
252
|
-
|
253
|
-
|
258
|
+
Process.wait
|
259
|
+
end
|
260
|
+
end
|
254
261
|
end
|
255
262
|
|
256
263
|
context 'common' do
|
257
264
|
check_property = proc { |msg, k, v| msg[k] && msg[k] == v }
|
258
265
|
|
259
|
-
let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :message_id => 5, :event =>
|
266
|
+
let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :message_id => 5, :event => 'coco barked', :name => 'coco' } }
|
260
267
|
|
261
268
|
it 'does not convert ids given as fixnums to strings' do
|
262
269
|
[:track, :screen, :page, :identify].each do |s|
|
@@ -268,6 +275,16 @@ module Segment
|
|
268
275
|
end
|
269
276
|
end
|
270
277
|
|
278
|
+
it 'returns false if queue is full' do
|
279
|
+
client.instance_variable_set(:@max_queue_size, 1)
|
280
|
+
|
281
|
+
[:track, :screen, :page, :group, :identify, :alias].each do |s|
|
282
|
+
expect(client.send(s, data)).to eq(true)
|
283
|
+
expect(client.send(s, data)).to eq(false) # Queue is full
|
284
|
+
queue.pop(true)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
271
288
|
it 'converts message id to string' do
|
272
289
|
[:track, :screen, :page, :group, :identify, :alias].each do |s|
|
273
290
|
client.send(s, data)
|
@@ -299,7 +316,7 @@ module Segment
|
|
299
316
|
|
300
317
|
it 'sends integrations' do
|
301
318
|
[:track, :screen, :page, :group, :identify, :alias].each do |s|
|
302
|
-
client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event =>
|
319
|
+
client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => 'coco barked', :name => 'coco'
|
303
320
|
message = queue.pop(true)
|
304
321
|
expect(message[:integrations][:All]).to eq(true)
|
305
322
|
expect(message[:integrations][:Salesforce]).to eq(false)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Segment
|
4
|
+
# End-to-end tests that send events to a segment source and verifies that a
|
5
|
+
# webhook connected to the source (configured manually via the app) is able
|
6
|
+
# to receive the data sent by this library.
|
7
|
+
describe 'End-to-end tests', e2e: true do
|
8
|
+
# Segment write key for
|
9
|
+
# https://app.segment.com/segment-libraries/sources/analytics_ruby_e2e_test/overview.
|
10
|
+
#
|
11
|
+
# This source is configured to send events to the Runscope bucket used by
|
12
|
+
# this test.
|
13
|
+
WRITE_KEY = 'qhdMksLsQTi9MES3CHyzsWRRt4ub5VM6'
|
14
|
+
|
15
|
+
# Runscope bucket key for https://www.runscope.com/stream/umkvkgv7ndby
|
16
|
+
RUNSCOPE_BUCKET_KEY = 'umkvkgv7ndby'
|
17
|
+
|
18
|
+
let(:client) { Segment::Analytics.new(write_key: WRITE_KEY) }
|
19
|
+
let(:runscope_client) { RunscopeClient.new(ENV.fetch('RUNSCOPE_TOKEN')) }
|
20
|
+
|
21
|
+
it 'tracks events' do
|
22
|
+
id = SecureRandom.uuid
|
23
|
+
client.track(
|
24
|
+
user_id: 'dummy_user_id',
|
25
|
+
event: 'E2E Test',
|
26
|
+
properties: { id: id }
|
27
|
+
)
|
28
|
+
client.flush
|
29
|
+
|
30
|
+
# Allow events to propagate to runscope
|
31
|
+
eventually(timeout: 30) {
|
32
|
+
expect(has_matching_request?(id)).to eq(true)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_matching_request?(id)
|
37
|
+
captured_requests = runscope_client.requests(RUNSCOPE_BUCKET_KEY)
|
38
|
+
captured_requests.any? do |request|
|
39
|
+
begin
|
40
|
+
body = JSON.parse(request['body'])
|
41
|
+
body['properties'] && body['properties']['id'] == id
|
42
|
+
rescue JSON::ParserError
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Segment
|
4
|
+
class Analytics
|
5
|
+
describe MessageBatch do
|
6
|
+
subject { described_class.new(100) }
|
7
|
+
|
8
|
+
describe '#<<' do
|
9
|
+
it 'appends messages' do
|
10
|
+
subject << Message.new('a' => 'b')
|
11
|
+
expect(subject.length).to eq(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'rejects messages that exceed the maximum allowed size' do
|
15
|
+
max_bytes = Defaults::Message::MAX_BYTES
|
16
|
+
hash = { 'a' => 'b' * max_bytes }
|
17
|
+
message = Message.new(hash)
|
18
|
+
|
19
|
+
subject << message
|
20
|
+
expect(subject.length).to eq(0)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#full?' do
|
25
|
+
it 'returns true once item count is exceeded' do
|
26
|
+
99.times { subject << Message.new(a: 'b') }
|
27
|
+
expect(subject.full?).to be(false)
|
28
|
+
|
29
|
+
subject << Message.new(a: 'b')
|
30
|
+
expect(subject.full?).to be(true)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns true once max size is almost exceeded' do
|
34
|
+
message = Message.new(a: 'b' * (Defaults::Message::MAX_BYTES - 10))
|
35
|
+
|
36
|
+
# Each message is under the individual limit
|
37
|
+
expect(message.json_size).to be < Defaults::Message::MAX_BYTES
|
38
|
+
|
39
|
+
# Size of the batch is over the limit
|
40
|
+
expect(50 * message.json_size).to be > Defaults::MessageBatch::MAX_BYTES
|
41
|
+
|
42
|
+
expect(subject.full?).to be(false)
|
43
|
+
50.times { subject << message }
|
44
|
+
expect(subject.full?).to be(true)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Segment
|
4
|
+
class Analytics
|
5
|
+
describe Message do
|
6
|
+
describe '#to_json' do
|
7
|
+
it 'caches JSON conversions' do
|
8
|
+
# Keeps track of the number of times to_json was called
|
9
|
+
nested_obj = Class.new do
|
10
|
+
attr_reader :to_json_call_count
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@to_json_call_count = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_json(*_)
|
17
|
+
@to_json_call_count += 1
|
18
|
+
'{}'
|
19
|
+
end
|
20
|
+
end.new
|
21
|
+
|
22
|
+
message = Message.new('some_key' => nested_obj)
|
23
|
+
expect(nested_obj.to_json_call_count).to eq(0)
|
24
|
+
|
25
|
+
message.to_json
|
26
|
+
expect(nested_obj.to_json_call_count).to eq(1)
|
27
|
+
|
28
|
+
# When called a second time, the call count shouldn't increase
|
29
|
+
message.to_json
|
30
|
+
expect(nested_obj.to_json_call_count).to eq(1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -37,19 +37,25 @@ module Segment
|
|
37
37
|
|
38
38
|
context 'no options are set' do
|
39
39
|
it 'sets a default path' do
|
40
|
-
|
40
|
+
path = subject.instance_variable_get(:@path)
|
41
|
+
expect(path).to eq(described_class::PATH)
|
41
42
|
end
|
42
43
|
|
43
44
|
it 'sets a default retries' do
|
44
|
-
|
45
|
+
retries = subject.instance_variable_get(:@retries)
|
46
|
+
expect(retries).to eq(described_class::RETRIES)
|
45
47
|
end
|
46
48
|
|
47
|
-
it 'sets a default backoff' do
|
48
|
-
|
49
|
+
it 'sets a default backoff policy' do
|
50
|
+
backoff_policy = subject.instance_variable_get(:@backoff_policy)
|
51
|
+
expect(backoff_policy).to be_a(Segment::Analytics::BackoffPolicy)
|
49
52
|
end
|
50
53
|
|
51
54
|
it 'initializes a new Net::HTTP with default host and port' do
|
52
|
-
expect(Net::HTTP).to receive(:new).with(
|
55
|
+
expect(Net::HTTP).to receive(:new).with(
|
56
|
+
described_class::HOST,
|
57
|
+
described_class::PORT
|
58
|
+
)
|
53
59
|
described_class.new
|
54
60
|
end
|
55
61
|
end
|
@@ -57,14 +63,14 @@ module Segment
|
|
57
63
|
context 'options are given' do
|
58
64
|
let(:path) { 'my/cool/path' }
|
59
65
|
let(:retries) { 1234 }
|
60
|
-
let(:
|
66
|
+
let(:backoff_policy) { FakeBackoffPolicy.new([1, 2, 3]) }
|
61
67
|
let(:host) { 'http://www.example.com' }
|
62
68
|
let(:port) { 8080 }
|
63
69
|
let(:options) do
|
64
70
|
{
|
65
71
|
path: path,
|
66
72
|
retries: retries,
|
67
|
-
|
73
|
+
backoff_policy: backoff_policy,
|
68
74
|
host: host,
|
69
75
|
port: port
|
70
76
|
}
|
@@ -80,8 +86,9 @@ module Segment
|
|
80
86
|
expect(subject.instance_variable_get(:@retries)).to eq(retries)
|
81
87
|
end
|
82
88
|
|
83
|
-
it 'sets passed in backoff' do
|
84
|
-
expect(subject.instance_variable_get(:@
|
89
|
+
it 'sets passed in backoff backoff policy' do
|
90
|
+
expect(subject.instance_variable_get(:@backoff_policy))
|
91
|
+
.to eq(backoff_policy)
|
85
92
|
end
|
86
93
|
|
87
94
|
it 'initializes a new Net::HTTP with passed in host and port' do
|
@@ -92,7 +99,9 @@ module Segment
|
|
92
99
|
end
|
93
100
|
|
94
101
|
describe '#post' do
|
95
|
-
let(:response) {
|
102
|
+
let(:response) {
|
103
|
+
Net::HTTPResponse.new(http_version, status_code, response_body)
|
104
|
+
}
|
96
105
|
let(:http_version) { 1.1 }
|
97
106
|
let(:status_code) { 200 }
|
98
107
|
let(:response_body) { {}.to_json }
|
@@ -100,19 +109,29 @@ module Segment
|
|
100
109
|
let(:batch) { [] }
|
101
110
|
|
102
111
|
before do
|
103
|
-
|
112
|
+
http = subject.instance_variable_get(:@http)
|
113
|
+
allow(http).to receive(:request) { response }
|
104
114
|
allow(response).to receive(:body) { response_body }
|
105
115
|
end
|
106
116
|
|
107
117
|
it 'initalizes a new Net::HTTP::Post with path and default headers' do
|
108
118
|
path = subject.instance_variable_get(:@path)
|
109
|
-
default_headers = {
|
110
|
-
|
119
|
+
default_headers = {
|
120
|
+
'Content-Type' => 'application/json',
|
121
|
+
'Accept' => 'application/json',
|
122
|
+
'User-Agent' => "analytics-ruby/#{Analytics::VERSION}"
|
123
|
+
}
|
124
|
+
expect(Net::HTTP::Post).to receive(:new).with(
|
125
|
+
path, default_headers
|
126
|
+
).and_call_original
|
127
|
+
|
111
128
|
subject.post(write_key, batch)
|
112
129
|
end
|
113
130
|
|
114
131
|
it 'adds basic auth to the Net::HTTP::Post' do
|
115
|
-
expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth)
|
132
|
+
expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth)
|
133
|
+
.with(write_key, nil)
|
134
|
+
|
116
135
|
subject.post(write_key, batch)
|
117
136
|
end
|
118
137
|
|
@@ -136,6 +155,41 @@ module Segment
|
|
136
155
|
end
|
137
156
|
|
138
157
|
context 'a real request' do
|
158
|
+
RSpec.shared_examples('retried request') do |status_code, body|
|
159
|
+
let(:status_code) { status_code }
|
160
|
+
let(:body) { body }
|
161
|
+
let(:retries) { 4 }
|
162
|
+
let(:backoff_policy) { FakeBackoffPolicy.new([1000, 1000, 1000]) }
|
163
|
+
subject {
|
164
|
+
described_class.new(retries: retries,
|
165
|
+
backoff_policy: backoff_policy)
|
166
|
+
}
|
167
|
+
|
168
|
+
it 'retries the request' do
|
169
|
+
expect(subject)
|
170
|
+
.to receive(:sleep)
|
171
|
+
.exactly(retries - 1).times
|
172
|
+
.with(1)
|
173
|
+
.and_return(nil)
|
174
|
+
subject.post(write_key, batch)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
RSpec.shared_examples('non-retried request') do |status_code, body|
|
179
|
+
let(:status_code) { status_code }
|
180
|
+
let(:body) { body }
|
181
|
+
let(:retries) { 4 }
|
182
|
+
let(:backoff) { 1 }
|
183
|
+
subject { described_class.new(retries: retries, backoff: backoff) }
|
184
|
+
|
185
|
+
it 'does not retry the request' do
|
186
|
+
expect(subject)
|
187
|
+
.to receive(:sleep)
|
188
|
+
.never
|
189
|
+
subject.post(write_key, batch)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
139
193
|
context 'request is successful' do
|
140
194
|
let(:status_code) { 201 }
|
141
195
|
it 'returns a response code' do
|
@@ -156,33 +210,32 @@ module Segment
|
|
156
210
|
end
|
157
211
|
end
|
158
212
|
|
159
|
-
context 'request
|
160
|
-
|
213
|
+
context 'a request returns a failure status code' do
|
214
|
+
# Server errors must be retried
|
215
|
+
it_behaves_like('retried request', 500, '{}')
|
216
|
+
it_behaves_like('retried request', 503, '{}')
|
161
217
|
|
162
|
-
|
218
|
+
# All 4xx errors other than 429 (rate limited) must be retried
|
219
|
+
it_behaves_like('retried request', 429, '{}')
|
220
|
+
it_behaves_like('non-retried request', 404, '{}')
|
221
|
+
it_behaves_like('non-retried request', 400, '{}')
|
222
|
+
end
|
163
223
|
|
164
|
-
|
224
|
+
context 'request or parsing of response results in an exception' do
|
225
|
+
let(:response_body) { 'Malformed JSON ---' }
|
165
226
|
|
166
|
-
|
167
|
-
let(:retries) { 2 }
|
227
|
+
subject { described_class.new(retries: 0) }
|
168
228
|
|
169
|
-
|
170
|
-
|
171
|
-
subject.post(write_key, batch)
|
172
|
-
end
|
229
|
+
it 'returns a -1 for status' do
|
230
|
+
expect(subject.post(write_key, batch).status).to eq(-1)
|
173
231
|
end
|
174
232
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
it 'returns a -1 for status' do
|
179
|
-
expect(subject.post(write_key, batch).status).to eq(-1)
|
180
|
-
end
|
181
|
-
|
182
|
-
it 'has a connection error' do
|
183
|
-
expect(subject.post(write_key, batch).error).to match(/Connection error/)
|
184
|
-
end
|
233
|
+
it 'has a connection error' do
|
234
|
+
error = subject.post(write_key, batch).error
|
235
|
+
expect(error).to match(/Connection error/)
|
185
236
|
end
|
237
|
+
|
238
|
+
it_behaves_like('retried request', 200, 'Malformed JSON ---')
|
186
239
|
end
|
187
240
|
end
|
188
241
|
end
|