airbrake-ruby 4.7.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.
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