airbrake-ruby 2.9.0 → 2.10.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/lib/airbrake-ruby.rb +40 -18
- data/lib/airbrake-ruby/async_sender.rb +0 -6
- data/lib/airbrake-ruby/backtrace.rb +0 -10
- data/lib/airbrake-ruby/code_hunk.rb +0 -4
- data/lib/airbrake-ruby/config.rb +23 -22
- data/lib/airbrake-ruby/config/validator.rb +0 -10
- data/lib/airbrake-ruby/file_cache.rb +0 -6
- data/lib/airbrake-ruby/filter_chain.rb +0 -5
- data/lib/airbrake-ruby/filters/context_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +2 -3
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +1 -2
- data/lib/airbrake-ruby/filters/keys_filter.rb +9 -14
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +0 -2
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +2 -3
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +2 -3
- data/lib/airbrake-ruby/filters/thread_filter.rb +2 -4
- data/lib/airbrake-ruby/nested_exception.rb +0 -2
- data/lib/airbrake-ruby/notice.rb +6 -44
- data/lib/airbrake-ruby/notifier.rb +4 -40
- data/lib/airbrake-ruby/promise.rb +0 -6
- data/lib/airbrake-ruby/response.rb +0 -4
- data/lib/airbrake-ruby/sync_sender.rb +0 -4
- data/lib/airbrake-ruby/version.rb +1 -3
- data/spec/airbrake_spec.rb +71 -140
- data/spec/async_sender_spec.rb +9 -0
- data/spec/config_spec.rb +4 -0
- data/spec/filters/dependency_filter_spec.rb +16 -0
- data/spec/filters/exception_attributes_filter_spec.rb +65 -0
- data/spec/filters/keys_whitelist_spec.rb +17 -23
- data/spec/notice_spec.rb +111 -69
- data/spec/notifier_spec.rb +304 -495
- data/spec/response_spec.rb +82 -0
- data/spec/sync_sender_spec.rb +31 -14
- metadata +10 -2
data/spec/async_sender_spec.rb
CHANGED
@@ -117,6 +117,15 @@ RSpec.describe Airbrake::AsyncSender do
|
|
117
117
|
@sender.close
|
118
118
|
expect(@sender).not_to have_workers
|
119
119
|
end
|
120
|
+
|
121
|
+
it "respawns workers on fork()", skip: %w[jruby rbx].include?(RUBY_ENGINE) do
|
122
|
+
pid = fork do
|
123
|
+
expect(@sender).to have_workers
|
124
|
+
end
|
125
|
+
Process.wait(pid)
|
126
|
+
@sender.close
|
127
|
+
expect(@sender).not_to have_workers
|
128
|
+
end
|
120
129
|
end
|
121
130
|
|
122
131
|
describe "#spawn_workers" do
|
data/spec/config_spec.rb
CHANGED
@@ -25,6 +25,10 @@ RSpec.describe Airbrake::Config do
|
|
25
25
|
expect(config.app_version).to be_nil
|
26
26
|
end
|
27
27
|
|
28
|
+
it "sets the default versions" do
|
29
|
+
expect(config.versions).to be_empty
|
30
|
+
end
|
31
|
+
|
28
32
|
it "sets the default host" do
|
29
33
|
expect(config.host).to eq('https://airbrake.io')
|
30
34
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::Filters::DependencyFilter do
|
4
|
+
let(:notice) do
|
5
|
+
Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#call" do
|
9
|
+
it "attaches loaded dependencies to context/versions/dependencies" do
|
10
|
+
subject.call(notice)
|
11
|
+
expect(notice[:context][:versions][:dependencies]).to include(
|
12
|
+
'airbrake-ruby' => Airbrake::AIRBRAKE_RUBY_VERSION
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::Filters::ExceptionAttributesFilter do
|
4
|
+
describe "#call" do
|
5
|
+
let(:out) { StringIO.new }
|
6
|
+
let(:notice) { Airbrake::Notice.new(Airbrake::Config.new, ex) }
|
7
|
+
|
8
|
+
subject { described_class.new(Logger.new(out)) }
|
9
|
+
|
10
|
+
context "when #to_airbrake returns a non-Hash object" do
|
11
|
+
let(:ex) do
|
12
|
+
Class.new(AirbrakeTestError) do
|
13
|
+
def to_airbrake
|
14
|
+
Object.new
|
15
|
+
end
|
16
|
+
end.new
|
17
|
+
end
|
18
|
+
|
19
|
+
it "doesn't raise" do
|
20
|
+
expect { subject.call(notice) }.not_to raise_error
|
21
|
+
expect(notice[:params]).to be_empty
|
22
|
+
end
|
23
|
+
|
24
|
+
it "logs the error" do
|
25
|
+
expect { subject.call(notice) }.not_to raise_error
|
26
|
+
expect(out.string).to match(/wanted Hash, got Object/)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when #to_airbrake errors out" do
|
31
|
+
let(:ex) do
|
32
|
+
Class.new(AirbrakeTestError) do
|
33
|
+
def to_airbrake
|
34
|
+
1 / 0
|
35
|
+
end
|
36
|
+
end.new
|
37
|
+
end
|
38
|
+
|
39
|
+
it "doesn't raise" do
|
40
|
+
expect { subject.call(notice) }.not_to raise_error
|
41
|
+
expect(notice[:params]).to be_empty
|
42
|
+
end
|
43
|
+
|
44
|
+
it "logs the error" do
|
45
|
+
expect { subject.call(notice) }.not_to raise_error
|
46
|
+
expect(out.string).to match(/#to_airbrake failed.+ZeroDivisionError/)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when #to_airbrake returns a hash" do
|
51
|
+
let(:ex) do
|
52
|
+
Class.new(AirbrakeTestError) do
|
53
|
+
def to_airbrake
|
54
|
+
{ params: { foo: '1' } }
|
55
|
+
end
|
56
|
+
end.new
|
57
|
+
end
|
58
|
+
|
59
|
+
it "merges parameters with the notice" do
|
60
|
+
subject.call(notice)
|
61
|
+
expect(notice[:params]).to eq(foo: '1')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -146,31 +146,25 @@ RSpec.describe Airbrake::Filters::KeysWhitelist do
|
|
146
146
|
end
|
147
147
|
|
148
148
|
context "and it is recursive" do
|
149
|
-
let(:patterns) {
|
150
|
-
|
151
|
-
it "
|
152
|
-
|
153
|
-
|
149
|
+
let(:patterns) { ['bingo'] }
|
150
|
+
|
151
|
+
it "raises error (MRI)", skip: (
|
152
|
+
# MRI 2.3 & 2.4 may segfault on Circle CI. Example build:
|
153
|
+
# https://circleci.com/workflow-run/c112358c-e7bf-4789-9eb2-4891ea84da68
|
154
|
+
RUBY_ENGINE == 'ruby' && RUBY_VERSION =~ /\A2\.[34]\.\d+\z/
|
155
|
+
) do
|
156
|
+
bongo = {}
|
157
|
+
bongo[:bingo] = bongo
|
154
158
|
notice[:params] = bongo
|
155
159
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
#
|
160
|
-
#
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end.to raise_error(SystemStackError)
|
165
|
-
rescue RSpec::Expectations::ExpectationNotMetError
|
166
|
-
expect do
|
167
|
-
subject.call(notice)
|
168
|
-
end.to raise_error(java.lang.StackOverflowError)
|
169
|
-
end
|
170
|
-
else
|
171
|
-
expect do
|
172
|
-
subject.call(notice)
|
173
|
-
end.to raise_error(SystemStackError)
|
160
|
+
begin
|
161
|
+
expect { subject.call(notice) }.to raise_error(SystemStackError)
|
162
|
+
rescue RSpec::Expectations::ExpectationNotMetError => ex
|
163
|
+
# JRuby might raise two different exceptions, which represent the same
|
164
|
+
# thing. One is a Java exception, the other is a Ruby exception.
|
165
|
+
# Likely a bug: https://github.com/jruby/jruby/issues/1903
|
166
|
+
raise ex unless RUBY_ENGINE == 'jruby'
|
167
|
+
expect { subject.call(notice) }.to raise_error(java.lang.StackOverflowError)
|
174
168
|
end
|
175
169
|
end
|
176
170
|
end
|
data/spec/notice_spec.rb
CHANGED
@@ -5,75 +5,6 @@ RSpec.describe Airbrake::Notice do
|
|
5
5
|
described_class.new(Airbrake::Config.new, AirbrakeTestError.new, bingo: '1')
|
6
6
|
end
|
7
7
|
|
8
|
-
describe "#new" do
|
9
|
-
let(:params) do
|
10
|
-
{ bingo: 'bango', bongo: 'bish' }
|
11
|
-
end
|
12
|
-
|
13
|
-
let(:ex) { airbrake_exception_class.new(params) }
|
14
|
-
|
15
|
-
context "given an exception class, which supports #to_airbrake" do
|
16
|
-
context "and when #to_airbrake returns a non-Hash object" do
|
17
|
-
let(:airbrake_exception_class) do
|
18
|
-
Class.new(AirbrakeTestError) do
|
19
|
-
def to_airbrake
|
20
|
-
Object.new
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
it "rescues the error, logs it and doesn't modify the payload" do
|
26
|
-
out = StringIO.new
|
27
|
-
config = Airbrake::Config.new(logger: Logger.new(out))
|
28
|
-
notice = nil
|
29
|
-
|
30
|
-
expect { notice = described_class.new(config, ex) }.not_to raise_error
|
31
|
-
expect(out.string).to match(/#to_airbrake failed:.+Object.+must be a Hash/)
|
32
|
-
expect(notice[:params]).to be_empty
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
context "and when #to_airbrake errors out" do
|
37
|
-
let(:airbrake_exception_class) do
|
38
|
-
Class.new(AirbrakeTestError) do
|
39
|
-
def to_airbrake
|
40
|
-
1 / 0
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
it "rescues the error, logs it and doesn't modify the payload" do
|
46
|
-
out = StringIO.new
|
47
|
-
config = Airbrake::Config.new(logger: Logger.new(out))
|
48
|
-
notice = nil
|
49
|
-
|
50
|
-
expect { notice = described_class.new(config, ex) }.not_to raise_error
|
51
|
-
expect(out.string).to match(/#to_airbrake failed: ZeroDivisionError/)
|
52
|
-
expect(notice[:params]).to be_empty
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
context "and when #to_airbrake succeeds" do
|
57
|
-
let(:airbrake_exception_class) do
|
58
|
-
Class.new(AirbrakeTestError) do
|
59
|
-
def initialize(params)
|
60
|
-
@params = params
|
61
|
-
end
|
62
|
-
|
63
|
-
def to_airbrake
|
64
|
-
{ params: @params }
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
it "merges the parameters with the notice" do
|
70
|
-
notice = described_class.new(Airbrake::Config.new, ex)
|
71
|
-
expect(notice[:params]).to eq(params)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
8
|
describe "#to_json" do
|
78
9
|
context "app_version" do
|
79
10
|
context "when missing" do
|
@@ -99,6 +30,23 @@ RSpec.describe Airbrake::Notice do
|
|
99
30
|
end
|
100
31
|
end
|
101
32
|
|
33
|
+
context "when versions is empty" do
|
34
|
+
it "doesn't set the 'versions' payload" do
|
35
|
+
expect(notice.to_json).not_to match(
|
36
|
+
/"context":{"versions":{"dep":"1.2.3"}}/
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when versions is not empty" do
|
42
|
+
it "sets the 'versions' payload" do
|
43
|
+
notice[:context][:versions] = { 'dep' => '1.2.3' }
|
44
|
+
expect(notice.to_json).to match(
|
45
|
+
/"context":{.*"versions":{"dep":"1.2.3"}.*}/
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
102
50
|
context "truncation" do
|
103
51
|
shared_examples 'payloads' do |size, msg|
|
104
52
|
it msg do
|
@@ -189,6 +137,86 @@ RSpec.describe Airbrake::Notice do
|
|
189
137
|
end
|
190
138
|
end
|
191
139
|
|
140
|
+
context "given a closed IO object" do
|
141
|
+
context "and when it is not monkey-patched by ActiveSupport" do
|
142
|
+
it "is not getting truncated" do
|
143
|
+
notice[:params] = { obj: IO.new(0).tap(&:close) }
|
144
|
+
expect(notice.to_json).to match(/"obj":"#<IO:0x.+>"/)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "and when it is monkey-patched by ActiveSupport" do
|
149
|
+
# Instances of this class contain a closed IO object assigned to an
|
150
|
+
# instance variable. Normally, the JSON gem, which we depend on can
|
151
|
+
# parse closed IO objects. However, because ActiveSupport monkey-patches
|
152
|
+
# #to_json and calls #to_a on them, they raise IOError when we try to
|
153
|
+
# serialize them.
|
154
|
+
#
|
155
|
+
# @see https://goo.gl/0A3xNC
|
156
|
+
class ObjectWithIoIvars
|
157
|
+
def initialize
|
158
|
+
@bongo = Tempfile.new('bongo').tap(&:close)
|
159
|
+
end
|
160
|
+
|
161
|
+
# @raise [NotImplementedError] when inside a Rails environment
|
162
|
+
def to_json(*)
|
163
|
+
raise NotImplementedError
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# @see ObjectWithIoIvars
|
168
|
+
class ObjectWithNestedIoIvars
|
169
|
+
def initialize
|
170
|
+
@bish = ObjectWithIoIvars.new
|
171
|
+
end
|
172
|
+
|
173
|
+
# @see ObjectWithIoIvars#to_json
|
174
|
+
def to_json(*)
|
175
|
+
raise NotImplementedError
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context "and also when it's a closed Tempfile" do
|
180
|
+
it "doesn't fail" do
|
181
|
+
notice[:params] = { obj: Tempfile.new('bongo').tap(&:close) }
|
182
|
+
expect(notice.to_json).to match(/"obj":"#<(Temp)?file:0x.+>"/i)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "and also when it's an IO ivar" do
|
187
|
+
it "doesn't fail" do
|
188
|
+
notice[:params] = { obj: ObjectWithIoIvars.new }
|
189
|
+
expect(notice.to_json).to match(/"obj":".+ObjectWithIoIvars.+"/)
|
190
|
+
end
|
191
|
+
|
192
|
+
context "and when it's deeply nested inside a hash" do
|
193
|
+
it "doesn't fail" do
|
194
|
+
notice[:params] = { a: { b: { c: ObjectWithIoIvars.new } } }
|
195
|
+
expect(notice.to_json).to match(
|
196
|
+
/"params":{"a":{"b":{"c":".+ObjectWithIoIvars.+"}}.*}/
|
197
|
+
)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context "and when it's deeply nested inside an array" do
|
202
|
+
it "doesn't fail" do
|
203
|
+
notice[:params] = { a: [[ObjectWithIoIvars.new]] }
|
204
|
+
expect(notice.to_json).to match(
|
205
|
+
/"params":{"a":\[\[".+ObjectWithIoIvars.+"\]\].*}/
|
206
|
+
)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
context "and also when it's a non-IO ivar, which contains an IO ivar itself" do
|
212
|
+
it "doesn't fail" do
|
213
|
+
notice[:params] = { obj: ObjectWithNestedIoIvars.new }
|
214
|
+
expect(notice.to_json).to match(/"obj":".+ObjectWithNested.+"/)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
192
220
|
it "overwrites the 'notifier' payload with the default values" do
|
193
221
|
notice[:notifier] = { name: 'bingo', bango: 'bongo' }
|
194
222
|
|
@@ -209,6 +237,20 @@ RSpec.describe Airbrake::Notice do
|
|
209
237
|
expect(notice.to_json).
|
210
238
|
to match(%r|"environment":{"program_name":.+/rspec.*|)
|
211
239
|
end
|
240
|
+
|
241
|
+
it "contains errors" do
|
242
|
+
expect(notice.to_json).
|
243
|
+
to match(/"errors":\[{"type":"AirbrakeTestError","message":"App crash/)
|
244
|
+
end
|
245
|
+
|
246
|
+
it "contains a backtrace" do
|
247
|
+
expect(notice.to_json).
|
248
|
+
to match(%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|)
|
249
|
+
end
|
250
|
+
|
251
|
+
it "contains params" do
|
252
|
+
expect(notice.to_json).to match(/"params":{"bingo":"1"}/)
|
253
|
+
end
|
212
254
|
end
|
213
255
|
|
214
256
|
describe "#[]" do
|
data/spec/notifier_spec.rb
CHANGED
@@ -1,628 +1,437 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
# rubocop:disable Layout/DotPosition
|
3
4
|
RSpec.describe Airbrake::Notifier do
|
4
|
-
|
5
|
-
|
5
|
+
let(:user_params) do
|
6
|
+
{ project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
|
6
7
|
end
|
7
8
|
|
8
|
-
|
9
|
-
let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
|
10
|
-
let(:localhost) { 'http://localhost:8080' }
|
11
|
-
let(:endpoint) { "https://airbrake.io/api/v3/projects/#{project_id}/notices" }
|
12
|
-
|
13
|
-
let(:airbrake_params) do
|
14
|
-
{ project_id: project_id,
|
15
|
-
project_key: project_key,
|
16
|
-
logger: Logger.new(StringIO.new) }
|
17
|
-
end
|
18
|
-
|
19
|
-
let(:ex) { AirbrakeTestError.new }
|
20
|
-
|
21
|
-
before do
|
22
|
-
# rubocop:disable Metrics/LineLength
|
23
|
-
body = '{"id":"00054414-b147-6ffa-85d6-1524d83362a6","url":"http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6"}'
|
24
|
-
# rubocop:enable Metrics/LineLength
|
25
|
-
stub_request(:post, endpoint).to_return(status: 201, body: body)
|
26
|
-
@airbrake = described_class.new(airbrake_params)
|
27
|
-
end
|
9
|
+
subject { described_class.new(user_params) }
|
28
10
|
|
29
11
|
describe "#new" do
|
30
|
-
|
31
|
-
|
32
|
-
expect { described_class.new(project_key: project_key) }.
|
33
|
-
to raise_error(Airbrake::Error, ':project_id is required')
|
34
|
-
end
|
12
|
+
describe "default filter addition" do
|
13
|
+
before { allow_any_instance_of(Airbrake::FilterChain).to receive(:add_filter) }
|
35
14
|
|
36
|
-
|
37
|
-
|
38
|
-
|
15
|
+
it "appends the context filter" do
|
16
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
17
|
+
.with(instance_of(Airbrake::Filters::ContextFilter))
|
18
|
+
described_class.new(user_params)
|
39
19
|
end
|
40
20
|
|
41
|
-
|
42
|
-
|
43
|
-
|
21
|
+
it "appends the exception attributes filter" do
|
22
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
23
|
+
.with(instance_of(Airbrake::Filters::ExceptionAttributesFilter))
|
24
|
+
described_class.new(user_params)
|
44
25
|
end
|
45
|
-
end
|
46
|
-
|
47
|
-
context "when the argument is Airbrake::Config" do
|
48
|
-
it "uses it instead of the hash" do
|
49
|
-
airbrake = described_class.new(
|
50
|
-
Airbrake::Config.new(project_id: 123, project_key: '321')
|
51
|
-
)
|
52
|
-
config = airbrake.instance_variable_get(:@config)
|
53
|
-
expect(config.project_id).to eq(123)
|
54
|
-
expect(config.project_key).to eq('321')
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
26
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
|
65
|
-
)
|
66
|
-
)
|
67
|
-
end
|
68
|
-
|
69
|
-
describe "first argument" do
|
70
|
-
context "when it is a Notice" do
|
71
|
-
it "sends the argument" do
|
72
|
-
notice = @airbrake.build_notice(ex)
|
73
|
-
@airbrake.notify_sync(notice)
|
74
|
-
|
75
|
-
# rubocop:disable Metrics/LineLength
|
76
|
-
expected_body = %r|
|
77
|
-
{"errors":\[{"type":"AirbrakeTestError","message":"App\scrashed!","backtrace":\[
|
78
|
-
{"file":"[\w/-]+/spec/spec_helper.rb","line":\d+,"function":"<top\s\(required\)>"},
|
79
|
-
{"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"},
|
80
|
-
{"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"}
|
81
|
-
|x
|
82
|
-
# rubocop:enable Metrics/LineLength
|
83
|
-
|
84
|
-
expect(
|
85
|
-
a_request(:post, endpoint).
|
86
|
-
with(body: expected_body)
|
87
|
-
).to have_been_made.once
|
27
|
+
context "when user config has some whitelist keys" do
|
28
|
+
it "appends the whitelist filter" do
|
29
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
30
|
+
.with(instance_of(Airbrake::Filters::KeysWhitelist))
|
31
|
+
described_class.new(user_params.merge(whitelist_keys: ['foo']))
|
88
32
|
end
|
89
|
-
|
90
|
-
it "appends provided params to the notice" do
|
91
|
-
notice = @airbrake.build_notice(ex)
|
92
|
-
@airbrake.notify_sync(notice, bingo: 'bango')
|
93
|
-
|
94
|
-
expect_a_request_with_body(/"params":{.*"bingo":"bango".*}/)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
describe "request" do
|
100
|
-
before do
|
101
|
-
@airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish')
|
102
33
|
end
|
103
34
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
|
111
|
-
describe "headers" do
|
112
|
-
def expect_a_request_with_headers(headers)
|
113
|
-
expect(
|
114
|
-
a_request(:post, endpoint).
|
115
|
-
with(headers: headers)
|
116
|
-
).to have_been_made.once
|
117
|
-
end
|
118
|
-
|
119
|
-
it "POSTs JSON to Airbrake" do
|
120
|
-
expect_a_request_with_headers('Content-Type' => 'application/json')
|
121
|
-
end
|
122
|
-
|
123
|
-
it "sets User-Agent" do
|
124
|
-
ua = "airbrake-ruby/#{Airbrake::AIRBRAKE_RUBY_VERSION} Ruby/#{RUBY_VERSION}"
|
125
|
-
expect_a_request_with_headers('User-Agent' => ua)
|
35
|
+
context "when user config doesn't have any whitelist keys" do
|
36
|
+
it "doesn't append the whitelist filter" do
|
37
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
38
|
+
.with(instance_of(Airbrake::Filters::KeysWhitelist))
|
39
|
+
described_class.new(user_params)
|
126
40
|
end
|
127
41
|
end
|
128
42
|
|
129
|
-
|
130
|
-
it "
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
it "features 'context'" do
|
135
|
-
expect_a_request_with_body(/"context":{.*"os":"[\.\w-]+"/)
|
136
|
-
end
|
137
|
-
|
138
|
-
it "features 'errors'" do
|
139
|
-
expect_a_request_with_body(
|
140
|
-
/"errors":\[{"type":"AirbrakeTestError","message":"App crash/
|
141
|
-
)
|
142
|
-
end
|
143
|
-
|
144
|
-
it "features 'backtrace'" do
|
145
|
-
expect_a_request_with_body(
|
146
|
-
%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|
|
147
|
-
)
|
43
|
+
context "when user config has some blacklist keys" do
|
44
|
+
it "appends the blacklist filter" do
|
45
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
46
|
+
.with(instance_of(Airbrake::Filters::KeysBlacklist))
|
47
|
+
described_class.new(user_params.merge(blacklist_keys: ['bar']))
|
148
48
|
end
|
149
|
-
|
150
|
-
it "features 'params'" do
|
151
|
-
expect_a_request_with_body(
|
152
|
-
/"params":{"bingo":\["bango"\],"bongo":"bish".*}/
|
153
|
-
)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
describe "response body when it is" do
|
159
|
-
before do
|
160
|
-
@stdout = StringIO.new
|
161
|
-
params = {
|
162
|
-
logger: Logger.new(@stdout).tap { |l| l.level = Logger::DEBUG }
|
163
|
-
}
|
164
|
-
@airbrake = described_class.new(airbrake_params.merge(params))
|
165
49
|
end
|
166
50
|
|
167
|
-
|
168
|
-
it "
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
response = @airbrake.notify_sync(ex)
|
174
|
-
|
175
|
-
expect(@stdout.string).to match(expected_output)
|
176
|
-
expect(response).to be_a Hash
|
177
|
-
|
178
|
-
if response['message']
|
179
|
-
expect(response['message']).to satisfy do |error|
|
180
|
-
error.is_a?(Exception) || error.is_a?(String)
|
181
|
-
end
|
182
|
-
end
|
51
|
+
context "when user config doesn't have any blacklist keys" do
|
52
|
+
it "doesn't append the blacklist filter" do
|
53
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
54
|
+
.with(instance_of(Airbrake::Filters::KeysBlacklist))
|
55
|
+
described_class.new(user_params)
|
183
56
|
end
|
184
57
|
end
|
185
58
|
|
186
|
-
context "
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
context "an empty page" do
|
193
|
-
include_examples 'HTTP codes', 200, '',
|
194
|
-
/ERROR -- : .+ unexpected code \(200\). Body: \[EMPTY_BODY\]/
|
59
|
+
context "when user config specifies a root directory" do
|
60
|
+
it "appends the root directory filter" do
|
61
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
62
|
+
.with(instance_of(Airbrake::Filters::RootDirectoryFilter))
|
63
|
+
described_class.new(user_params.merge(root_directory: '/foo'))
|
64
|
+
end
|
195
65
|
end
|
196
66
|
|
197
|
-
context "
|
198
|
-
|
199
|
-
|
200
|
-
|
67
|
+
context "when user config doesn't specify a root directory" do
|
68
|
+
it "doesn't append the root directory filter" do
|
69
|
+
expect_any_instance_of(Airbrake::Config).to receive(:root_directory)
|
70
|
+
.and_return(nil)
|
71
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
72
|
+
.with(instance_of(Airbrake::Filters::RootDirectoryFilter))
|
73
|
+
described_class.new(user_params)
|
74
|
+
end
|
201
75
|
end
|
76
|
+
end
|
202
77
|
|
203
|
-
|
204
|
-
|
205
|
-
/ERROR -- : .+unexpected token at 'bingo.+'\)\. Body: bingo.+/
|
206
|
-
end
|
78
|
+
context "when user config doesn't contain a project id" do
|
79
|
+
let(:user_config) { { project_id: nil } }
|
207
80
|
|
208
|
-
|
209
|
-
|
210
|
-
|
81
|
+
it "raises error" do
|
82
|
+
expect { described_class.new(user_config) }
|
83
|
+
.to raise_error(Airbrake::Error, ':project_id is required')
|
211
84
|
end
|
85
|
+
end
|
212
86
|
|
213
|
-
|
214
|
-
|
215
|
-
'{"message":"Project not found or access denied."}',
|
216
|
-
/ERROR -- : .+ Project not found or access denied./
|
217
|
-
end
|
87
|
+
context "when user config doesn't contain a project key" do
|
88
|
+
let(:user_config) { { project_id: 1, project_key: nil } }
|
218
89
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
/ERROR -- : .+ Project is rate limited./
|
90
|
+
it "raises error" do
|
91
|
+
expect { described_class.new(user_config) }
|
92
|
+
.to raise_error(Airbrake::Error, ':project_key is required')
|
223
93
|
end
|
94
|
+
end
|
95
|
+
end
|
224
96
|
|
225
|
-
|
226
|
-
|
227
|
-
/ERROR -- : .+ IP is rate limited/
|
228
|
-
end
|
97
|
+
describe "#notify" do
|
98
|
+
let(:endpoint) { 'https://airbrake.io/api/v3/projects/1/notices' }
|
229
99
|
|
230
|
-
|
231
|
-
include_examples 'HTTP codes', 500, 'Internal Server Error',
|
232
|
-
/ERROR -- : .+ unexpected code \(500\). Body: Internal.+ Error/
|
233
|
-
end
|
100
|
+
subject { described_class.new(user_params) }
|
234
101
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
102
|
+
let(:body) do
|
103
|
+
{
|
104
|
+
'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
|
105
|
+
'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
|
106
|
+
}.to_json
|
239
107
|
end
|
240
108
|
|
241
|
-
|
242
|
-
it "logs the error when it occurs" do
|
243
|
-
stub_request(:post, endpoint).to_timeout
|
109
|
+
before { stub_request(:post, endpoint).to_return(status: 201, body: body) }
|
244
110
|
|
245
|
-
|
246
|
-
|
247
|
-
|
111
|
+
it "returns a promise" do
|
112
|
+
expect(subject.notify('ex')).to be_an(Airbrake::Promise)
|
113
|
+
sleep 1
|
114
|
+
end
|
248
115
|
|
249
|
-
|
116
|
+
it "refines the notice object" do
|
117
|
+
subject.add_filter { |n| n[:params] = { foo: 'bar' } }
|
118
|
+
notice = subject.build_notice('ex')
|
119
|
+
subject.notify(notice)
|
120
|
+
expect(notice[:params]).to eq(foo: 'bar')
|
121
|
+
sleep 1
|
122
|
+
end
|
250
123
|
|
251
|
-
|
252
|
-
|
124
|
+
context "when a notice is not ignored" do
|
125
|
+
it "yields the notice" do
|
126
|
+
expect { |b| subject.notify('ex', &b) }
|
127
|
+
.to yield_with_args(Airbrake::Notice)
|
128
|
+
sleep 1
|
253
129
|
end
|
254
130
|
end
|
255
131
|
|
256
|
-
|
257
|
-
|
258
|
-
it "works correctly" do
|
259
|
-
@airbrake.notify_sync(ex, unicode: "ü ö ä Ä Ü Ö ß привет €25.00 한글")
|
132
|
+
context "when a notice is ignored" do
|
133
|
+
before { subject.add_filter(&:ignore!) }
|
260
134
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
).to have_been_made.once
|
265
|
-
end
|
135
|
+
it "doesn't yield the notice" do
|
136
|
+
expect { |b| subject.notify('ex', &b) }
|
137
|
+
.not_to yield_with_args(Airbrake::Notice)
|
266
138
|
end
|
267
139
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
@airbrake.notify_sync('bingo', bongo: "bango\xAE")
|
272
|
-
end.not_to raise_error
|
273
|
-
end
|
274
|
-
|
275
|
-
it "doesn't raise error when string has valid encoding, but invalid characters" do
|
276
|
-
# Shenanigans to get a bad ASCII-8BIT string. Direct conversion raises error.
|
277
|
-
encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
|
278
|
-
bad_string = Base64.decode64(encoded)
|
279
|
-
|
280
|
-
expect do
|
281
|
-
@airbrake.notify_sync('bingo', bongo: bad_string)
|
282
|
-
end.not_to raise_error
|
283
|
-
end
|
140
|
+
it "returns a rejected promise" do
|
141
|
+
value = subject.notify('ex').value
|
142
|
+
expect(value['error']).to match(/was marked as ignored/)
|
284
143
|
end
|
285
144
|
end
|
286
145
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
expect(
|
293
|
-
a_request(:post, endpoint).with(body: /"bingo":"#<IO:0x.+>"/)
|
294
|
-
).to have_been_made.once
|
295
|
-
end
|
146
|
+
context "when async sender has workers" do
|
147
|
+
it "sends an exception asynchronously" do
|
148
|
+
expect_any_instance_of(Airbrake::AsyncSender).to receive(:send)
|
149
|
+
subject.notify('foo', bingo: 'bango')
|
296
150
|
end
|
151
|
+
end
|
297
152
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
#
|
305
|
-
# @see https://goo.gl/0A3xNC
|
306
|
-
class ObjectWithIoIvars
|
307
|
-
def initialize
|
308
|
-
@bongo = Tempfile.new('bongo').tap(&:close)
|
309
|
-
end
|
310
|
-
|
311
|
-
# @raise [NotImplementedError] when inside a Rails environment
|
312
|
-
def to_json(*)
|
313
|
-
raise NotImplementedError
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
##
|
318
|
-
# @see ObjectWithIoIvars
|
319
|
-
class ObjectWithNestedIoIvars
|
320
|
-
def initialize
|
321
|
-
@bish = ObjectWithIoIvars.new
|
322
|
-
end
|
323
|
-
|
324
|
-
# @see ObjectWithIoIvars#to_json
|
325
|
-
def to_json(*)
|
326
|
-
raise NotImplementedError
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
shared_examples 'truncation' do |params, expected|
|
331
|
-
it "filters it out" do
|
332
|
-
@airbrake.notify_sync(ex, params)
|
333
|
-
|
334
|
-
expect(
|
335
|
-
a_request(:post, endpoint).with(body: expected)
|
336
|
-
).to have_been_made.once
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
context "which is an instance of" do
|
341
|
-
context "Tempfile" do
|
342
|
-
params = { bango: Tempfile.new('bongo').tap(&:close) }
|
343
|
-
include_examples 'truncation', params, /"bango":"#<(Temp)?file:0x.+>"/i
|
344
|
-
end
|
345
|
-
|
346
|
-
context "a non-IO class but with" do
|
347
|
-
context "IO ivars" do
|
348
|
-
params = { bongo: ObjectWithIoIvars.new }
|
349
|
-
include_examples 'truncation', params, /"bongo":".+ObjectWithIoIvars.+"/
|
350
|
-
end
|
351
|
-
|
352
|
-
context "a non-IO ivar, which contains an IO ivar itself" do
|
353
|
-
params = { bish: ObjectWithNestedIoIvars.new }
|
354
|
-
include_examples 'truncation', params, /"bish":".+ObjectWithNested.+"/
|
355
|
-
end
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
context "which is deeply nested inside a hash" do
|
360
|
-
params = { bingo: { bango: { bongo: ObjectWithIoIvars.new } } }
|
361
|
-
include_examples(
|
362
|
-
'truncation',
|
363
|
-
params,
|
364
|
-
/"params":{"bingo":{"bango":{"bongo":".+ObjectWithIoIvars.+"}}.*}/
|
365
|
-
)
|
366
|
-
end
|
367
|
-
|
368
|
-
context "which is deeply nested inside an array" do
|
369
|
-
params = { bingo: [[ObjectWithIoIvars.new]] }
|
370
|
-
include_examples(
|
371
|
-
'truncation',
|
372
|
-
params,
|
373
|
-
/"params":{"bingo":\[\[".+ObjectWithIoIvars.+"\]\].*}/
|
374
|
-
)
|
375
|
-
end
|
153
|
+
context "when async sender doesn't have workers" do
|
154
|
+
it "sends an exception synchronously" do
|
155
|
+
expect_any_instance_of(Airbrake::AsyncSender)
|
156
|
+
.to receive(:has_workers?).and_return(false)
|
157
|
+
expect_any_instance_of(Airbrake::SyncSender).to receive(:send)
|
158
|
+
subject.notify('foo', bingo: 'bango')
|
376
159
|
end
|
377
160
|
end
|
378
161
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
end
|
162
|
+
context "when the provided environment is ignored" do
|
163
|
+
subject do
|
164
|
+
described_class.new(
|
165
|
+
user_params.merge(environment: 'test', ignore_environments: ['test'])
|
166
|
+
)
|
385
167
|
end
|
386
168
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
@airbrake.notify_sync(ex) { |n| n[:params][:bingo] = :bango }
|
391
|
-
expect(
|
392
|
-
a_request(:post, endpoint).
|
393
|
-
with(body: /params":{.*"bingo":"bango".*}/)
|
394
|
-
).not_to have_been_made
|
395
|
-
end
|
169
|
+
it "doesn't send an notice" do
|
170
|
+
expect_any_instance_of(Airbrake::AsyncSender).not_to receive(:send)
|
171
|
+
subject.notify('foo', bingo: 'bango')
|
396
172
|
end
|
397
173
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
expect(a_request(:post, endpoint)).not_to have_been_made
|
402
|
-
end
|
174
|
+
it "returns a rejected promise" do
|
175
|
+
promise = subject.notify('foo', bingo: 'bango')
|
176
|
+
expect(promise.value).to eq('error' => "The 'test' environment is ignored")
|
403
177
|
end
|
404
178
|
end
|
405
179
|
end
|
406
180
|
|
407
|
-
describe "#
|
408
|
-
|
409
|
-
@airbrake.notify(ex, bingo: 'bango')
|
410
|
-
|
411
|
-
sleep 1
|
181
|
+
describe "#notify_sync" do
|
182
|
+
let(:endpoint) { 'https://airbrake.io/api/v3/projects/1/notices' }
|
412
183
|
|
413
|
-
|
184
|
+
let(:user_params) do
|
185
|
+
{ project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
|
414
186
|
end
|
415
187
|
|
416
|
-
|
417
|
-
|
418
|
-
|
188
|
+
let(:body) do
|
189
|
+
{
|
190
|
+
'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
|
191
|
+
'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
|
192
|
+
}
|
419
193
|
end
|
420
194
|
|
421
|
-
|
422
|
-
out = StringIO.new
|
423
|
-
notifier = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
|
424
|
-
async_sender = notifier.instance_variable_get(:@async_sender)
|
195
|
+
before { stub_request(:post, endpoint).to_return(status: 201, body: body.to_json) }
|
425
196
|
|
426
|
-
|
427
|
-
|
428
|
-
sleep 1
|
429
|
-
expect(async_sender).not_to have_workers
|
430
|
-
|
431
|
-
notifier.notify('bango')
|
432
|
-
expect(out.string).to match(/falling back to sync delivery/)
|
433
|
-
|
434
|
-
notifier.close
|
197
|
+
it "returns a reponse hash" do
|
198
|
+
expect(subject.notify_sync('ex')).to eq(body)
|
435
199
|
end
|
436
200
|
|
437
|
-
it "
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
sleep 1
|
443
|
-
expect(out.string).not_to match(/falling back to sync delivery/)
|
444
|
-
expect_a_request_with_body(/"bingo":"bango"/)
|
445
|
-
|
446
|
-
pid = fork do
|
447
|
-
expect(notifier.instance_variable_get(:@async_sender)).to have_workers
|
448
|
-
notifier.notify('bango', bongo: 'bish')
|
449
|
-
sleep 1
|
450
|
-
expect(out.string).not_to match(/falling back to sync delivery/)
|
451
|
-
expect_a_request_with_body(/"bingo":"bango"/)
|
452
|
-
end
|
453
|
-
|
454
|
-
Process.wait(pid)
|
455
|
-
notifier.close
|
456
|
-
expect(notifier.instance_variable_get(:@async_sender)).not_to have_workers
|
201
|
+
it "refines the notice object" do
|
202
|
+
subject.add_filter { |n| n[:params] = { foo: 'bar' } }
|
203
|
+
notice = subject.build_notice('ex')
|
204
|
+
subject.notify_sync(notice)
|
205
|
+
expect(notice[:params]).to eq(foo: 'bar')
|
457
206
|
end
|
458
|
-
end
|
459
|
-
|
460
|
-
describe "#add_filter" do
|
461
|
-
it "filters notices" do
|
462
|
-
@airbrake.add_filter do |notice|
|
463
|
-
notice[:params][:password] = '[Filtered]'.freeze if notice[:params][:password]
|
464
|
-
end
|
465
|
-
|
466
|
-
@airbrake.notify_sync(ex, password: 's4kr4t')
|
467
207
|
|
208
|
+
it "sends an exception synchronously" do
|
209
|
+
subject.notify_sync('foo', bingo: 'bango')
|
468
210
|
expect(
|
469
|
-
a_request(:post, endpoint).
|
470
|
-
|
211
|
+
a_request(:post, endpoint).with(
|
212
|
+
body: /"params":{.*"bingo":"bango".*}/
|
213
|
+
)
|
471
214
|
).to have_been_made.once
|
472
215
|
end
|
473
216
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
end
|
217
|
+
context "when a notice is not ignored" do
|
218
|
+
it "yields the notice" do
|
219
|
+
expect { |b| subject.notify_sync('ex', &b) }
|
220
|
+
.to yield_with_args(Airbrake::Notice)
|
479
221
|
end
|
480
|
-
|
481
|
-
@airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish', bash: 'bosh')
|
482
|
-
|
483
|
-
# rubocop:disable Metrics/LineLength
|
484
|
-
body = /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]","bash":"\[Filtered\]".*}/
|
485
|
-
# rubocop:enable Metrics/LineLength
|
486
|
-
|
487
|
-
expect(
|
488
|
-
a_request(:post, endpoint).
|
489
|
-
with(body: body)
|
490
|
-
).to have_been_made.once
|
491
222
|
end
|
492
223
|
|
493
|
-
|
494
|
-
|
224
|
+
context "when a notice is ignored" do
|
225
|
+
before { subject.add_filter(&:ignore!) }
|
495
226
|
|
496
|
-
|
227
|
+
it "doesn't yield the notice" do
|
228
|
+
expect { |b| subject.notify_sync('ex', &b) }
|
229
|
+
.not_to yield_with_args(Airbrake::Notice)
|
230
|
+
end
|
497
231
|
|
498
|
-
|
232
|
+
it "returns an error hash" do
|
233
|
+
response = subject.notify_sync('ex')
|
234
|
+
expect(response['error']).to match(/was marked as ignored/)
|
235
|
+
end
|
499
236
|
end
|
500
237
|
|
501
|
-
|
502
|
-
|
503
|
-
|
238
|
+
context "when the provided environment is ignored" do
|
239
|
+
subject do
|
240
|
+
described_class.new(
|
241
|
+
user_params.merge(environment: 'test', ignore_environments: ['test'])
|
242
|
+
)
|
504
243
|
end
|
505
244
|
|
506
|
-
|
507
|
-
|
245
|
+
it "doesn't send an notice" do
|
246
|
+
expect_any_instance_of(Airbrake::SyncSender).not_to receive(:send)
|
247
|
+
subject.notify_sync('foo', bingo: 'bango')
|
248
|
+
end
|
508
249
|
|
509
|
-
|
510
|
-
|
250
|
+
it "returns an error hash" do
|
251
|
+
expect(subject.notify_sync('foo'))
|
252
|
+
.to eq('error' => "The 'test' environment is ignored")
|
253
|
+
end
|
511
254
|
end
|
255
|
+
end
|
512
256
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
257
|
+
describe "#add_filter" do
|
258
|
+
context "given a block" do
|
259
|
+
it "appends a new filter to the filter chain" do
|
260
|
+
notifier = subject
|
261
|
+
b = proc {}
|
262
|
+
expect_any_instance_of(Airbrake::FilterChain)
|
263
|
+
.to receive(:add_filter) { |*args| expect(args.last).to be(b) }
|
264
|
+
notifier.add_filter(&b)
|
518
265
|
end
|
266
|
+
end
|
519
267
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
268
|
+
context "given a class" do
|
269
|
+
it "appends a new filter to the filter chain" do
|
270
|
+
notifier = subject
|
271
|
+
klass = Class.new
|
272
|
+
expect_any_instance_of(Airbrake::FilterChain)
|
273
|
+
.to receive(:add_filter).with(klass)
|
274
|
+
notifier.add_filter(klass)
|
275
|
+
end
|
525
276
|
end
|
526
277
|
end
|
527
278
|
|
528
279
|
describe "#build_notice" do
|
529
|
-
|
530
|
-
|
280
|
+
context "when given exception is another notice" do
|
281
|
+
it "merges params with the notice" do
|
282
|
+
notice = subject.build_notice('ex')
|
283
|
+
other = subject.build_notice(notice, foo: 'bar')
|
284
|
+
expect(other[:params]).to eq(foo: 'bar')
|
285
|
+
end
|
286
|
+
|
287
|
+
it "it returns the provided notice" do
|
288
|
+
notice = subject.build_notice('ex')
|
289
|
+
other = subject.build_notice(notice, foo: 'bar')
|
290
|
+
expect(other).to eq(notice)
|
291
|
+
end
|
531
292
|
end
|
532
293
|
|
533
|
-
context "given
|
294
|
+
context "when given exception is an Exception" do
|
534
295
|
it "prevents mutation of passed-in params hash" do
|
535
|
-
params = {
|
536
|
-
notice =
|
537
|
-
notice[:params][:
|
538
|
-
expect(params).to eq(
|
296
|
+
params = { immutable: true }
|
297
|
+
notice = subject.build_notice('ex', params)
|
298
|
+
notice[:params][:mutable] = true
|
299
|
+
expect(params).to eq(immutable: true)
|
300
|
+
end
|
301
|
+
|
302
|
+
context "and also when it doesn't have own backtrace" do
|
303
|
+
context "and when the generated backtrace consists only of library frames" do
|
304
|
+
it "returns the full generated backtrace" do
|
305
|
+
backtrace = [
|
306
|
+
"/lib/airbrake-ruby/a.rb:84:in `build_notice'",
|
307
|
+
"/lib/airbrake-ruby/b.rb:124:in `send_notice'"
|
308
|
+
]
|
309
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
310
|
+
|
311
|
+
notice = subject.build_notice(Exception.new)
|
312
|
+
|
313
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
314
|
+
[
|
315
|
+
{ file: '/lib/airbrake-ruby/a.rb', line: 84, function: 'build_notice' },
|
316
|
+
{ file: '/lib/airbrake-ruby/b.rb', line: 124, function: 'send_notice' }
|
317
|
+
]
|
318
|
+
)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context "and when the generated backtrace consists of mixed frames" do
|
323
|
+
it "returns the filtered backtrace" do
|
324
|
+
backtrace = [
|
325
|
+
"/airbrake-ruby/lib/airbrake-ruby/a.rb:84:in `b'",
|
326
|
+
"/airbrake-ruby/lib/foo/b.rb:84:in `build'",
|
327
|
+
"/airbrake-ruby/lib/bar/c.rb:124:in `send'"
|
328
|
+
]
|
329
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
330
|
+
|
331
|
+
notice = subject.build_notice(Exception.new)
|
332
|
+
|
333
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
334
|
+
[
|
335
|
+
{ file: '/airbrake-ruby/lib/foo/b.rb', line: 84, function: 'build' },
|
336
|
+
{ file: '/airbrake-ruby/lib/bar/c.rb', line: 124, function: 'send' }
|
337
|
+
]
|
338
|
+
)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# TODO: this seems to be bugged. Fix later.
|
345
|
+
context "when given exception is a Java exception", skip: true do
|
346
|
+
before do
|
347
|
+
expect(Airbrake::Backtrace).to receive(:java_exception?).and_return(true)
|
539
348
|
end
|
540
349
|
|
541
|
-
it "
|
350
|
+
it "automatically generates the backtrace" do
|
542
351
|
backtrace = [
|
543
|
-
"/
|
544
|
-
"/
|
545
|
-
"/
|
352
|
+
"org/jruby/RubyKernel.java:998:in `eval'",
|
353
|
+
"/ruby/stdlib/irb/workspace.rb:87:in `evaluate'",
|
354
|
+
"/ruby/stdlib/irb.rb:489:in `block in eval_input'"
|
546
355
|
]
|
356
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
357
|
+
|
358
|
+
notice = subject.build_notice(Exception.new)
|
547
359
|
|
548
360
|
# rubocop:disable Metrics/LineLength
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
361
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
362
|
+
[
|
363
|
+
{ file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
|
364
|
+
{ file: '/ruby/stdlib/irb/workspace.rb', line: 87, function: 'evaluate' },
|
365
|
+
{ file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' }
|
366
|
+
]
|
367
|
+
)
|
554
368
|
# rubocop:enable Metrics/LineLength
|
555
|
-
|
556
|
-
allow(Kernel).to receive(:caller).and_return(backtrace)
|
557
|
-
|
558
|
-
notice = @airbrake.build_notice('bingo')
|
559
|
-
expect(notice[:errors][0][:backtrace]).to eq(parsed_backtrace)
|
560
369
|
end
|
561
370
|
end
|
562
|
-
end
|
563
|
-
|
564
|
-
describe "#close" do
|
565
|
-
context "when using #notify on a closed notifier" do
|
566
|
-
it "raises error" do
|
567
|
-
notifier = described_class.new(airbrake_params)
|
568
|
-
notifier.close
|
569
371
|
|
570
|
-
|
571
|
-
|
372
|
+
context "when async sender is closed" do
|
373
|
+
before do
|
374
|
+
expect_any_instance_of(Airbrake::AsyncSender)
|
375
|
+
.to receive(:closed?).and_return(true)
|
572
376
|
end
|
573
|
-
end
|
574
377
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
378
|
+
it "raises error" do
|
379
|
+
expect { subject.build_notice(Exception.new) }.to raise_error(
|
380
|
+
Airbrake::Error,
|
381
|
+
'attempted to build Exception with closed Airbrake instance'
|
382
|
+
)
|
580
383
|
end
|
581
384
|
end
|
582
385
|
end
|
583
386
|
|
584
|
-
describe "#
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
before do
|
590
|
-
stub_request(:post, deploy_endpoint).to_return(status: 201, body: '{"id":"123"}')
|
387
|
+
describe "#close" do
|
388
|
+
it "sends the close message to async sender" do
|
389
|
+
expect_any_instance_of(Airbrake::AsyncSender).to receive(:close)
|
390
|
+
subject.close
|
591
391
|
end
|
392
|
+
end
|
592
393
|
|
593
|
-
|
594
|
-
|
595
|
-
|
394
|
+
describe "#create_deploy" do
|
395
|
+
it "returns a promise" do
|
396
|
+
stub_request(:post, "https://airbrake.io/api/v4/projects/1/deploys?key=abc")
|
397
|
+
.to_return(status: 201, body: '')
|
398
|
+
expect(subject.create_deploy({})).to be_an(Airbrake::Promise)
|
596
399
|
end
|
597
400
|
|
598
|
-
context "when
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
401
|
+
context "when environment is configured" do
|
402
|
+
it "prefers the passed environment to the config env" do
|
403
|
+
expect_any_instance_of(Airbrake::SyncSender).to receive(:send).with(
|
404
|
+
{ environment: 'barenv' },
|
405
|
+
instance_of(Airbrake::Promise),
|
406
|
+
URI('https://airbrake.io/api/v4/projects/1/deploys?key=abc')
|
407
|
+
)
|
408
|
+
described_class.new(
|
409
|
+
user_params.merge(environment: 'fooenv')
|
410
|
+
).create_deploy(environment: 'barenv')
|
603
411
|
end
|
412
|
+
end
|
604
413
|
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
414
|
+
context "when environment is not configured" do
|
415
|
+
it "sets the environment from the config" do
|
416
|
+
expect_any_instance_of(Airbrake::SyncSender).to receive(:send).with(
|
417
|
+
{ environment: 'fooenv' },
|
418
|
+
instance_of(Airbrake::Promise),
|
419
|
+
URI('https://airbrake.io/api/v4/projects/1/deploys?key=abc')
|
420
|
+
)
|
421
|
+
subject.create_deploy(environment: 'fooenv')
|
609
422
|
end
|
610
423
|
end
|
611
424
|
end
|
612
425
|
|
613
426
|
describe "#configured?" do
|
614
|
-
subject { described_class.new(airbrake_params) }
|
615
427
|
it { is_expected.to be_configured }
|
616
428
|
end
|
617
429
|
|
618
430
|
describe "#merge_context" do
|
619
431
|
it "merges the provided context with the notice object" do
|
620
|
-
|
621
|
-
|
622
|
-
expect(
|
623
|
-
a_request(:post, endpoint).
|
624
|
-
with(body: /"params":{"airbrake_context":{"apples":"oranges"}/)
|
625
|
-
).to have_been_made.once
|
432
|
+
expect_any_instance_of(Hash).to receive(:merge!).with(apples: 'oranges')
|
433
|
+
subject.merge_context(apples: 'oranges')
|
626
434
|
end
|
627
435
|
end
|
628
436
|
end
|
437
|
+
# rubocop:enable Layout/DotPosition
|