bugsnag 6.14.0 → 6.15.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/VERSION +1 -1
  4. data/features/fixtures/docker-compose.yml +5 -1
  5. data/features/fixtures/plain/app/report_modification/initiators/handled_on_error.rb +10 -0
  6. data/features/fixtures/plain/app/report_modification/initiators/unhandled_on_error.rb +11 -0
  7. data/features/fixtures/plain/app/stack_frame_modification/initiators/handled_on_error.rb +29 -0
  8. data/features/fixtures/plain/app/stack_frame_modification/initiators/unhandled_on_error.rb +26 -0
  9. data/features/fixtures/rails3/app/config/initializers/bugsnag.rb +8 -0
  10. data/features/fixtures/rails4/app/config/initializers/bugsnag.rb +8 -0
  11. data/features/fixtures/rails5/app/config/initializers/bugsnag.rb +8 -0
  12. data/features/fixtures/rails6/app/config/initializers/bugsnag.rb +8 -0
  13. data/features/plain_features/add_tab.feature +7 -1
  14. data/features/plain_features/ignore_report.feature +2 -0
  15. data/features/plain_features/report_api_key.feature +3 -1
  16. data/features/plain_features/report_severity.feature +2 -0
  17. data/features/plain_features/report_stack_frames.feature +4 -0
  18. data/features/plain_features/report_user.feature +7 -1
  19. data/features/rails_features/on_error.feature +29 -0
  20. data/lib/bugsnag.rb +35 -0
  21. data/lib/bugsnag/code_extractor.rb +137 -0
  22. data/lib/bugsnag/configuration.rb +27 -0
  23. data/lib/bugsnag/middleware_stack.rb +38 -3
  24. data/lib/bugsnag/on_error_callbacks.rb +33 -0
  25. data/lib/bugsnag/report.rb +1 -1
  26. data/lib/bugsnag/session_tracker.rb +3 -3
  27. data/lib/bugsnag/stacktrace.rb +25 -68
  28. data/spec/code_extractor_spec.rb +129 -0
  29. data/spec/fixtures/crashes/file1.rb +29 -0
  30. data/spec/fixtures/crashes/file2.rb +25 -0
  31. data/spec/fixtures/crashes/file_with_long_lines.rb +7 -0
  32. data/spec/fixtures/crashes/functions.rb +29 -0
  33. data/spec/fixtures/crashes/short_file.rb +2 -0
  34. data/spec/on_error_spec.rb +332 -0
  35. data/spec/report_spec.rb +7 -4
  36. data/spec/spec_helper.rb +8 -0
  37. data/spec/stacktrace_spec.rb +276 -30
  38. metadata +15 -2
@@ -0,0 +1,25 @@
1
+ module File2
2
+ def self.foo2
3
+ File1.bar1
4
+ end
5
+
6
+ def self.bar2
7
+ File1.baz1
8
+ end
9
+
10
+ def self.baz2
11
+ raise 'uh oh'
12
+ end
13
+
14
+ def self.abc2
15
+ puts 'abc'
16
+ end
17
+
18
+ def self.abcdef2
19
+ puts 'abcdef2'
20
+ end
21
+
22
+ def self.abcdefghi2
23
+ puts 'abcdefghi2'
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # rubocop:disable Layout/LineLength
2
+ def a_super_long_function_name_that_would_be_really_impractical_to_use_but_luckily_this_is_just_for_a_test_to_prove_we_can_handle_really_long_lines_of_code_that_go_over_200_characters_and_some_more_padding
3
+ puts 'This is a shorter string'
4
+ puts 'A more realistic example of when a line would be really long is long strings such as this one, which extends over the 200 character limit by containing a lot of excess words for padding its length so that it is super long'
5
+ puts 'and another shorter string for comparison'
6
+ end
7
+ # rubocop:enable Layout/LineLength
@@ -0,0 +1,29 @@
1
+ def foo
2
+ bar
3
+ end
4
+
5
+ def bar
6
+ baz
7
+ end
8
+
9
+ def baz
10
+ xyz
11
+ end
12
+
13
+ def xyz
14
+ raise 'uh oh'
15
+ end
16
+
17
+ def abc
18
+ puts 'abc'
19
+ end
20
+
21
+ def abcdef
22
+ puts 'abcdef'
23
+ end
24
+
25
+ def abcdefghi
26
+ puts 'abcdefghi'
27
+ end
28
+
29
+ foo
@@ -1 +1,3 @@
1
+ #
1
2
  raise 'hell'
3
+ #
@@ -0,0 +1,332 @@
1
+ require "spec_helper"
2
+
3
+ describe "on_error callbacks" do
4
+ it "runs callbacks on notify" do
5
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
6
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
7
+
8
+ Bugsnag.add_on_error(callback1)
9
+ Bugsnag.add_on_error(callback2)
10
+
11
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
12
+
13
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
14
+ event = get_event_from_payload(payload)
15
+
16
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
17
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
18
+ end)
19
+ end
20
+
21
+ it "can add callbacks in a configure block" do
22
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
23
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
24
+
25
+ Bugsnag.configure do |config|
26
+ config.add_on_error(callback1)
27
+ config.add_on_error(callback2)
28
+ end
29
+
30
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
31
+
32
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
33
+ event = get_event_from_payload(payload)
34
+
35
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
36
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
37
+ end)
38
+ end
39
+
40
+ it "can remove an already registered callback" do
41
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
42
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
43
+
44
+ Bugsnag.add_on_error(callback1)
45
+ Bugsnag.add_on_error(callback2)
46
+
47
+ Bugsnag.remove_on_error(callback1)
48
+
49
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
50
+
51
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
52
+ event = get_event_from_payload(payload)
53
+
54
+ expect(event["metaData"]["important"]).to be_nil
55
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
56
+ end)
57
+ end
58
+
59
+ it "can remove all registered callbacks" do
60
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
61
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
62
+
63
+ Bugsnag.add_on_error(callback1)
64
+ Bugsnag.add_on_error(callback2)
65
+
66
+ Bugsnag.remove_on_error(callback2)
67
+ Bugsnag.remove_on_error(callback1)
68
+
69
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
70
+
71
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
72
+ event = get_event_from_payload(payload)
73
+
74
+ expect(event["metaData"]["important"]).to be_nil
75
+ expect(event["metaData"]["significant"]).to be_nil
76
+ end)
77
+ end
78
+
79
+ it "does not remove an identical callback if it is not the same Proc" do
80
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
81
+ callback1_duplicate = proc {|report| report.add_tab(:important, { hello: "world" }) }
82
+
83
+ Bugsnag.add_on_error(callback1)
84
+ Bugsnag.remove_on_error(callback1_duplicate)
85
+
86
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
87
+
88
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
89
+ event = get_event_from_payload(payload)
90
+
91
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
92
+ end)
93
+ end
94
+
95
+ it "can re-add callbacks that have previously been removed" do
96
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
97
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
98
+
99
+ Bugsnag.add_on_error(callback1)
100
+ Bugsnag.add_on_error(callback2)
101
+
102
+ Bugsnag.remove_on_error(callback1)
103
+
104
+ Bugsnag.add_on_error(callback1)
105
+
106
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
107
+
108
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
109
+ event = get_event_from_payload(payload)
110
+
111
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
112
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
113
+ end)
114
+ end
115
+
116
+ it "will only add a callback once" do
117
+ called_count = 0
118
+
119
+ callback = proc do |report|
120
+ called_count += 1
121
+
122
+ report.add_tab(:important, { called: called_count })
123
+ end
124
+
125
+ 1.upto(10) do |i|
126
+ Bugsnag.add_on_error(callback)
127
+ end
128
+
129
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
130
+
131
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
132
+ event = get_event_from_payload(payload)
133
+
134
+ expect(called_count).to be(1)
135
+ expect(event["metaData"]["important"]).to eq({ "called" => 1 })
136
+ end)
137
+ end
138
+
139
+ it "will ignore the report and stop calling callbacks if one returns false" do
140
+ logger = spy('logger')
141
+ Bugsnag.configuration.logger = logger
142
+
143
+ called_count = 0
144
+
145
+ callback1 = proc { called_count += 1 }
146
+ callback2 = proc { called_count += 1 }
147
+ callback3 = proc { false }
148
+ callback4 = proc { called_count += 1 }
149
+ callback5 = proc { called_count += 1 }
150
+
151
+ Bugsnag.add_on_error(callback1)
152
+ Bugsnag.add_on_error(callback2)
153
+ Bugsnag.add_on_error(callback3)
154
+ Bugsnag.add_on_error(callback4)
155
+ Bugsnag.add_on_error(callback5)
156
+
157
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
158
+
159
+ expect(Bugsnag).not_to have_sent_notification
160
+ expect(called_count).to be(2)
161
+
162
+ expect(logger).to have_received(:debug).with("[Bugsnag]") do |&block|
163
+ expect(block.call).to eq("Not notifying RuntimeError due to ignore being signified in user provided middleware")
164
+ end
165
+ end
166
+
167
+ it "callbacks are called in the same order they are added (FIFO)" do
168
+ callback1 = proc do |report|
169
+ expect(report.meta_data[:important]).to be_nil
170
+
171
+ report.add_tab(:important, { magic_number: 9 })
172
+ end
173
+
174
+ callback2 = proc do |report|
175
+ expect(report.meta_data[:important]).to eq({ magic_number: 9 })
176
+
177
+ report.add_tab(:important, { magic_number: 99 })
178
+ end
179
+
180
+ callback3 = proc do |report|
181
+ expect(report.meta_data[:important]).to eq({ magic_number: 99 })
182
+
183
+ report.add_tab(:important, { magic_number: 999 })
184
+ end
185
+
186
+ Bugsnag.add_on_error(callback1)
187
+ Bugsnag.add_on_error(callback2)
188
+ Bugsnag.add_on_error(callback3)
189
+
190
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
191
+
192
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
193
+ event = get_event_from_payload(payload)
194
+
195
+ expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 })
196
+ end)
197
+ end
198
+
199
+ it "callbacks continue being called after a callback raises" do
200
+ logger = spy('logger')
201
+ Bugsnag.configuration.logger = logger
202
+
203
+ callback1 = proc {|report| report.add_tab(:important, { a: "b" }) }
204
+ callback2 = proc {|_report| raise "bad things" }
205
+ callback3 = proc {|report| report.add_tab(:important, { c: "d" }) }
206
+
207
+ Bugsnag.add_on_error(callback1)
208
+ Bugsnag.add_on_error(callback2)
209
+ Bugsnag.add_on_error(callback3)
210
+
211
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
212
+
213
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
214
+ event = get_event_from_payload(payload)
215
+
216
+ expect(event["metaData"]["important"]).to eq({ "a" => "b", "c" => "d" })
217
+ end)
218
+
219
+ message_index = 0
220
+ expected_messages = [
221
+ /^Error occurred in on_error callback: 'bad things'$/,
222
+ /^on_error callback stacktrace:/
223
+ ]
224
+
225
+ expect(logger).to have_received(:warn).with("[Bugsnag]").twice do |&block|
226
+ expect(block.call).to match(expected_messages[message_index])
227
+ message_index += 1
228
+ end
229
+ end
230
+
231
+ it "runs callbacks even if no other middleware is registered" do
232
+ # Reset the middleware stack so any default middleware are removed
233
+ Bugsnag.configuration.middleware = Bugsnag::MiddlewareStack.new
234
+
235
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
236
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
237
+
238
+ Bugsnag.add_on_error(callback1)
239
+ Bugsnag.add_on_error(callback2)
240
+
241
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
242
+
243
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
244
+ event = get_event_from_payload(payload)
245
+
246
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
247
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
248
+ end)
249
+ end
250
+
251
+ describe "using callbacks across threads" do
252
+ it "runs callbacks that are added in different threads" do
253
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
254
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
255
+ callback3 = proc {|report| report.add_tab(:crucial, { magic_number: 999 }) }
256
+
257
+ Bugsnag.add_on_error(callback1)
258
+
259
+ threads = [
260
+ Thread.new { Bugsnag.add_on_error(callback2) },
261
+ Thread.new { Bugsnag.add_on_error(callback3) }
262
+ ]
263
+
264
+ threads.each(&:join)
265
+
266
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
267
+
268
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
269
+ event = get_event_from_payload(payload)
270
+
271
+ expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
272
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
273
+ expect(event["metaData"]["crucial"]).to eq({ "magic_number" => 999 })
274
+ end)
275
+ end
276
+
277
+ it "can remove callbacks that are added in different threads" do
278
+ callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
279
+ callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
280
+
281
+ # We need to create & join these one at a time so that callback1 has
282
+ # definitely been added before it is removed, otherwise this test will fail
283
+ # at random
284
+ Thread.new { Bugsnag.add_on_error(callback1) }.join
285
+ Thread.new { Bugsnag.remove_on_error(callback1) }.join
286
+ Thread.new { Bugsnag.add_on_error(callback2) }.join
287
+
288
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
289
+
290
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
291
+ event = get_event_from_payload(payload)
292
+
293
+ expect(event["metaData"]["important"]).to be_nil
294
+ expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
295
+ end)
296
+ end
297
+
298
+ it "callbacks are called in FIFO order when added in separate threads" do
299
+ callback1 = proc do |report|
300
+ expect(report.meta_data[:important]).to be_nil
301
+
302
+ report.add_tab(:important, { magic_number: 9 })
303
+ end
304
+
305
+ callback2 = proc do |report|
306
+ expect(report.meta_data[:important]).to eq({ magic_number: 9 })
307
+
308
+ report.add_tab(:important, { magic_number: 99 })
309
+ end
310
+
311
+ callback3 = proc do |report|
312
+ expect(report.meta_data[:important]).to eq({ magic_number: 99 })
313
+
314
+ report.add_tab(:important, { magic_number: 999 })
315
+ end
316
+
317
+ # As above, we need to create & join these one at a time so that each
318
+ # callback is added in sequence
319
+ Thread.new { Bugsnag.add_on_error(callback1) }.join
320
+ Thread.new { Bugsnag.add_on_error(callback2) }.join
321
+ Thread.new { Bugsnag.add_on_error(callback3) }.join
322
+
323
+ Bugsnag.notify(RuntimeError.new("Oh no!"))
324
+
325
+ expect(Bugsnag).to(have_sent_notification do |payload, _headers|
326
+ event = get_event_from_payload(payload)
327
+
328
+ expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 })
329
+ end)
330
+ end
331
+ end
332
+ end
@@ -1373,27 +1373,30 @@ describe Bugsnag::Report do
1373
1373
  notify_test_exception
1374
1374
  expect(Bugsnag).to have_sent_notification{ |payload, headers|
1375
1375
  exception = get_exception_from_payload(payload)
1376
+
1376
1377
  bugsnag_count = 0
1378
+
1377
1379
  exception["stacktrace"].each do |frame|
1378
1380
  if /.*lib\/bugsnag.*\.rb/.match(frame["file"])
1379
1381
  bugsnag_count += 1
1380
1382
  expect(frame["inProject"]).to be_nil
1381
1383
  end
1382
1384
  end
1383
- # 7 is used here as the called bugsnag frames for a `notify` call should be:
1385
+
1386
+ # 6 is used here as the called bugsnag frames for a `notify` call should be:
1384
1387
  # - Bugsnag.notify
1385
1388
  # - Report.new
1386
1389
  # - Report.initialize
1387
1390
  # - Report.generate_exceptions_list
1388
1391
  # - Report.generate_exceptions_list | raw_exceptions.map
1389
1392
  # - Report.generate_exceptions_list | raw_exceptions.map | block
1390
- # - Report.generate_exceptions_list | raw_exceptions.map | block | Stacktrace.new
1391
- # However, JRUBY does not include the two `new` frames, resulting in 5 bugsnag frames
1393
+ # However, JRUBY does not include the `Report.new` frame, resulting in 5 bugsnag frames
1392
1394
  if defined?(JRUBY_VERSION)
1393
1395
  frame_count = 5
1394
1396
  else
1395
- frame_count = 7
1397
+ frame_count = 6
1396
1398
  end
1399
+
1397
1400
  expect(bugsnag_count).to equal frame_count
1398
1401
  }
1399
1402
  end
@@ -30,6 +30,14 @@ def get_exception_from_payload(payload)
30
30
  event["exceptions"].last
31
31
  end
32
32
 
33
+ def get_code_from_payload(payload, index = 0)
34
+ exception = get_exception_from_payload(payload)
35
+
36
+ expect(exception["stacktrace"].size).to be > index
37
+
38
+ exception["stacktrace"][index]["code"]
39
+ end
40
+
33
41
  def notify_test_exception(*args)
34
42
  Bugsnag.notify(RuntimeError.new("test message"), *args)
35
43
  end