analytics-ruby 2.0.13 → 2.1.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 +4 -4
- data/Gemfile.lock +15 -15
- data/History.md +15 -0
- data/Makefile +1 -1
- data/analytics-ruby-2.0.13.gem +0 -0
- data/analytics-ruby-2.1.0.gem +0 -0
- data/lib/segment/analytics.rb +1 -1
- data/lib/segment/analytics/client.rb +19 -2
- data/lib/segment/analytics/version.rb +1 -1
- data/lib/segment/analytics/worker.rb +2 -2
- data/spec/segment/analytics/client_spec.rb +160 -140
- data/spec/segment/analytics/request_spec.rb +191 -0
- data/spec/segment/analytics/response_spec.rb +30 -0
- data/spec/segment/analytics/worker_spec.rb +33 -27
- data/spec/segment/analytics_spec.rb +40 -29
- metadata +25 -21
@@ -0,0 +1,191 @@
|
|
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
|
+
expect(subject.instance_variable_get(:@path)).to eq(described_class::PATH)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'sets a default retries' do
|
44
|
+
expect(subject.instance_variable_get(:@retries)).to eq(described_class::RETRIES)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'sets a default backoff' do
|
48
|
+
expect(subject.instance_variable_get(:@backoff)).to eq(described_class::BACKOFF)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'initializes a new Net::HTTP with default host and port' do
|
52
|
+
expect(Net::HTTP).to receive(:new).with(described_class::HOST, described_class::PORT)
|
53
|
+
described_class.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'options are given' do
|
58
|
+
let(:path) { 'my/cool/path' }
|
59
|
+
let(:retries) { 1234 }
|
60
|
+
let(:backoff) { 10 }
|
61
|
+
let(:host) { 'http://www.example.com' }
|
62
|
+
let(:port) { 8080 }
|
63
|
+
let(:options) do
|
64
|
+
{
|
65
|
+
path: path,
|
66
|
+
retries: retries,
|
67
|
+
backoff: backoff,
|
68
|
+
host: host,
|
69
|
+
port: port
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
subject { described_class.new(options) }
|
74
|
+
|
75
|
+
it 'sets passed in path' do
|
76
|
+
expect(subject.instance_variable_get(:@path)).to eq(path)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'sets passed in retries' do
|
80
|
+
expect(subject.instance_variable_get(:@retries)).to eq(retries)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'sets passed in backoff' do
|
84
|
+
expect(subject.instance_variable_get(:@backoff)).to eq(backoff)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'initializes a new Net::HTTP with passed in host and port' do
|
88
|
+
expect(Net::HTTP).to receive(:new).with(host, port)
|
89
|
+
described_class.new(options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#post' do
|
95
|
+
let(:response) { Net::HTTPResponse.new(http_version, status_code, response_body) }
|
96
|
+
let(:http_version) { 1.1 }
|
97
|
+
let(:status_code) { 200 }
|
98
|
+
let(:response_body) { {}.to_json }
|
99
|
+
let(:write_key) { 'abcdefg' }
|
100
|
+
let(:batch) { [] }
|
101
|
+
|
102
|
+
before do
|
103
|
+
allow(subject.instance_variable_get(:@http)).to receive(:request) { response }
|
104
|
+
allow(response).to receive(:body) { response_body }
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'initalizes a new Net::HTTP::Post with path and default headers' do
|
108
|
+
path = subject.instance_variable_get(:@path)
|
109
|
+
default_headers = { 'Content-Type' => 'application/json', 'accept' => 'application/json' }
|
110
|
+
expect(Net::HTTP::Post).to receive(:new).with(path, default_headers).and_call_original
|
111
|
+
subject.post(write_key, batch)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'adds basic auth to the Net::HTTP::Post' do
|
115
|
+
expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth).with(write_key, nil)
|
116
|
+
subject.post(write_key, batch)
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'with a stub' do
|
120
|
+
before do
|
121
|
+
allow(described_class).to receive(:stub) { true }
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns a 200 response' do
|
125
|
+
expect(subject.post(write_key, batch).status).to eq(200)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'has a nil error' do
|
129
|
+
expect(subject.post(write_key, batch).error).to be_nil
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'logs a debug statement' do
|
133
|
+
expect(subject.logger).to receive(:debug).with(/stubbed request to/)
|
134
|
+
subject.post(write_key, batch)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'a real request' do
|
139
|
+
context 'request is successful' do
|
140
|
+
let(:status_code) { 201 }
|
141
|
+
it 'returns a response code' do
|
142
|
+
expect(subject.post(write_key, batch).status).to eq(status_code)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'returns a nil error' do
|
146
|
+
expect(subject.post(write_key, batch).error).to be_nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'request results in errorful response' do
|
151
|
+
let(:error) { 'this is an error' }
|
152
|
+
let(:response_body) { { error: error }.to_json }
|
153
|
+
|
154
|
+
it 'returns the parsed error' do
|
155
|
+
expect(subject.post(write_key, batch).error).to eq(error)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'request or parsing of response results in an exception' do
|
160
|
+
let(:response_body) { 'Malformed JSON ---' }
|
161
|
+
|
162
|
+
let(:backoff) { 0 }
|
163
|
+
|
164
|
+
subject { described_class.new(retries: retries, backoff: backoff) }
|
165
|
+
|
166
|
+
context 'remaining retries is > 1' do
|
167
|
+
let(:retries) { 2 }
|
168
|
+
|
169
|
+
it 'sleeps' do
|
170
|
+
expect(subject).to receive(:sleep).exactly(retries - 1).times
|
171
|
+
subject.post(write_key, batch)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'remaining retries is 1' do
|
176
|
+
let(:retries) { 1 }
|
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
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
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
|
@@ -7,11 +7,11 @@ module Segment
|
|
7
7
|
it 'accepts string keys' do
|
8
8
|
queue = Queue.new
|
9
9
|
worker = Segment::Analytics::Worker.new(queue, 'secret', 'batch_size' => 100)
|
10
|
-
worker.instance_variable_get(:@batch_size).
|
10
|
+
expect(worker.instance_variable_get(:@batch_size)).to eq(100)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
describe '#
|
14
|
+
describe '#run' do
|
15
15
|
before :all do
|
16
16
|
Segment::Analytics::Defaults::Request::BACKOFF = 0.1
|
17
17
|
end
|
@@ -20,75 +20,81 @@ module Segment
|
|
20
20
|
Segment::Analytics::Defaults::Request::BACKOFF = 30.0
|
21
21
|
end
|
22
22
|
|
23
|
-
it '
|
24
|
-
|
23
|
+
it 'does not error if the endpoint is unreachable' do
|
24
|
+
expect do
|
25
|
+
Net::HTTP.any_instance.stub(:post).and_raise(Exception)
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
queue = Queue.new
|
28
|
+
queue << {}
|
29
|
+
worker = Segment::Analytics::Worker.new(queue, 'secret')
|
30
|
+
worker.run
|
30
31
|
|
31
|
-
|
32
|
+
expect(queue).to be_empty
|
32
33
|
|
33
|
-
|
34
|
+
Net::HTTP.any_instance.unstub(:post)
|
35
|
+
end.to_not raise_error
|
34
36
|
end
|
35
37
|
|
36
|
-
it '
|
38
|
+
it 'executes the error handler, before the request phase ends, if the request is invalid' do
|
37
39
|
Segment::Analytics::Request.any_instance.stub(:post).and_return(Segment::Analytics::Response.new(400, "Some error"))
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
+
status = error = nil
|
42
|
+
on_error = Proc.new do |yielded_status, yielded_error|
|
43
|
+
sleep 0.2 # Make this take longer than thread spin-up (below)
|
44
|
+
status, error = yielded_status, yielded_error
|
41
45
|
end
|
42
46
|
|
43
|
-
on_error.should_receive(:call).once
|
44
|
-
|
45
47
|
queue = Queue.new
|
46
48
|
queue << {}
|
47
49
|
worker = Segment::Analytics::Worker.new queue, 'secret', :on_error => on_error
|
48
|
-
|
50
|
+
|
51
|
+
# This is to ensure that Client#flush doesn’t finish before calling the error handler.
|
52
|
+
Thread.new { worker.run }
|
53
|
+
sleep 0.1 # First give thread time to spin-up.
|
54
|
+
sleep 0.01 while worker.is_requesting?
|
49
55
|
|
50
56
|
Segment::Analytics::Request::any_instance.unstub(:post)
|
51
57
|
|
52
|
-
queue.
|
58
|
+
expect(queue).to be_empty
|
59
|
+
expect(status).to eq(400)
|
60
|
+
expect(error).to eq('Some error')
|
53
61
|
end
|
54
62
|
|
55
|
-
it '
|
56
|
-
|
63
|
+
it 'does not call on_error if the request is good' do
|
57
64
|
on_error = Proc.new do |status, error|
|
58
65
|
puts "#{status}, #{error}"
|
59
66
|
end
|
60
67
|
|
61
|
-
on_error.
|
68
|
+
expect(on_error).to_not receive(:call)
|
62
69
|
|
63
70
|
queue = Queue.new
|
64
71
|
queue << Requested::TRACK
|
65
72
|
worker = Segment::Analytics::Worker.new queue, 'testsecret', :on_error => on_error
|
66
73
|
worker.run
|
67
74
|
|
68
|
-
queue.
|
75
|
+
expect(queue).to be_empty
|
69
76
|
end
|
70
77
|
end
|
71
78
|
|
72
79
|
describe '#is_requesting?' do
|
73
|
-
it '
|
74
|
-
|
80
|
+
it 'does not return true if there isn\'t a current batch' do
|
75
81
|
queue = Queue.new
|
76
82
|
worker = Segment::Analytics::Worker.new(queue, 'testsecret')
|
77
83
|
|
78
|
-
worker.is_requesting
|
84
|
+
expect(worker.is_requesting?).to eq(false)
|
79
85
|
end
|
80
86
|
|
81
|
-
it '
|
87
|
+
it 'returns true if there is a current batch' do
|
82
88
|
queue = Queue.new
|
83
89
|
queue << Requested::TRACK
|
84
90
|
worker = Segment::Analytics::Worker.new(queue, 'testsecret')
|
85
91
|
|
86
92
|
Thread.new do
|
87
93
|
worker.run
|
88
|
-
worker.is_requesting
|
94
|
+
expect(worker.is_requesting?).to eq(false)
|
89
95
|
end
|
90
96
|
|
91
|
-
eventually { worker.is_requesting
|
97
|
+
eventually { expect(worker.is_requesting?).to eq(true) }
|
92
98
|
end
|
93
99
|
end
|
94
100
|
end
|
@@ -6,88 +6,99 @@ module Segment
|
|
6
6
|
let(:analytics) { Segment::Analytics.new :write_key => WRITE_KEY, :stub => true }
|
7
7
|
|
8
8
|
describe '#track' do
|
9
|
-
it '
|
9
|
+
it 'errors without an event' do
|
10
10
|
expect { analytics.track(:user_id => 'user') }.to raise_error(ArgumentError)
|
11
11
|
end
|
12
12
|
|
13
|
-
it '
|
13
|
+
it 'errors without a user_id' do
|
14
14
|
expect { analytics.track(:event => 'Event') }.to raise_error(ArgumentError)
|
15
15
|
end
|
16
16
|
|
17
|
-
it '
|
18
|
-
|
19
|
-
|
17
|
+
it 'does not error with the required options' do
|
18
|
+
expect do
|
19
|
+
analytics.track Queued::TRACK
|
20
|
+
sleep(1)
|
21
|
+
end.to_not raise_error
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
|
-
|
24
25
|
describe '#identify' do
|
25
|
-
it '
|
26
|
+
it 'errors without a user_id' do
|
26
27
|
expect { analytics.identify :traits => {} }.to raise_error(ArgumentError)
|
27
28
|
end
|
28
29
|
|
29
|
-
it '
|
30
|
+
it 'does not error with the required options' do
|
30
31
|
analytics.identify Queued::IDENTIFY
|
31
32
|
sleep(1)
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
36
|
describe '#alias' do
|
36
|
-
it '
|
37
|
+
it 'errors without from' do
|
37
38
|
expect { analytics.alias :user_id => 1234 }.to raise_error(ArgumentError)
|
38
39
|
end
|
39
40
|
|
40
|
-
it '
|
41
|
+
it 'errors without to' do
|
41
42
|
expect { analytics.alias :previous_id => 1234 }.to raise_error(ArgumentError)
|
42
43
|
end
|
43
44
|
|
44
|
-
it '
|
45
|
-
|
46
|
-
|
45
|
+
it 'does not error with the required options' do
|
46
|
+
expect do
|
47
|
+
analytics.alias ALIAS
|
48
|
+
sleep(1)
|
49
|
+
end.to_not raise_error
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
53
|
describe '#group' do
|
51
|
-
it '
|
54
|
+
it 'errors without group_id' do
|
52
55
|
expect { analytics.group :user_id => 'foo' }.to raise_error(ArgumentError)
|
53
56
|
end
|
54
57
|
|
55
|
-
it '
|
58
|
+
it 'errors without user_id or anonymous_id' do
|
56
59
|
expect { analytics.group :group_id => 'foo' }.to raise_error(ArgumentError)
|
57
60
|
end
|
58
61
|
|
59
|
-
it '
|
60
|
-
|
61
|
-
|
62
|
+
it 'does not error with the required options' do
|
63
|
+
expect do
|
64
|
+
analytics.group Queued::GROUP
|
65
|
+
sleep(1)
|
66
|
+
end.to_not raise_error
|
62
67
|
end
|
63
68
|
end
|
64
69
|
|
65
70
|
describe '#page' do
|
66
|
-
it '
|
71
|
+
it 'errors without user_id or anonymous_id' do
|
67
72
|
expect { analytics.page :name => 'foo' }.to raise_error(ArgumentError)
|
68
73
|
end
|
69
74
|
|
70
|
-
it '
|
71
|
-
|
72
|
-
|
75
|
+
it 'does not error with the required options' do
|
76
|
+
expect do
|
77
|
+
analytics.page Queued::PAGE
|
78
|
+
sleep(1)
|
79
|
+
end.to_not raise_error
|
73
80
|
end
|
74
81
|
end
|
75
82
|
|
76
83
|
describe '#screen' do
|
77
|
-
it '
|
84
|
+
it 'errors without user_id or anonymous_id' do
|
78
85
|
expect { analytics.screen :name => 'foo' }.to raise_error(ArgumentError)
|
79
86
|
end
|
80
87
|
|
81
|
-
it '
|
82
|
-
|
83
|
-
|
88
|
+
it 'does not error with the required options' do
|
89
|
+
expect do
|
90
|
+
analytics.screen Queued::SCREEN
|
91
|
+
sleep(1)
|
92
|
+
end.to_not raise_error
|
84
93
|
end
|
85
94
|
end
|
86
95
|
|
87
96
|
describe '#flush' do
|
88
|
-
it '
|
89
|
-
|
90
|
-
|
97
|
+
it 'flushes without error' do
|
98
|
+
expect do
|
99
|
+
analytics.identify Queued::IDENTIFY
|
100
|
+
analytics.flush
|
101
|
+
end.to_not raise_error
|
91
102
|
end
|
92
103
|
end
|
93
104
|
end
|