bugsnag 6.14.0 → 6.15.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37ddadee8c92423e803ed8d9be43b564c79110ea9ddf8dd4180a9a3b1080e575
4
- data.tar.gz: 887d7017da4d23fe00515109c8c791c2e4d205e5968faaa3178d522ab5a7c51c
3
+ metadata.gz: 1a4f6c896c7046dba3d9db2adfa32e232232e3a7923cb9984321ee3a8b09b6e3
4
+ data.tar.gz: f6b27c0b50846c1a3e012bb80bccbe5ad6663257c8de4e300742e7af4b065b3f
5
5
  SHA512:
6
- metadata.gz: 0a4808f2f7f73728ebab03d434f3ddccc38ceb743d986adf56225725ebdeee84c9ec63c4a2334078fbef44aa084b362468ab6cc98f2bcf8bc6ada991db44099a
7
- data.tar.gz: ad4ba7af2a2aae7360cf111507dbaec30e0a6e668b606c111e231fc75683556ff14e0376673c50f45231dbcec5f59ff0649cf2e89c85e8cedf7e14b9b4fc196d
6
+ metadata.gz: d51d64bbf0b2cc094742a8a207702e963c33eac9e18f58f2348f3d9e6475a1bd0c43dc44baf139bb976823bfabd8b3ff5bab261ae3dababd46f083b7539602f4
7
+ data.tar.gz: 3621690efd266608313b000f7acff6df354380457e9b78e448290573e97dd0b17beb68afa5efff96fdcc7b21b39d2d292fddc38082eae7bece24dd1f2d457f0d
@@ -1,6 +1,23 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## 6.15.0 (27 July 2020)
5
+
6
+ ### Enhancements
7
+
8
+ * Add `on_error` callbacks to replace `before_notify_callbacks`
9
+ | [#608](https://github.com/bugsnag/bugsnag-ruby/pull/608)
10
+
11
+ * Improve performance when extracting code from files in stacktraces
12
+ | [#604](https://github.com/bugsnag/bugsnag-ruby/pull/604)
13
+
14
+ * Reduce memory use when session tracking is disabled
15
+ | [#606](https://github.com/bugsnag/bugsnag-ruby/pull/606)
16
+
17
+ ### Deprecated
18
+
19
+ * `before_notify_callbacks` have been deprecated in favour of `on_error` and will be removed in the next major release
20
+
4
21
  ## 6.14.0 (20 July 2020)
5
22
 
6
23
  ### Enhancements
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.14.0
1
+ 6.15.0
@@ -120,6 +120,7 @@ services:
120
120
  - BUGSNAG_TIMEOUT
121
121
  - CALLBACK_INITIATOR
122
122
  - SQL_ONLY_BREADCRUMBS
123
+ - ADD_ON_ERROR
123
124
  - USE_DEFAULT_AUTO_CAPTURE_SESSIONS
124
125
  restart: "no"
125
126
 
@@ -155,6 +156,7 @@ services:
155
156
  - BUGSNAG_TIMEOUT
156
157
  - CALLBACK_INITIATOR
157
158
  - SQL_ONLY_BREADCRUMBS
159
+ - ADD_ON_ERROR
158
160
  - USE_DEFAULT_AUTO_CAPTURE_SESSIONS
159
161
  restart: "no"
160
162
 
@@ -190,6 +192,7 @@ services:
190
192
  - BUGSNAG_TIMEOUT
191
193
  - CALLBACK_INITIATOR
192
194
  - SQL_ONLY_BREADCRUMBS
195
+ - ADD_ON_ERROR
193
196
  - USE_DEFAULT_AUTO_CAPTURE_SESSIONS
194
197
  restart: "no"
195
198
 
@@ -225,6 +228,7 @@ services:
225
228
  - BUGSNAG_TIMEOUT
226
229
  - CALLBACK_INITIATOR
227
230
  - SQL_ONLY_BREADCRUMBS
231
+ - ADD_ON_ERROR
228
232
  - USE_DEFAULT_AUTO_CAPTURE_SESSIONS
229
233
  restart: "no"
230
234
  networks:
@@ -296,4 +300,4 @@ services:
296
300
 
297
301
  networks:
298
302
  default:
299
- name: ${NETWORK_NAME}
303
+ name: ${NETWORK_NAME}
@@ -0,0 +1,10 @@
1
+ require 'bugsnag'
2
+ require './app'
3
+
4
+ configure_basics
5
+
6
+ def run(callback)
7
+ Bugsnag.add_on_error(callback)
8
+
9
+ Bugsnag.notify(RuntimeError.new("Oh no"))
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'bugsnag'
2
+ require './app'
3
+
4
+ configure_basics
5
+ add_at_exit
6
+
7
+ def run(callback)
8
+ Bugsnag.add_on_error(callback)
9
+
10
+ raise RuntimeError.new "Oh no"
11
+ end
@@ -0,0 +1,29 @@
1
+ require 'bugsnag'
2
+ require './app'
3
+
4
+ configure_basics
5
+
6
+ def run(callback)
7
+ Bugsnag.add_on_error(callback)
8
+ step_one
9
+ end
10
+
11
+ def step_one
12
+ step_two
13
+ end
14
+
15
+ def step_two
16
+ step_three
17
+ end
18
+
19
+ def step_three
20
+ crash
21
+ end
22
+
23
+ def crash
24
+ begin
25
+ "Test".insrt(-1, "!")
26
+ rescue Exception => e
27
+ Bugsnag.notify(e)
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'bugsnag'
2
+ require './app'
3
+
4
+ configure_basics
5
+ add_at_exit
6
+
7
+ def run(callback)
8
+ Bugsnag.add_on_error(callback)
9
+ step_one
10
+ end
11
+
12
+ def step_one
13
+ step_two
14
+ end
15
+
16
+ def step_two
17
+ step_three
18
+ end
19
+
20
+ def step_three
21
+ crash
22
+ end
23
+
24
+ def crash
25
+ raise RuntimeError.new "Oh no"
26
+ end
@@ -18,4 +18,12 @@ Bugsnag.configure do |config|
18
18
  breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load"
19
19
  end
20
20
  end
21
+
22
+ if ENV["ADD_ON_ERROR"] == "true"
23
+ config.add_on_error(proc do |report|
24
+ report.add_tab(:on_error, {
25
+ source: report.unhandled ? 'on_error unhandled' : 'on_error handled'
26
+ })
27
+ end)
28
+ end
21
29
  end
@@ -18,4 +18,12 @@ Bugsnag.configure do |config|
18
18
  breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load"
19
19
  end
20
20
  end
21
+
22
+ if ENV["ADD_ON_ERROR"] == "true"
23
+ config.add_on_error(proc do |report|
24
+ report.add_tab(:on_error, {
25
+ source: report.unhandled ? 'on_error unhandled' : 'on_error handled'
26
+ })
27
+ end)
28
+ end
21
29
  end
@@ -18,4 +18,12 @@ Bugsnag.configure do |config|
18
18
  breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load"
19
19
  end
20
20
  end
21
+
22
+ if ENV["ADD_ON_ERROR"] == "true"
23
+ config.add_on_error(proc do |report|
24
+ report.add_tab(:on_error, {
25
+ source: report.unhandled ? 'on_error unhandled' : 'on_error handled'
26
+ })
27
+ end)
28
+ end
21
29
  end
@@ -18,4 +18,12 @@ Bugsnag.configure do |config|
18
18
  breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load"
19
19
  end
20
20
  end
21
+
22
+ if ENV["ADD_ON_ERROR"] == "true"
23
+ config.add_on_error(proc do |report|
24
+ report.add_tab(:on_error, {
25
+ source: report.unhandled ? 'on_error unhandled' : 'on_error handled'
26
+ })
27
+ end)
28
+ end
21
29
  end
@@ -15,6 +15,8 @@ Scenario Outline: Metadata can be added to a report using add_tab
15
15
  | handled_before_notify |
16
16
  | handled_block |
17
17
  | unhandled_before_notify |
18
+ | handled_on_error |
19
+ | unhandled_on_error |
18
20
 
19
21
  Scenario Outline: Metadata can be added to an existing tab using add_tab
20
22
  Given I set environment variable "CALLBACK_INITIATOR" to "<initiator>"
@@ -33,6 +35,8 @@ Scenario Outline: Metadata can be added to an existing tab using add_tab
33
35
  | handled_before_notify |
34
36
  | handled_block |
35
37
  | unhandled_before_notify |
38
+ | handled_on_error |
39
+ | unhandled_on_error |
36
40
 
37
41
  Scenario Outline: Metadata can be overwritten using add_tab
38
42
  Given I set environment variable "CALLBACK_INITIATOR" to "<initiator>"
@@ -46,4 +50,6 @@ Scenario Outline: Metadata can be overwritten using add_tab
46
50
  | initiator |
47
51
  | handled_before_notify |
48
52
  | handled_block |
49
- | unhandled_before_notify |
53
+ | unhandled_before_notify |
54
+ | handled_on_error |
55
+ | unhandled_on_error |
@@ -10,3 +10,5 @@ Scenario Outline: A reports severity can be modified
10
10
  | initiator |
11
11
  | handled_before_notify |
12
12
  | unhandled_before_notify |
13
+ | handled_on_error |
14
+ | unhandled_on_error |
@@ -11,4 +11,6 @@ Scenario Outline: A report can have its api_key modified
11
11
  | initiator |
12
12
  | handled_before_notify |
13
13
  | handled_block |
14
- | unhandled_before_notify |
14
+ | unhandled_before_notify |
15
+ | handled_on_error |
16
+ | unhandled_on_error |
@@ -13,3 +13,5 @@ Scenario Outline: A reports severity can be modified
13
13
  | handled_before_notify |
14
14
  | handled_block |
15
15
  | unhandled_before_notify |
16
+ | handled_on_error |
17
+ | unhandled_on_error |
@@ -12,6 +12,8 @@ Scenario Outline: Stack frames can be removed
12
12
  | initiator | lineNumber |
13
13
  | handled_before_notify | 20 |
14
14
  | unhandled_before_notify | 21 |
15
+ | handled_on_error | 20 |
16
+ | unhandled_on_error | 21 |
15
17
 
16
18
  Scenario: Stack frames can be removed from a notified string
17
19
  Given I set environment variable "CALLBACK_INITIATOR" to "handled_block"
@@ -36,6 +38,8 @@ Scenario Outline: Stack frames can be marked as in project
36
38
  | initiator |
37
39
  | handled_before_notify |
38
40
  | unhandled_before_notify |
41
+ | handled_on_error |
42
+ | unhandled_on_error |
39
43
 
40
44
  Scenario: Stack frames can be marked as in project with a handled string
41
45
  Given I set environment variable "CALLBACK_INITIATOR" to "handled_block"
@@ -14,6 +14,8 @@ Scenario Outline: A report can have a user name, email, and id set
14
14
  | handled_before_notify |
15
15
  | handled_block |
16
16
  | unhandled_before_notify |
17
+ | handled_on_error |
18
+ | unhandled_on_error |
17
19
 
18
20
  Scenario Outline: A report can have custom info set
19
21
  Given I set environment variable "CALLBACK_INITIATOR" to "<initiator>"
@@ -30,6 +32,8 @@ Scenario Outline: A report can have custom info set
30
32
  | handled_before_notify |
31
33
  | handled_block |
32
34
  | unhandled_before_notify |
35
+ | handled_on_error |
36
+ | unhandled_on_error |
33
37
 
34
38
  Scenario Outline: A report can have its user info removed
35
39
  Given I set environment variable "CALLBACK_INITIATOR" to "<initiator>"
@@ -42,4 +46,6 @@ Scenario Outline: A report can have its user info removed
42
46
  | initiator |
43
47
  | handled_before_notify |
44
48
  | handled_block |
45
- | unhandled_before_notify |
49
+ | unhandled_before_notify |
50
+ | handled_on_error |
51
+ | unhandled_on_error |
@@ -0,0 +1,29 @@
1
+ Feature: On error callbacks
2
+
3
+ @rails3 @rails4 @rails5 @rails6
4
+ Scenario: Rails on_error works on handled errors
5
+ Given I set environment variable "ADD_ON_ERROR" to "true"
6
+ And I start the rails service
7
+ When I navigate to the route "/handled/unthrown" on the rails app
8
+ And I wait to receive a request
9
+ Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier"
10
+ And the exception "errorClass" equals "RuntimeError"
11
+ And the exception "message" starts with "handled unthrown error"
12
+ And the event "unhandled" is false
13
+ And the event "app.type" equals "rails"
14
+ And the event "metaData.request.url" ends with "/handled/unthrown"
15
+ And the event "metaData.on_error.source" equals "on_error handled"
16
+
17
+ @rails3 @rails4 @rails5 @rails6
18
+ Scenario: Rails on_error works on unhandled errors
19
+ Given I set environment variable "ADD_ON_ERROR" to "true"
20
+ And I start the rails service
21
+ When I navigate to the route "/unhandled/error" on the rails app
22
+ And I wait to receive a request
23
+ Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier"
24
+ And the exception "errorClass" equals "NameError"
25
+ And the exception "message" starts with "undefined local variable or method `generate_unhandled_error' for #<UnhandledController"
26
+ And the event "unhandled" is true
27
+ And the event "app.type" equals "rails"
28
+ And the event "metaData.request.url" ends with "/unhandled/error"
29
+ And the event "metaData.on_error.source" equals "on_error unhandled"
@@ -48,6 +48,12 @@ module Bugsnag
48
48
  def configure(validate_api_key=true)
49
49
  yield(configuration) if block_given?
50
50
 
51
+ # Create the session tracker if sessions are enabled to avoid the overhead
52
+ # of creating it on the first request. We skip this if we're not validating
53
+ # the API key as we use this internally before the user's configure block
54
+ # has run, so we don't know if sessions are enabled yet.
55
+ session_tracker if validate_api_key && configuration.auto_capture_sessions
56
+
51
57
  check_key_valid if validate_api_key
52
58
  check_endpoint_setup
53
59
 
@@ -169,6 +175,8 @@ module Bugsnag
169
175
  # Allow access to "before notify" callbacks as an array.
170
176
  #
171
177
  # These callbacks will be called whenever an error notification is being made.
178
+ #
179
+ # @deprecated Use {Bugsnag#add_on_error} instead
172
180
  def before_notify_callbacks
173
181
  Bugsnag.configuration.request_data[:before_callbacks] ||= []
174
182
  end
@@ -227,6 +235,33 @@ module Bugsnag
227
235
  configuration.breadcrumbs << breadcrumb unless breadcrumb.ignore?
228
236
  end
229
237
 
238
+ ##
239
+ # Add the given callback to the list of on_error callbacks
240
+ #
241
+ # The on_error callbacks will be called when an error is captured or reported
242
+ # and are passed a {Bugsnag::Report} object
243
+ #
244
+ # Returning false from an on_error callback will cause the error to be ignored
245
+ # and will prevent any remaining callbacks from being called
246
+ #
247
+ # @param callback [Proc]
248
+ # @return [void]
249
+ def add_on_error(callback)
250
+ configuration.add_on_error(callback)
251
+ end
252
+
253
+ ##
254
+ # Remove the given callback from the list of on_error callbacks
255
+ #
256
+ # Note that this must be the same Proc instance that was passed to
257
+ # {Bugsnag#add_on_error}, otherwise it will not be removed
258
+ #
259
+ # @param callback [Proc]
260
+ # @return [void]
261
+ def remove_on_error(callback)
262
+ configuration.remove_on_error(callback)
263
+ end
264
+
230
265
  ##
231
266
  # Returns the client's Cleaner object, or creates one if not yet created.
232
267
  #
@@ -0,0 +1,137 @@
1
+ module Bugsnag
2
+ # @api private
3
+ class CodeExtractor
4
+ MAXIMUM_LINES_TO_KEEP = 7
5
+
6
+ ##
7
+ # @param configuration [Configuration]
8
+ def initialize(configuration)
9
+ @files = {}
10
+ @configuration = configuration
11
+ end
12
+
13
+ ##
14
+ # Add a file and its corresponding trace hash to be processed.
15
+ #
16
+ # @param path [String] The full path to the file
17
+ # @param trace [Hash]
18
+ # @return [void]
19
+ def add_file(path, trace)
20
+ # If the file doesn't exist we can't extract code from it, so we can skip
21
+ # this file entirely
22
+ unless File.exist?(path)
23
+ trace[:code] = nil
24
+
25
+ return
26
+ end
27
+
28
+ @files[path] ||= []
29
+ @files[path].push(trace)
30
+
31
+ # Record the line numbers we want to fetch for this trace
32
+ # We grab extra lines so that we can compensate if the error is on the
33
+ # first or last line of a file
34
+ first_line_number = trace[:lineNumber] - MAXIMUM_LINES_TO_KEEP
35
+
36
+ trace[:first_line_number] = first_line_number < 1 ? 1 : first_line_number
37
+ trace[:last_line_number] = trace[:lineNumber] + MAXIMUM_LINES_TO_KEEP
38
+ end
39
+
40
+ ##
41
+ # Add the code to the hashes that were given in {#add_file} by modifying
42
+ # them in-place. They will have a new ':code' key containing a hash of line
43
+ # number => string of code for that line
44
+ #
45
+ # @return [void]
46
+ def extract!
47
+ @files.each do |path, traces|
48
+ begin
49
+ line_numbers = Set.new
50
+
51
+ traces.each do |trace|
52
+ trace[:first_line_number].upto(trace[:last_line_number]) do |line_number|
53
+ line_numbers << line_number
54
+ end
55
+ end
56
+
57
+ extract_from(path, traces, line_numbers)
58
+ rescue StandardError => e
59
+ # Clean up after ourselves
60
+ traces.each do |trace|
61
+ trace[:code] ||= nil
62
+ trace.delete(:first_line_number)
63
+ trace.delete(:last_line_number)
64
+ end
65
+
66
+ @configuration.warn("Error extracting code: #{e.inspect}")
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ ##
74
+ # @param path [String]
75
+ # @param traces [Array<Hash>]
76
+ # @param line_numbers [Set<Integer>]
77
+ # @return [void]
78
+ def extract_from(path, traces, line_numbers)
79
+ code = {}
80
+
81
+ File.open(path) do |file|
82
+ current_line_number = 0
83
+
84
+ file.each_line do |line|
85
+ current_line_number += 1
86
+
87
+ next unless line_numbers.include?(current_line_number)
88
+
89
+ code[current_line_number] = line[0...200].rstrip
90
+ end
91
+ end
92
+
93
+ associate_code_with_trace(code, traces)
94
+ end
95
+
96
+ ##
97
+ # @param code [Hash{Integer => String}]
98
+ # @param traces [Array<Hash>]
99
+ # @return [void]
100
+ def associate_code_with_trace(code, traces)
101
+ traces.each do |trace|
102
+ trace[:code] = {}
103
+
104
+ code.each do |line_number, line|
105
+ # If we've gone past the last line we care about, we can stop iterating
106
+ break if line_number > trace[:last_line_number]
107
+
108
+ # Skip lines that aren't in the range we want
109
+ next unless line_number >= trace[:first_line_number]
110
+
111
+ trace[:code][line_number] = line
112
+ end
113
+
114
+ trim_excess_lines(trace[:code], trace[:lineNumber])
115
+ trace.delete(:first_line_number)
116
+ trace.delete(:last_line_number)
117
+ end
118
+ end
119
+
120
+ ##
121
+ # @param code [Hash{Integer => String}]
122
+ # @param line_number [Integer]
123
+ # @return [void]
124
+ def trim_excess_lines(code, line_number)
125
+ while code.length > MAXIMUM_LINES_TO_KEEP
126
+ last_line = code.keys.max
127
+ first_line = code.keys.min
128
+
129
+ if (last_line - line_number) > (line_number - first_line)
130
+ code.delete(last_line)
131
+ else
132
+ code.delete(first_line)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end