airbrake-ruby 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +515 -0
  3. data/lib/airbrake-ruby/async_sender.rb +80 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +54 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +125 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +155 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +54 -0
  38. data/lib/airbrake-ruby/request.rb +46 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/thread_pool.rb +128 -0
  45. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +115 -0
  48. data/lib/airbrake-ruby/version.rb +6 -0
  49. data/spec/airbrake_spec.rb +324 -0
  50. data/spec/async_sender_spec.rb +72 -0
  51. data/spec/backtrace_spec.rb +427 -0
  52. data/spec/benchmark_spec.rb +33 -0
  53. data/spec/code_hunk_spec.rb +115 -0
  54. data/spec/config/validator_spec.rb +184 -0
  55. data/spec/config_spec.rb +154 -0
  56. data/spec/deploy_notifier_spec.rb +48 -0
  57. data/spec/file_cache_spec.rb +34 -0
  58. data/spec/filter_chain_spec.rb +92 -0
  59. data/spec/filters/context_filter_spec.rb +23 -0
  60. data/spec/filters/dependency_filter_spec.rb +12 -0
  61. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  62. data/spec/filters/gem_root_filter_spec.rb +41 -0
  63. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  64. data/spec/filters/git_repository_filter.rb +61 -0
  65. data/spec/filters/git_revision_filter_spec.rb +126 -0
  66. data/spec/filters/keys_blacklist_spec.rb +225 -0
  67. data/spec/filters/keys_whitelist_spec.rb +194 -0
  68. data/spec/filters/root_directory_filter_spec.rb +39 -0
  69. data/spec/filters/sql_filter_spec.rb +262 -0
  70. data/spec/filters/system_exit_filter_spec.rb +14 -0
  71. data/spec/filters/thread_filter_spec.rb +277 -0
  72. data/spec/fixtures/notroot.txt +7 -0
  73. data/spec/fixtures/project_root/code.rb +221 -0
  74. data/spec/fixtures/project_root/empty_file.rb +0 -0
  75. data/spec/fixtures/project_root/long_line.txt +1 -0
  76. data/spec/fixtures/project_root/short_file.rb +3 -0
  77. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  78. data/spec/helpers.rb +9 -0
  79. data/spec/ignorable_spec.rb +14 -0
  80. data/spec/inspectable_spec.rb +45 -0
  81. data/spec/monotonic_time_spec.rb +12 -0
  82. data/spec/nested_exception_spec.rb +73 -0
  83. data/spec/notice_notifier/options_spec.rb +259 -0
  84. data/spec/notice_notifier_spec.rb +356 -0
  85. data/spec/notice_spec.rb +296 -0
  86. data/spec/performance_breakdown_spec.rb +12 -0
  87. data/spec/performance_notifier_spec.rb +491 -0
  88. data/spec/promise_spec.rb +197 -0
  89. data/spec/query_spec.rb +11 -0
  90. data/spec/request_spec.rb +11 -0
  91. data/spec/response_spec.rb +88 -0
  92. data/spec/spec_helper.rb +100 -0
  93. data/spec/stashable_spec.rb +23 -0
  94. data/spec/stat_spec.rb +47 -0
  95. data/spec/sync_sender_spec.rb +133 -0
  96. data/spec/tdigest_spec.rb +230 -0
  97. data/spec/thread_pool_spec.rb +158 -0
  98. data/spec/time_truncate_spec.rb +13 -0
  99. data/spec/timed_trace_spec.rb +125 -0
  100. data/spec/truncator_spec.rb +238 -0
  101. metadata +216 -0
@@ -0,0 +1,356 @@
1
+ RSpec.describe Airbrake::NoticeNotifier do
2
+ before do
3
+ Airbrake::Config.instance = Airbrake::Config.new(
4
+ project_id: 1,
5
+ project_key: 'abc',
6
+ logger: Logger.new('/dev/null'),
7
+ performance_stats: true
8
+ )
9
+ end
10
+
11
+ describe "#new" do
12
+ describe "default filter addition" do
13
+ before { allow_any_instance_of(Airbrake::FilterChain).to receive(:add_filter) }
14
+
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
+ subject
19
+ end
20
+
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
+ subject
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "#notify" do
30
+ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
31
+
32
+ let(:body) do
33
+ {
34
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
35
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
36
+ }.to_json
37
+ end
38
+
39
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body) }
40
+
41
+ it "returns a promise" do
42
+ expect(subject.notify('ex')).to be_an(Airbrake::Promise)
43
+ sleep 1
44
+ end
45
+
46
+ it "refines the notice object" do
47
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
48
+ notice = subject.build_notice('ex')
49
+ subject.notify(notice)
50
+ expect(notice[:params]).to eq(foo: 'bar')
51
+ sleep 1
52
+ end
53
+
54
+ context "when config is invalid" do
55
+ before { Airbrake::Config.instance.merge(project_id: nil) }
56
+
57
+ it "returns a rejected promise" do
58
+ promise = subject.notify({})
59
+ expect(promise).to be_rejected
60
+ end
61
+ end
62
+
63
+ context "when a notice is not ignored" do
64
+ it "yields the notice" do
65
+ expect { |b| subject.notify('ex', &b) }
66
+ .to yield_with_args(Airbrake::Notice)
67
+ sleep 1
68
+ end
69
+ end
70
+
71
+ context "when a notice is ignored via a filter" do
72
+ before { subject.add_filter(&:ignore!) }
73
+
74
+ it "yields the notice" do
75
+ expect { |b| subject.notify('ex', &b) }
76
+ .to yield_with_args(Airbrake::Notice)
77
+ end
78
+
79
+ it "returns a rejected promise" do
80
+ value = subject.notify('ex').value
81
+ expect(value['error']).to match(/was marked as ignored/)
82
+ end
83
+ end
84
+
85
+ context "when a notice is ignored via an inline filter" do
86
+ before { subject.add_filter { raise AirbrakeTestError } }
87
+
88
+ it "doesn't invoke regular filters" do
89
+ expect { subject.notify('ex', &:ignore!) }.not_to raise_error
90
+ end
91
+ end
92
+
93
+ context "when async sender has workers" do
94
+ it "sends an exception asynchronously" do
95
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:send)
96
+ subject.notify('foo', bingo: 'bango')
97
+ end
98
+ end
99
+
100
+ context "when async sender doesn't have workers" do
101
+ it "sends an exception synchronously" do
102
+ expect_any_instance_of(Airbrake::AsyncSender)
103
+ .to receive(:has_workers?).and_return(false)
104
+ expect_any_instance_of(Airbrake::SyncSender).to receive(:send)
105
+ subject.notify('foo', bingo: 'bango')
106
+ end
107
+ end
108
+
109
+ context "when the provided environment is ignored" do
110
+ before do
111
+ Airbrake::Config.instance.merge(
112
+ environment: 'test',
113
+ ignore_environments: %w[test]
114
+ )
115
+ end
116
+
117
+ it "doesn't send an notice" do
118
+ expect_any_instance_of(Airbrake::AsyncSender).not_to receive(:send)
119
+ subject.notify('foo', bingo: 'bango')
120
+ end
121
+
122
+ it "returns a rejected promise" do
123
+ promise = subject.notify('foo', bingo: 'bango')
124
+ expect(promise.value).to eq('error' => "current environment 'test' is ignored")
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#notify_sync" do
130
+ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
131
+
132
+ let(:body) do
133
+ {
134
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
135
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
136
+ }
137
+ end
138
+
139
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body.to_json) }
140
+
141
+ it "returns a reponse hash" do
142
+ expect(subject.notify_sync('ex')).to eq(body)
143
+ end
144
+
145
+ it "refines the notice object" do
146
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
147
+ notice = subject.build_notice('ex')
148
+ subject.notify_sync(notice)
149
+ expect(notice[:params]).to eq(foo: 'bar')
150
+ end
151
+
152
+ it "sends an exception synchronously" do
153
+ subject.notify_sync('foo', bingo: 'bango')
154
+ expect(
155
+ a_request(:post, endpoint).with(
156
+ body: /"params":{.*"bingo":"bango".*}/
157
+ )
158
+ ).to have_been_made.once
159
+ end
160
+
161
+ context "when a notice is not ignored" do
162
+ it "yields the notice" do
163
+ expect { |b| subject.notify_sync('ex', &b) }
164
+ .to yield_with_args(Airbrake::Notice)
165
+ end
166
+ end
167
+
168
+ context "when a notice is ignored via a filter" do
169
+ before { subject.add_filter(&:ignore!) }
170
+
171
+ it "yields the notice" do
172
+ expect { |b| subject.notify_sync('ex', &b) }
173
+ .to yield_with_args(Airbrake::Notice)
174
+ end
175
+
176
+ it "returns an error hash" do
177
+ response = subject.notify_sync('ex')
178
+ expect(response['error']).to match(/was marked as ignored/)
179
+ end
180
+ end
181
+
182
+ context "when a notice is ignored via an inline filter" do
183
+ before { subject.add_filter { raise AirbrakeTestError } }
184
+
185
+ it "doesn't invoke regular filters" do
186
+ expect { subject.notify('ex', &:ignore!) }.not_to raise_error
187
+ end
188
+ end
189
+
190
+ context "when the provided environment is ignored" do
191
+ before do
192
+ Airbrake::Config.instance.merge(
193
+ environment: 'test', ignore_environments: %w[test]
194
+ )
195
+ end
196
+
197
+ it "doesn't send an notice" do
198
+ expect_any_instance_of(Airbrake::SyncSender).not_to receive(:send)
199
+ subject.notify_sync('foo', bingo: 'bango')
200
+ end
201
+
202
+ it "returns an error hash" do
203
+ expect(subject.notify_sync('foo'))
204
+ .to eq('error' => "current environment 'test' is ignored")
205
+ end
206
+ end
207
+ end
208
+
209
+ describe "#add_filter" do
210
+ context "given a block" do
211
+ it "appends a new filter to the filter chain" do
212
+ notifier = subject
213
+ b = proc {}
214
+ expect_any_instance_of(Airbrake::FilterChain)
215
+ .to receive(:add_filter) { |*args| expect(args.last).to be(b) }
216
+ notifier.add_filter(&b)
217
+ end
218
+ end
219
+
220
+ context "given a class" do
221
+ it "appends a new filter to the filter chain" do
222
+ notifier = subject
223
+ klass = Class.new
224
+ expect_any_instance_of(Airbrake::FilterChain)
225
+ .to receive(:add_filter).with(klass)
226
+ notifier.add_filter(klass)
227
+ end
228
+ end
229
+ end
230
+
231
+ describe "#build_notice" do
232
+ context "when given exception is another notice" do
233
+ it "merges params with the notice" do
234
+ notice = subject.build_notice('ex')
235
+ other = subject.build_notice(notice, foo: 'bar')
236
+ expect(other[:params]).to eq(foo: 'bar')
237
+ end
238
+
239
+ it "it returns the provided notice" do
240
+ notice = subject.build_notice('ex')
241
+ other = subject.build_notice(notice, foo: 'bar')
242
+ expect(other).to eq(notice)
243
+ end
244
+ end
245
+
246
+ context "when given exception is an Exception" do
247
+ it "prevents mutation of passed-in params hash" do
248
+ params = { immutable: true }
249
+ notice = subject.build_notice('ex', params)
250
+ notice[:params][:mutable] = true
251
+ expect(params).to eq(immutable: true)
252
+ end
253
+
254
+ context "and also when it doesn't have own backtrace" do
255
+ context "and when the generated backtrace consists only of library frames" do
256
+ it "returns the full generated backtrace" do
257
+ backtrace = [
258
+ "/lib/airbrake-ruby/a.rb:84:in `build_notice'",
259
+ "/lib/airbrake-ruby/b.rb:124:in `send_notice'"
260
+ ]
261
+ allow(Kernel).to receive(:caller).and_return(backtrace)
262
+
263
+ notice = subject.build_notice(Exception.new)
264
+
265
+ expect(notice[:errors].first[:backtrace]).to eq(
266
+ [
267
+ { file: '/lib/airbrake-ruby/a.rb', line: 84, function: 'build_notice' },
268
+ { file: '/lib/airbrake-ruby/b.rb', line: 124, function: 'send_notice' }
269
+ ]
270
+ )
271
+ end
272
+ end
273
+
274
+ context "and when the generated backtrace consists of mixed frames" do
275
+ it "returns the filtered backtrace" do
276
+ backtrace = [
277
+ "/airbrake-ruby/lib/airbrake-ruby/a.rb:84:in `b'",
278
+ "/airbrake-ruby/lib/foo/b.rb:84:in `build'",
279
+ "/airbrake-ruby/lib/bar/c.rb:124:in `send'"
280
+ ]
281
+ allow(Kernel).to receive(:caller).and_return(backtrace)
282
+
283
+ notice = subject.build_notice(Exception.new)
284
+
285
+ expect(notice[:errors].first[:backtrace]).to eq(
286
+ [
287
+ { file: '/airbrake-ruby/lib/foo/b.rb', line: 84, function: 'build' },
288
+ { file: '/airbrake-ruby/lib/bar/c.rb', line: 124, function: 'send' }
289
+ ]
290
+ )
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ # TODO: this seems to be bugged. Fix later.
297
+ context "when given exception is a Java exception", skip: true do
298
+ before do
299
+ expect(Airbrake::Backtrace).to receive(:java_exception?).and_return(true)
300
+ end
301
+
302
+ it "automatically generates the backtrace" do
303
+ backtrace = [
304
+ "org/jruby/RubyKernel.java:998:in `eval'",
305
+ "/ruby/stdlib/irb/workspace.rb:87:in `evaluate'",
306
+ "/ruby/stdlib/irb.rb:489:in `block in eval_input'"
307
+ ]
308
+ allow(Kernel).to receive(:caller).and_return(backtrace)
309
+
310
+ notice = subject.build_notice(Exception.new)
311
+
312
+ # rubocop:disable Metrics/LineLength
313
+ expect(notice[:errors].first[:backtrace]).to eq(
314
+ [
315
+ { file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
316
+ { file: '/ruby/stdlib/irb/workspace.rb', line: 87, function: 'evaluate' },
317
+ { file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' }
318
+ ]
319
+ )
320
+ # rubocop:enable Metrics/LineLength
321
+ end
322
+ end
323
+
324
+ context "when async sender is closed" do
325
+ before do
326
+ expect_any_instance_of(Airbrake::AsyncSender)
327
+ .to receive(:closed?).and_return(true)
328
+ end
329
+
330
+ it "raises error" do
331
+ expect { subject.build_notice(Exception.new) }.to raise_error(
332
+ Airbrake::Error,
333
+ 'attempted to build Exception with closed Airbrake instance'
334
+ )
335
+ end
336
+ end
337
+ end
338
+
339
+ describe "#close" do
340
+ it "sends the close message to async sender" do
341
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:close)
342
+ subject.close
343
+ end
344
+ end
345
+
346
+ describe "#configured?" do
347
+ it { is_expected.to be_configured }
348
+ end
349
+
350
+ describe "#merge_context" do
351
+ it "merges the provided context with the notice object" do
352
+ expect_any_instance_of(Hash).to receive(:merge!).with(apples: 'oranges')
353
+ subject.merge_context(apples: 'oranges')
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,296 @@
1
+ RSpec.describe Airbrake::Notice do
2
+ let(:notice) { described_class.new(AirbrakeTestError.new, bingo: '1') }
3
+
4
+ describe "#to_json" do
5
+ context "app_version" do
6
+ context "when missing" do
7
+ before { Airbrake::Config.instance.merge(app_version: nil) }
8
+
9
+ it "doesn't include app_version" do
10
+ expect(notice.to_json).not_to match(/"context":{"version":"1.2.3"/)
11
+ end
12
+ end
13
+
14
+ context "when present" do
15
+ let(:notice) { described_class.new(AirbrakeTestError.new) }
16
+
17
+ before do
18
+ Airbrake::Config.instance.merge(
19
+ app_version: "1.2.3",
20
+ root_directory: "/one/two"
21
+ )
22
+ end
23
+
24
+ it "includes app_version" do
25
+ expect(notice.to_json).to match(/"context":{"version":"1.2.3"/)
26
+ end
27
+
28
+ it "includes root_directory" do
29
+ expect(notice.to_json).to match(%r{"rootDirectory":"/one/two"})
30
+ end
31
+ end
32
+ end
33
+
34
+ context "when versions is empty" do
35
+ it "doesn't set the 'versions' payload" do
36
+ expect(notice.to_json).not_to match(
37
+ /"context":{"versions":{"dep":"1.2.3"}}/
38
+ )
39
+ end
40
+ end
41
+
42
+ context "when versions is not empty" do
43
+ it "sets the 'versions' payload" do
44
+ notice[:context][:versions] = { 'dep' => '1.2.3' }
45
+ expect(notice.to_json).to match(
46
+ /"context":{.*"versions":{"dep":"1.2.3"}.*}/
47
+ )
48
+ end
49
+ end
50
+
51
+ context "truncation" do
52
+ shared_examples 'payloads' do |size, msg|
53
+ it msg do
54
+ ex = AirbrakeTestError.new
55
+
56
+ backtrace = []
57
+ size.times { backtrace << "bin/rails:3:in `<main>'" }
58
+ ex.set_backtrace(backtrace)
59
+
60
+ notice = described_class.new(ex)
61
+
62
+ expect(notice.to_json.bytesize).to be < 64000
63
+ end
64
+ end
65
+
66
+ max_msg = 'truncates to the max allowed size'
67
+
68
+ context "with an extremely huge payload" do
69
+ include_examples 'payloads', 200_000, max_msg
70
+ end
71
+
72
+ context "with a big payload" do
73
+ include_examples 'payloads', 50_000, max_msg
74
+ end
75
+
76
+ small_msg = "doesn't truncate it"
77
+
78
+ context "with a small payload" do
79
+ include_examples 'payloads', 1000, small_msg
80
+ end
81
+
82
+ context "with a tiny payload" do
83
+ include_examples 'payloads', 300, small_msg
84
+ end
85
+
86
+ context "when truncation failed" do
87
+ it "returns nil" do
88
+ expect_any_instance_of(Airbrake::Truncator)
89
+ .to receive(:reduce_max_size).and_return(0)
90
+
91
+ encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
92
+ bad_string = Base64.decode64(encoded)
93
+
94
+ ex = AirbrakeTestError.new
95
+
96
+ backtrace = []
97
+ 10.times { backtrace << "bin/rails:3:in `<#{bad_string}>'" }
98
+ ex.set_backtrace(backtrace)
99
+
100
+ notice = described_class.new(ex)
101
+ expect(notice.to_json).to be_nil
102
+ end
103
+ end
104
+
105
+ describe "object replacement with its string version" do
106
+ let(:klass) { Class.new {} }
107
+ let(:ex) { AirbrakeTestError.new }
108
+ let(:params) { { bingo: [Object.new, klass.new] } }
109
+ let(:notice) { described_class.new(ex, params) }
110
+
111
+ before do
112
+ backtrace = []
113
+ backtrace_size.times { backtrace << "bin/rails:3:in `<main>'" }
114
+ ex.set_backtrace(backtrace)
115
+ end
116
+
117
+ context "with payload within the limits" do
118
+ let(:backtrace_size) { 1000 }
119
+
120
+ it "doesn't happen" do
121
+ expect(notice.to_json)
122
+ .to match(/bingo":\["#<Object:.+>","#<#<Class:.+>:.+>"/)
123
+ end
124
+ end
125
+
126
+ context "with payload bigger than the limit" do
127
+ context "with payload within the limits" do
128
+ let(:backtrace_size) { 50_000 }
129
+
130
+ it "happens" do
131
+ expect(notice.to_json)
132
+ .to match(/bingo":\[".+Object.+",".+Class.+"/)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ context "given a closed IO object" do
140
+ context "and when it is not monkey-patched by ActiveSupport" do
141
+ it "is not getting truncated" do
142
+ notice[:params] = { obj: IO.new(0).tap(&:close) }
143
+ expect(notice.to_json).to match(/"obj":"#<IO:0x.+>"/)
144
+ end
145
+ end
146
+
147
+ context "and when it is monkey-patched by ActiveSupport" do
148
+ # Instances of this class contain a closed IO object assigned to an
149
+ # instance variable. Normally, the JSON gem, which we depend on can
150
+ # parse closed IO objects. However, because ActiveSupport monkey-patches
151
+ # #to_json and calls #to_a on them, they raise IOError when we try to
152
+ # serialize them.
153
+ #
154
+ # @see https://goo.gl/0A3xNC
155
+ class ObjectWithIoIvars
156
+ def initialize
157
+ @bongo = Tempfile.new('bongo').tap(&:close)
158
+ end
159
+
160
+ # @raise [NotImplementedError] when inside a Rails environment
161
+ def to_json(*)
162
+ raise NotImplementedError
163
+ end
164
+ end
165
+
166
+ # @see ObjectWithIoIvars
167
+ class ObjectWithNestedIoIvars
168
+ def initialize
169
+ @bish = ObjectWithIoIvars.new
170
+ end
171
+
172
+ # @see ObjectWithIoIvars#to_json
173
+ def to_json(*)
174
+ raise NotImplementedError
175
+ end
176
+ end
177
+
178
+ context "and also when it's a closed Tempfile" do
179
+ it "doesn't fail" do
180
+ notice[:params] = { obj: Tempfile.new('bongo').tap(&:close) }
181
+ expect(notice.to_json).to match(/"obj":"#<(Temp)?file:0x.+>"/i)
182
+ end
183
+ end
184
+
185
+ context "and also when it's an IO ivar" do
186
+ it "doesn't fail" do
187
+ notice[:params] = { obj: ObjectWithIoIvars.new }
188
+ expect(notice.to_json).to match(/"obj":".+ObjectWithIoIvars.+"/)
189
+ end
190
+
191
+ context "and when it's deeply nested inside a hash" do
192
+ it "doesn't fail" do
193
+ notice[:params] = { a: { b: { c: ObjectWithIoIvars.new } } }
194
+ expect(notice.to_json).to match(
195
+ /"params":{"a":{"b":{"c":".+ObjectWithIoIvars.+"}}.*}/
196
+ )
197
+ end
198
+ end
199
+
200
+ context "and when it's deeply nested inside an array" do
201
+ it "doesn't fail" do
202
+ notice[:params] = { a: [[ObjectWithIoIvars.new]] }
203
+ expect(notice.to_json).to match(
204
+ /"params":{"a":\[\[".+ObjectWithIoIvars.+"\]\].*}/
205
+ )
206
+ end
207
+ end
208
+ end
209
+
210
+ context "and also when it's a non-IO ivar, which contains an IO ivar itself" do
211
+ it "doesn't fail" do
212
+ notice[:params] = { obj: ObjectWithNestedIoIvars.new }
213
+ expect(notice.to_json).to match(/"obj":".+ObjectWithNested.+"/)
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ it "overwrites the 'notifier' payload with the default values" do
220
+ notice[:notifier] = { name: 'bingo', bango: 'bongo' }
221
+
222
+ expect(notice.to_json)
223
+ .to match(/"notifier":{"name":"airbrake-ruby","version":".+","url":".+"}/)
224
+ end
225
+
226
+ it "always contains context/hostname" do
227
+ expect(notice.to_json)
228
+ .to match(/"context":{.*"hostname":".+".*}/)
229
+ end
230
+
231
+ it "defaults to the error severity" do
232
+ expect(notice.to_json).to match(/"context":{.*"severity":"error".*}/)
233
+ end
234
+
235
+ it "always contains environment/program_name" do
236
+ expect(notice.to_json)
237
+ .to match(%r|"environment":{"program_name":.+/rspec.*|)
238
+ end
239
+
240
+ it "contains errors" do
241
+ expect(notice.to_json)
242
+ .to match(/"errors":\[{"type":"AirbrakeTestError","message":"App crash/)
243
+ end
244
+
245
+ it "contains a backtrace" do
246
+ expect(notice.to_json)
247
+ .to match(%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|)
248
+ end
249
+
250
+ it "contains params" do
251
+ expect(notice.to_json).to match(/"params":{"bingo":"1"}/)
252
+ end
253
+ end
254
+
255
+ describe "#[]" do
256
+ it "accesses payload" do
257
+ expect(notice[:params]).to eq(bingo: '1')
258
+ end
259
+
260
+ it "raises error if notice is ignored" do
261
+ notice.ignore!
262
+ expect { notice[:params] }
263
+ .to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
264
+ end
265
+ end
266
+
267
+ describe "#[]=" do
268
+ it "sets a payload value" do
269
+ hash = { bingo: 'bango' }
270
+ notice[:params] = hash
271
+ expect(notice[:params]).to equal(hash)
272
+ end
273
+
274
+ it "raises error if notice is ignored" do
275
+ notice.ignore!
276
+ expect { notice[:params] = {} }
277
+ .to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
278
+ end
279
+
280
+ it "raises error when trying to assign unrecognized key" do
281
+ expect { notice[:bingo] = 1 }
282
+ .to raise_error(Airbrake::Error, /:bingo is not recognized among/)
283
+ end
284
+
285
+ it "raises when setting non-hash objects as the value" do
286
+ expect { notice[:params] = Object.new }
287
+ .to raise_error(Airbrake::Error, 'Got Object value, wanted a Hash')
288
+ end
289
+ end
290
+
291
+ describe "#stash" do
292
+ subject { described_class.new(AirbrakeTestError.new) }
293
+
294
+ it { is_expected.to respond_to(:stash) }
295
+ end
296
+ end