segment 2.2.5

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.
@@ -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
@@ -0,0 +1,244 @@
1
+ require 'spec_helper'
2
+
3
+ module Segment
4
+ class Analytics
5
+ describe Request do
6
+ before do
7
+ # Try and keep debug statements out of tests
8
+ allow(subject.logger).to receive(:error)
9
+ allow(subject.logger).to receive(:debug)
10
+ end
11
+
12
+ describe '#initialize' do
13
+ let!(:net_http) { Net::HTTP.new(anything, anything) }
14
+
15
+ before do
16
+ allow(Net::HTTP).to receive(:new) { net_http }
17
+ end
18
+
19
+ it 'sets an initalized Net::HTTP read_timeout' do
20
+ expect(net_http).to receive(:use_ssl=)
21
+ described_class.new
22
+ end
23
+
24
+ it 'sets an initalized Net::HTTP read_timeout' do
25
+ expect(net_http).to receive(:read_timeout=)
26
+ described_class.new
27
+ end
28
+
29
+ it 'sets an initalized Net::HTTP open_timeout' do
30
+ expect(net_http).to receive(:open_timeout=)
31
+ described_class.new
32
+ end
33
+
34
+ it 'sets the http client' do
35
+ expect(subject.instance_variable_get(:@http)).to_not be_nil
36
+ end
37
+
38
+ context 'no options are set' do
39
+ it 'sets a default path' do
40
+ path = subject.instance_variable_get(:@path)
41
+ expect(path).to eq(described_class::PATH)
42
+ end
43
+
44
+ it 'sets a default retries' do
45
+ retries = subject.instance_variable_get(:@retries)
46
+ expect(retries).to eq(described_class::RETRIES)
47
+ end
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)
52
+ end
53
+
54
+ it 'initializes a new Net::HTTP with default host and port' do
55
+ expect(Net::HTTP).to receive(:new).with(
56
+ described_class::HOST,
57
+ described_class::PORT
58
+ )
59
+ described_class.new
60
+ end
61
+ end
62
+
63
+ context 'options are given' do
64
+ let(:path) { 'my/cool/path' }
65
+ let(:retries) { 1234 }
66
+ let(:backoff_policy) { FakeBackoffPolicy.new([1, 2, 3]) }
67
+ let(:host) { 'http://www.example.com' }
68
+ let(:port) { 8080 }
69
+ let(:options) do
70
+ {
71
+ path: path,
72
+ retries: retries,
73
+ backoff_policy: backoff_policy,
74
+ host: host,
75
+ port: port
76
+ }
77
+ end
78
+
79
+ subject { described_class.new(options) }
80
+
81
+ it 'sets passed in path' do
82
+ expect(subject.instance_variable_get(:@path)).to eq(path)
83
+ end
84
+
85
+ it 'sets passed in retries' do
86
+ expect(subject.instance_variable_get(:@retries)).to eq(retries)
87
+ end
88
+
89
+ it 'sets passed in backoff backoff policy' do
90
+ expect(subject.instance_variable_get(:@backoff_policy))
91
+ .to eq(backoff_policy)
92
+ end
93
+
94
+ it 'initializes a new Net::HTTP with passed in host and port' do
95
+ expect(Net::HTTP).to receive(:new).with(host, port)
96
+ described_class.new(options)
97
+ end
98
+ end
99
+ end
100
+
101
+ describe '#post' do
102
+ let(:response) {
103
+ Net::HTTPResponse.new(http_version, status_code, response_body)
104
+ }
105
+ let(:http_version) { 1.1 }
106
+ let(:status_code) { 200 }
107
+ let(:response_body) { {}.to_json }
108
+ let(:write_key) { 'abcdefg' }
109
+ let(:batch) { [] }
110
+
111
+ before do
112
+ http = subject.instance_variable_get(:@http)
113
+ allow(http).to receive(:request) { response }
114
+ allow(response).to receive(:body) { response_body }
115
+ end
116
+
117
+ it 'initalizes a new Net::HTTP::Post with path and default headers' do
118
+ path = subject.instance_variable_get(:@path)
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
+
128
+ subject.post(write_key, batch)
129
+ end
130
+
131
+ it 'adds basic auth to the Net::HTTP::Post' do
132
+ expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth)
133
+ .with(write_key, nil)
134
+
135
+ subject.post(write_key, batch)
136
+ end
137
+
138
+ context 'with a stub' do
139
+ before do
140
+ allow(described_class).to receive(:stub) { true }
141
+ end
142
+
143
+ it 'returns a 200 response' do
144
+ expect(subject.post(write_key, batch).status).to eq(200)
145
+ end
146
+
147
+ it 'has a nil error' do
148
+ expect(subject.post(write_key, batch).error).to be_nil
149
+ end
150
+
151
+ it 'logs a debug statement' do
152
+ expect(subject.logger).to receive(:debug).with(/stubbed request to/)
153
+ subject.post(write_key, batch)
154
+ end
155
+ end
156
+
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
+
193
+ context 'request is successful' do
194
+ let(:status_code) { 201 }
195
+ it 'returns a response code' do
196
+ expect(subject.post(write_key, batch).status).to eq(status_code)
197
+ end
198
+
199
+ it 'returns a nil error' do
200
+ expect(subject.post(write_key, batch).error).to be_nil
201
+ end
202
+ end
203
+
204
+ context 'request results in errorful response' do
205
+ let(:error) { 'this is an error' }
206
+ let(:response_body) { { error: error }.to_json }
207
+
208
+ it 'returns the parsed error' do
209
+ expect(subject.post(write_key, batch).error).to eq(error)
210
+ end
211
+ end
212
+
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, '{}')
217
+
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
223
+
224
+ context 'request or parsing of response results in an exception' do
225
+ let(:response_body) { 'Malformed JSON ---' }
226
+
227
+ subject { described_class.new(retries: 0) }
228
+
229
+ it 'returns a -1 for status' do
230
+ expect(subject.post(write_key, batch).status).to eq(-1)
231
+ end
232
+
233
+ it 'has a connection error' do
234
+ error = subject.post(write_key, batch).error
235
+ expect(error).to match(/Connection error/)
236
+ end
237
+
238
+ it_behaves_like('retried request', 200, 'Malformed JSON ---')
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module Segment
4
+ class Analytics
5
+ describe Response do
6
+ describe '#status' do
7
+ it { expect(subject).to respond_to(:status) }
8
+ end
9
+
10
+ describe '#error' do
11
+ it { expect(subject).to respond_to(:error) }
12
+ end
13
+
14
+ describe '#initialize' do
15
+ let(:status) { 404 }
16
+ let(:error) { 'Oh No' }
17
+
18
+ subject { described_class.new(status, error) }
19
+
20
+ it 'sets the instance variable status' do
21
+ expect(subject.instance_variable_get(:@status)).to eq(status)
22
+ end
23
+
24
+ it 'sets the instance variable error' do
25
+ expect(subject.instance_variable_get(:@error)).to eq(error)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ module Segment
4
+ class Analytics
5
+ describe Worker do
6
+ describe '#init' do
7
+ it 'accepts string keys' do
8
+ queue = Queue.new
9
+ worker = Segment::Analytics::Worker.new(queue,
10
+ 'secret',
11
+ 'batch_size' => 100)
12
+ batch = worker.instance_variable_get(:@batch)
13
+ expect(batch.instance_variable_get(:@max_message_count)).to eq(100)
14
+ end
15
+ end
16
+
17
+ describe '#run' do
18
+ before :all do
19
+ Segment::Analytics::Defaults::Request::BACKOFF = 0.1
20
+ end
21
+
22
+ after :all do
23
+ Segment::Analytics::Defaults::Request::BACKOFF = 30.0
24
+ end
25
+
26
+ it 'does not error if the endpoint is unreachable' do
27
+ expect do
28
+ Net::HTTP.any_instance.stub(:post).and_raise(Exception)
29
+
30
+ queue = Queue.new
31
+ queue << {}
32
+ worker = Segment::Analytics::Worker.new(queue, 'secret')
33
+ worker.run
34
+
35
+ expect(queue).to be_empty
36
+
37
+ Net::HTTP.any_instance.unstub(:post)
38
+ end.to_not raise_error
39
+ end
40
+
41
+ it 'executes the error handler if the request is invalid' do
42
+ Segment::Analytics::Request
43
+ .any_instance
44
+ .stub(:post)
45
+ .and_return(Segment::Analytics::Response.new(400, 'Some error'))
46
+
47
+ status = error = nil
48
+ on_error = proc do |yielded_status, yielded_error|
49
+ sleep 0.2 # Make this take longer than thread spin-up (below)
50
+ status, error = yielded_status, yielded_error
51
+ end
52
+
53
+ queue = Queue.new
54
+ queue << {}
55
+ worker = described_class.new(queue, 'secret', :on_error => on_error)
56
+
57
+ # This is to ensure that Client#flush doesn't finish before calling
58
+ # the error handler.
59
+ Thread.new { worker.run }
60
+ sleep 0.1 # First give thread time to spin-up.
61
+ sleep 0.01 while worker.is_requesting?
62
+
63
+ Segment::Analytics::Request.any_instance.unstub(:post)
64
+
65
+ expect(queue).to be_empty
66
+ expect(status).to eq(400)
67
+ expect(error).to eq('Some error')
68
+ end
69
+
70
+ it 'does not call on_error if the request is good' do
71
+ on_error = proc do |status, error|
72
+ puts "#{status}, #{error}"
73
+ end
74
+
75
+ expect(on_error).to_not receive(:call)
76
+
77
+ queue = Queue.new
78
+ queue << Requested::TRACK
79
+ worker = described_class.new(queue,
80
+ 'testsecret',
81
+ :on_error => on_error)
82
+ worker.run
83
+
84
+ expect(queue).to be_empty
85
+ end
86
+ end
87
+
88
+ describe '#is_requesting?' do
89
+ it 'does not return true if there isn\'t a current batch' do
90
+ queue = Queue.new
91
+ worker = Segment::Analytics::Worker.new(queue, 'testsecret')
92
+
93
+ expect(worker.is_requesting?).to eq(false)
94
+ end
95
+
96
+ it 'returns true if there is a current batch' do
97
+ queue = Queue.new
98
+ queue << Requested::TRACK
99
+ worker = Segment::Analytics::Worker.new(queue, 'testsecret')
100
+
101
+ worker_thread = Thread.new { worker.run }
102
+ eventually { expect(worker.is_requesting?).to eq(true) }
103
+
104
+ worker_thread.join
105
+ expect(worker.is_requesting?).to eq(false)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end