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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/VERSION +1 -1
- data/features/fixtures/docker-compose.yml +5 -1
- data/features/fixtures/plain/app/report_modification/initiators/handled_on_error.rb +10 -0
- data/features/fixtures/plain/app/report_modification/initiators/unhandled_on_error.rb +11 -0
- data/features/fixtures/plain/app/stack_frame_modification/initiators/handled_on_error.rb +29 -0
- data/features/fixtures/plain/app/stack_frame_modification/initiators/unhandled_on_error.rb +26 -0
- data/features/fixtures/rails3/app/config/initializers/bugsnag.rb +8 -0
- data/features/fixtures/rails4/app/config/initializers/bugsnag.rb +8 -0
- data/features/fixtures/rails5/app/config/initializers/bugsnag.rb +8 -0
- data/features/fixtures/rails6/app/config/initializers/bugsnag.rb +8 -0
- data/features/plain_features/add_tab.feature +7 -1
- data/features/plain_features/ignore_report.feature +2 -0
- data/features/plain_features/report_api_key.feature +3 -1
- data/features/plain_features/report_severity.feature +2 -0
- data/features/plain_features/report_stack_frames.feature +4 -0
- data/features/plain_features/report_user.feature +7 -1
- data/features/rails_features/on_error.feature +29 -0
- data/lib/bugsnag.rb +35 -0
- data/lib/bugsnag/code_extractor.rb +137 -0
- data/lib/bugsnag/configuration.rb +27 -0
- data/lib/bugsnag/middleware_stack.rb +38 -3
- data/lib/bugsnag/on_error_callbacks.rb +33 -0
- data/lib/bugsnag/report.rb +1 -1
- data/lib/bugsnag/session_tracker.rb +3 -3
- data/lib/bugsnag/stacktrace.rb +25 -68
- data/spec/code_extractor_spec.rb +129 -0
- data/spec/fixtures/crashes/file1.rb +29 -0
- data/spec/fixtures/crashes/file2.rb +25 -0
- data/spec/fixtures/crashes/file_with_long_lines.rb +7 -0
- data/spec/fixtures/crashes/functions.rb +29 -0
- data/spec/fixtures/crashes/short_file.rb +2 -0
- data/spec/on_error_spec.rb +332 -0
- data/spec/report_spec.rb +7 -4
- data/spec/spec_helper.rb +8 -0
- data/spec/stacktrace_spec.rb +276 -30
- metadata +15 -2
@@ -318,6 +318,33 @@ module Bugsnag
|
|
318
318
|
@enable_sessions = false
|
319
319
|
end
|
320
320
|
|
321
|
+
##
|
322
|
+
# Add the given callback to the list of on_error callbacks
|
323
|
+
#
|
324
|
+
# The on_error callbacks will be called when an error is captured or reported
|
325
|
+
# and are passed a {Bugsnag::Report} object
|
326
|
+
#
|
327
|
+
# Returning false from an on_error callback will cause the error to be ignored
|
328
|
+
# and will prevent any remaining callbacks from being called
|
329
|
+
#
|
330
|
+
# @param callback [Proc]
|
331
|
+
# @return [void]
|
332
|
+
def add_on_error(callback)
|
333
|
+
middleware.use(callback)
|
334
|
+
end
|
335
|
+
|
336
|
+
##
|
337
|
+
# Remove the given callback from the list of on_error callbacks
|
338
|
+
#
|
339
|
+
# Note that this must be the same Proc instance that was passed to
|
340
|
+
# {#add_on_error}, otherwise it will not be removed
|
341
|
+
#
|
342
|
+
# @param callback [Proc]
|
343
|
+
# @return [void]
|
344
|
+
def remove_on_error(callback)
|
345
|
+
middleware.remove(callback)
|
346
|
+
end
|
347
|
+
|
321
348
|
private
|
322
349
|
|
323
350
|
attr_writer :scopes_to_filter
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "bugsnag/on_error_callbacks"
|
2
|
+
|
1
3
|
module Bugsnag
|
2
4
|
class MiddlewareStack
|
3
5
|
##
|
@@ -65,11 +67,26 @@ module Bugsnag
|
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
70
|
+
##
|
71
|
+
# Disable the given middleware. This removes them from the list of
|
72
|
+
# middleware and ensures they cannot be added again
|
73
|
+
#
|
74
|
+
# See also {#remove}
|
68
75
|
def disable(*middlewares)
|
69
76
|
@mutex.synchronize do
|
70
77
|
@disabled_middleware += middlewares
|
71
78
|
|
72
|
-
@middlewares.delete_if {|m| @disabled_middleware.include?(m)}
|
79
|
+
@middlewares.delete_if {|m| @disabled_middleware.include?(m) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Remove the given middleware from the list of middleware
|
85
|
+
#
|
86
|
+
# This is like {#disable} but allows the middleware to be added again
|
87
|
+
def remove(*middlewares)
|
88
|
+
@mutex.synchronize do
|
89
|
+
@middlewares.delete_if {|m| middlewares.include?(m) }
|
73
90
|
end
|
74
91
|
end
|
75
92
|
|
@@ -91,7 +108,7 @@ module Bugsnag
|
|
91
108
|
|
92
109
|
begin
|
93
110
|
# We reverse them, so we can call "call" on the first middleware
|
94
|
-
middleware_procs.reverse.inject(notify_lambda) {
|
111
|
+
middleware_procs.reverse.inject(notify_lambda) {|n, e| e.call(n) }.call(report)
|
95
112
|
rescue StandardError => e
|
96
113
|
# KLUDGE: Since we don't re-raise middleware exceptions, this breaks rspec
|
97
114
|
raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError"
|
@@ -107,10 +124,28 @@ module Bugsnag
|
|
107
124
|
end
|
108
125
|
|
109
126
|
private
|
127
|
+
|
128
|
+
##
|
110
129
|
# Generates a list of middleware procs that are ready to be run
|
111
130
|
# Pass each one a reference to the next in the queue
|
131
|
+
#
|
132
|
+
# @return [Array<Proc>]
|
112
133
|
def middleware_procs
|
113
|
-
|
134
|
+
# Split the middleware into separate lists of Procs and Classes
|
135
|
+
procs, classes = @middlewares.partition {|middleware| middleware.is_a?(Proc) }
|
136
|
+
|
137
|
+
# Wrap the classes in a proc that, when called, news up the middleware and
|
138
|
+
# passes the next middleware in the queue
|
139
|
+
middleware_instances = classes.map do |middleware|
|
140
|
+
proc {|next_middleware| middleware.new(next_middleware) }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Wrap the list of procs in a proc that, when called, wraps them in an
|
144
|
+
# 'OnErrorCallbacks' instance that also has a reference to the next middleware
|
145
|
+
wrapped_procs = proc {|next_middleware| OnErrorCallbacks.new(next_middleware, procs) }
|
146
|
+
|
147
|
+
# Return the combined middleware and wrapped procs
|
148
|
+
middleware_instances.push(wrapped_procs)
|
114
149
|
end
|
115
150
|
end
|
116
151
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Bugsnag
|
2
|
+
# @api private
|
3
|
+
class OnErrorCallbacks
|
4
|
+
def initialize(next_middleware, callbacks)
|
5
|
+
@next_middleware = next_middleware
|
6
|
+
@callbacks = callbacks
|
7
|
+
end
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param report [Report]
|
11
|
+
def call(report)
|
12
|
+
@callbacks.each do |callback|
|
13
|
+
begin
|
14
|
+
should_continue = callback.call(report)
|
15
|
+
rescue StandardError => e
|
16
|
+
Bugsnag.configuration.warn("Error occurred in on_error callback: '#{e}'")
|
17
|
+
Bugsnag.configuration.warn("on_error callback stacktrace: #{e.backtrace.inspect}")
|
18
|
+
end
|
19
|
+
|
20
|
+
# If a callback returns false, we ignore the report and stop running callbacks
|
21
|
+
# Note that we explicitly check for 'false' so that callbacks don't need
|
22
|
+
# to return anything (i.e. can return 'nil') and we still continue
|
23
|
+
next unless should_continue == false
|
24
|
+
|
25
|
+
report.ignore!
|
26
|
+
|
27
|
+
break
|
28
|
+
end
|
29
|
+
|
30
|
+
@next_middleware.call(report)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/bugsnag/report.rb
CHANGED
@@ -182,7 +182,7 @@ module Bugsnag
|
|
182
182
|
{
|
183
183
|
errorClass: error_class(exception),
|
184
184
|
message: exception.message,
|
185
|
-
stacktrace: Stacktrace.
|
185
|
+
stacktrace: Stacktrace.process(exception.backtrace, configuration)
|
186
186
|
}
|
187
187
|
end
|
188
188
|
end
|
@@ -1,11 +1,9 @@
|
|
1
1
|
require 'thread'
|
2
2
|
require 'time'
|
3
3
|
require 'securerandom'
|
4
|
-
require 'concurrent'
|
5
4
|
|
6
5
|
module Bugsnag
|
7
6
|
class SessionTracker
|
8
|
-
|
9
7
|
THREAD_SESSION = "bugsnag_session"
|
10
8
|
SESSION_PAYLOAD_VERSION = "1.0"
|
11
9
|
MUTEX = Mutex.new
|
@@ -27,6 +25,8 @@ module Bugsnag
|
|
27
25
|
##
|
28
26
|
# Initializes the session tracker.
|
29
27
|
def initialize
|
28
|
+
require 'concurrent'
|
29
|
+
|
30
30
|
@session_counts = Concurrent::Hash.new(0)
|
31
31
|
end
|
32
32
|
|
@@ -139,4 +139,4 @@ module Bugsnag
|
|
139
139
|
Bugsnag::Delivery[Bugsnag.configuration.delivery_method].deliver(Bugsnag.configuration.session_endpoint, payload, Bugsnag.configuration, options)
|
140
140
|
end
|
141
141
|
end
|
142
|
-
end
|
142
|
+
end
|
data/lib/bugsnag/stacktrace.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
2
|
-
class Stacktrace
|
1
|
+
require_relative 'code_extractor'
|
3
2
|
|
3
|
+
module Bugsnag
|
4
|
+
module Stacktrace
|
4
5
|
# e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
|
5
6
|
BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/
|
6
7
|
|
@@ -10,13 +11,15 @@ module Bugsnag
|
|
10
11
|
##
|
11
12
|
# Process a backtrace and the configuration into a parsed stacktrace.
|
12
13
|
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
14
|
+
# @param backtrace [Array, nil] If nil, 'caller' will be used instead
|
15
|
+
# @param configuration [Configuration]
|
16
|
+
# @return [Array]
|
17
|
+
def self.process(backtrace, configuration)
|
18
|
+
code_extractor = CodeExtractor.new(configuration)
|
16
19
|
|
17
20
|
backtrace = caller if !backtrace || backtrace.empty?
|
18
21
|
|
19
|
-
|
22
|
+
processed_backtrace = backtrace.map do |trace|
|
20
23
|
# Parse the stacktrace line
|
21
24
|
if trace.match(BACKTRACE_LINE_REGEX)
|
22
25
|
file, line_str, method = [$1, $2, $3]
|
@@ -24,27 +27,25 @@ module Bugsnag
|
|
24
27
|
method, file, line_str = [$1, $2, $3]
|
25
28
|
end
|
26
29
|
|
27
|
-
next
|
30
|
+
next if file.nil?
|
28
31
|
|
29
32
|
# Expand relative paths
|
30
33
|
file = File.realpath(file) rescue file
|
31
34
|
|
32
35
|
# Generate the stacktrace line hash
|
33
|
-
trace_hash = {}
|
34
|
-
trace_hash[:lineNumber] = line_str.to_i
|
36
|
+
trace_hash = { lineNumber: line_str.to_i }
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
# Save a copy of the file path as we're about to modify it but need the
|
39
|
+
# raw version when extracting code (otherwise we can't open the file)
|
40
|
+
raw_file_path = file.dup
|
39
41
|
|
40
42
|
# Clean up the file path in the stacktrace
|
41
|
-
if defined?(
|
42
|
-
trace_hash[:inProject] = true if file.start_with?(
|
43
|
-
file.sub!(/#{
|
44
|
-
trace_hash.delete(:inProject) if file.match(
|
43
|
+
if defined?(configuration.project_root) && configuration.project_root.to_s != ''
|
44
|
+
trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
|
45
|
+
file.sub!(/#{configuration.project_root}\//, "")
|
46
|
+
trace_hash.delete(:inProject) if file.match(configuration.vendor_path)
|
45
47
|
end
|
46
48
|
|
47
|
-
|
48
49
|
# Strip common gem path prefixes
|
49
50
|
if defined?(Gem)
|
50
51
|
file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") }
|
@@ -55,60 +56,16 @@ module Bugsnag
|
|
55
56
|
# Add a method if we have it
|
56
57
|
trace_hash[:method] = method if method && (method =~ /^__bind/).nil?
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
nil
|
62
|
-
end
|
63
|
-
end.compact
|
64
|
-
end
|
65
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
66
|
-
|
67
|
-
##
|
68
|
-
# Returns the processed backtrace
|
69
|
-
def to_a
|
70
|
-
@processed_backtrace
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
59
|
+
# If we're going to send code then record the raw file path and the
|
60
|
+
# trace_hash, so we can extract from it later
|
61
|
+
code_extractor.add_file(raw_file_path, trace_hash) if configuration.send_code
|
74
62
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
from_line = [line_number - num_lines, 1].max
|
79
|
-
|
80
|
-
# don't try and open '(irb)' or '-e'
|
81
|
-
return unless File.exist?(file)
|
82
|
-
|
83
|
-
# Populate code hash with line numbers and code lines
|
84
|
-
File.open(file) do |f|
|
85
|
-
current_line_number = 0
|
86
|
-
f.each_line do |line|
|
87
|
-
current_line_number += 1
|
88
|
-
|
89
|
-
next if current_line_number < from_line
|
90
|
-
|
91
|
-
code_hash[current_line_number] = line[0...200].rstrip
|
92
|
-
|
93
|
-
break if code_hash.length >= ( num_lines * 1.5 ).ceil
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
while code_hash.length > num_lines
|
98
|
-
last_line = code_hash.keys.max
|
99
|
-
first_line = code_hash.keys.min
|
63
|
+
trace_hash
|
64
|
+
end.compact
|
100
65
|
|
101
|
-
|
102
|
-
code_hash.delete(last_line)
|
103
|
-
else
|
104
|
-
code_hash.delete(first_line)
|
105
|
-
end
|
106
|
-
end
|
66
|
+
code_extractor.extract! if configuration.send_code
|
107
67
|
|
108
|
-
|
109
|
-
rescue
|
110
|
-
@configuration.warn("Error fetching code: #{$!.inspect}")
|
111
|
-
nil
|
68
|
+
processed_backtrace
|
112
69
|
end
|
113
70
|
end
|
114
71
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Bugsnag::CodeExtractor do
|
4
|
+
it "extracts code from a file and adds it to the given hash" do
|
5
|
+
file1_hash = { lineNumber: 5 }
|
6
|
+
file2_hash = { lineNumber: 7 }
|
7
|
+
|
8
|
+
code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new)
|
9
|
+
code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash)
|
10
|
+
code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash)
|
11
|
+
|
12
|
+
code_extractor.extract!
|
13
|
+
|
14
|
+
expect(file1_hash).to eq({
|
15
|
+
lineNumber: 5,
|
16
|
+
code: {
|
17
|
+
2 => "",
|
18
|
+
3 => "module File1",
|
19
|
+
4 => " def self.foo1",
|
20
|
+
5 => " File2.foo2",
|
21
|
+
6 => " end",
|
22
|
+
7 => "",
|
23
|
+
8 => " def self.bar1"
|
24
|
+
}
|
25
|
+
})
|
26
|
+
|
27
|
+
expect(file2_hash).to eq({
|
28
|
+
lineNumber: 7,
|
29
|
+
code: {
|
30
|
+
4 => " end",
|
31
|
+
5 => "",
|
32
|
+
6 => " def self.bar2",
|
33
|
+
7 => " File1.baz1",
|
34
|
+
8 => " end",
|
35
|
+
9 => "",
|
36
|
+
10 => " def self.baz2"
|
37
|
+
}
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
it "handles extracting code from the first & last line in a file" do
|
42
|
+
file1_hash = { lineNumber: 1 }
|
43
|
+
file2_hash = { lineNumber: 25 }
|
44
|
+
|
45
|
+
code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new)
|
46
|
+
code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash)
|
47
|
+
code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash)
|
48
|
+
|
49
|
+
code_extractor.extract!
|
50
|
+
|
51
|
+
expect(file1_hash).to eq({
|
52
|
+
lineNumber: 1,
|
53
|
+
code: {
|
54
|
+
1 => "require_relative 'file2'",
|
55
|
+
2 => "",
|
56
|
+
3 => "module File1",
|
57
|
+
4 => " def self.foo1",
|
58
|
+
5 => " File2.foo2",
|
59
|
+
6 => " end",
|
60
|
+
7 => ""
|
61
|
+
}
|
62
|
+
})
|
63
|
+
|
64
|
+
expect(file2_hash).to eq({
|
65
|
+
lineNumber: 25,
|
66
|
+
code: {
|
67
|
+
19 => " puts 'abcdef2'",
|
68
|
+
20 => " end",
|
69
|
+
21 => "",
|
70
|
+
22 => " def self.abcdefghi2",
|
71
|
+
23 => " puts 'abcdefghi2'",
|
72
|
+
24 => " end",
|
73
|
+
25 => "end"
|
74
|
+
}
|
75
|
+
})
|
76
|
+
end
|
77
|
+
|
78
|
+
it "truncates lines to a maximum of 200 characters" do
|
79
|
+
hash = { lineNumber: 4 }
|
80
|
+
|
81
|
+
code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new)
|
82
|
+
code_extractor.add_file("spec/fixtures/crashes/file_with_long_lines.rb", hash)
|
83
|
+
|
84
|
+
code_extractor.extract!
|
85
|
+
|
86
|
+
# rubocop:disable Layout/LineLength
|
87
|
+
expect(hash).to eq({
|
88
|
+
lineNumber: 4,
|
89
|
+
code: {
|
90
|
+
1 => "# rubocop:disable Layout/LineLength",
|
91
|
+
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_pa",
|
92
|
+
3 => " puts 'This is a shorter string'",
|
93
|
+
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 le",
|
94
|
+
5 => " puts 'and another shorter string for comparison'",
|
95
|
+
6 => "end",
|
96
|
+
7 => "# rubocop:enable Layout/LineLength",
|
97
|
+
}
|
98
|
+
})
|
99
|
+
# rubocop:enable Layout/LineLength
|
100
|
+
end
|
101
|
+
|
102
|
+
it "rescues exceptions raised in extract!" do
|
103
|
+
file1_hash = { lineNumber: 1 }
|
104
|
+
file2_hash = { lineNumber: 25 }
|
105
|
+
|
106
|
+
code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new)
|
107
|
+
code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash)
|
108
|
+
code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash)
|
109
|
+
|
110
|
+
file1_hash[:first_line_number] = nil
|
111
|
+
|
112
|
+
code_extractor.extract!
|
113
|
+
|
114
|
+
expect(file1_hash).to eq({ lineNumber: 1, code: nil })
|
115
|
+
|
116
|
+
expect(file2_hash).to eq({
|
117
|
+
lineNumber: 25,
|
118
|
+
code: {
|
119
|
+
19 => " puts 'abcdef2'",
|
120
|
+
20 => " end",
|
121
|
+
21 => "",
|
122
|
+
22 => " def self.abcdefghi2",
|
123
|
+
23 => " puts 'abcdefghi2'",
|
124
|
+
24 => " end",
|
125
|
+
25 => "end"
|
126
|
+
}
|
127
|
+
})
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'file2'
|
2
|
+
|
3
|
+
module File1
|
4
|
+
def self.foo1
|
5
|
+
File2.foo2
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.bar1
|
9
|
+
File2.bar2
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.baz1
|
13
|
+
File2.baz2
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.abc1
|
17
|
+
puts 'abc'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.abcdef1
|
21
|
+
puts 'abcdef1'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.abcdefghi1
|
25
|
+
puts 'abcdefghi1'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
File1.foo1
|