airbrake-ruby 1.0.2 → 1.0.3
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/lib/airbrake-ruby.rb +6 -3
- data/lib/airbrake-ruby/async_sender.rb +19 -6
- data/lib/airbrake-ruby/backtrace.rb +14 -12
- data/lib/airbrake-ruby/filters/keys_filter.rb +2 -2
- data/lib/airbrake-ruby/nested_exception.rb +37 -0
- data/lib/airbrake-ruby/notice.rb +4 -27
- data/lib/airbrake-ruby/notifier.rb +14 -10
- data/lib/airbrake-ruby/sync_sender.rb +1 -1
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/async_sender_spec.rb +71 -54
- data/spec/backtrace_spec.rb +54 -16
- data/spec/nested_exception_spec.rb +77 -0
- data/spec/notice_spec.rb +0 -45
- data/spec/notifier_spec.rb +58 -22
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b51cb8b53a17fbe261374e06275f714d72dfe4c3
|
4
|
+
data.tar.gz: f3b407f5473a75890e56dde82fce32a2e13753c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9778f6f6fcd5d027d78bd5b330c490648efe0af2fc9649dc4274d46e91c7eedd8e5fe31698a195f5246cd6113fdabcc4be986f4d1c150ae013a4fdc726d48c97
|
7
|
+
data.tar.gz: aedd2549376dc458b30511d2160b024f53f77b0836c5df66cd0f73330e5b49d5cbd2dc1f1e4ccc49c89618b392104a432eaf80c1fda6dc799dc2741df216cb9c
|
data/lib/airbrake-ruby.rb
CHANGED
@@ -10,6 +10,7 @@ require 'airbrake-ruby/config'
|
|
10
10
|
require 'airbrake-ruby/sync_sender'
|
11
11
|
require 'airbrake-ruby/async_sender'
|
12
12
|
require 'airbrake-ruby/response'
|
13
|
+
require 'airbrake-ruby/nested_exception'
|
13
14
|
require 'airbrake-ruby/notice'
|
14
15
|
require 'airbrake-ruby/backtrace'
|
15
16
|
require 'airbrake-ruby/filter_chain'
|
@@ -279,6 +280,10 @@ module Airbrake
|
|
279
280
|
if @notifiers.key?(notifier)
|
280
281
|
@notifiers[notifier].__send__(method, *args, &block)
|
281
282
|
else
|
283
|
+
# If we raise SystemExit, the Ruby process can gracefully quit without
|
284
|
+
# the unwanted Airbrake::Error.
|
285
|
+
raise args.first if args.first.class == SystemExit
|
286
|
+
|
282
287
|
raise Airbrake::Error,
|
283
288
|
"the '#{notifier}' notifier isn't configured"
|
284
289
|
end
|
@@ -288,7 +293,5 @@ end
|
|
288
293
|
|
289
294
|
# Notify of unhandled exceptions, if there were any, but ignore SystemExit.
|
290
295
|
at_exit do
|
291
|
-
|
292
|
-
Airbrake.notify_sync($ERROR_INFO)
|
293
|
-
end
|
296
|
+
Airbrake.notify_sync($ERROR_INFO) if $ERROR_INFO
|
294
297
|
end
|
@@ -14,9 +14,7 @@ module Airbrake
|
|
14
14
|
@sender = SyncSender.new(config)
|
15
15
|
@closed = false
|
16
16
|
@workers = ThreadGroup.new
|
17
|
-
|
18
|
-
(0...config.workers).each { @workers.add(spawn_worker) }
|
19
|
-
@workers.enclose
|
17
|
+
@pid = nil
|
20
18
|
end
|
21
19
|
|
22
20
|
##
|
@@ -67,18 +65,33 @@ module Airbrake
|
|
67
65
|
# went wrong.
|
68
66
|
#
|
69
67
|
# Workers are expected to crash when you +fork+ the process the workers are
|
70
|
-
# living in.
|
71
|
-
#
|
72
|
-
#
|
68
|
+
# living in. In this case we detect a +fork+ and try to revive them here.
|
69
|
+
#
|
70
|
+
# Another possible scenario that crashes workers is when you close the
|
71
|
+
# instance on +at_exit+, but some other +at_exit+ hook prevents the process
|
72
|
+
# from exiting.
|
73
73
|
#
|
74
74
|
# @return [Boolean] true if an instance wasn't closed, but has no workers
|
75
75
|
# @see https://goo.gl/oydz8h Example of at_exit that prevents exit
|
76
76
|
def has_workers?
|
77
|
+
return false if @closed
|
78
|
+
|
79
|
+
if @pid != Process.pid && @workers.list.empty?
|
80
|
+
@pid = Process.pid
|
81
|
+
spawn_workers
|
82
|
+
end
|
83
|
+
|
77
84
|
!@closed && @workers.list.any?
|
78
85
|
end
|
79
86
|
|
80
87
|
private
|
81
88
|
|
89
|
+
def spawn_workers
|
90
|
+
@workers = ThreadGroup.new
|
91
|
+
@config.workers.times { @workers.add(spawn_worker) }
|
92
|
+
@workers.enclose
|
93
|
+
end
|
94
|
+
|
82
95
|
def spawn_worker
|
83
96
|
Thread.new do
|
84
97
|
while (notice = @unsent.pop) != :stop
|
@@ -19,7 +19,7 @@ module Airbrake
|
|
19
19
|
:
|
20
20
|
(?<line>\d+) # Matches '43'
|
21
21
|
:in\s
|
22
|
-
`(?<function
|
22
|
+
`(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
|
23
23
|
\z}x
|
24
24
|
|
25
25
|
##
|
@@ -38,10 +38,10 @@ module Airbrake
|
|
38
38
|
# @return [Regexp] the template that tries to assume what a generic stack
|
39
39
|
# frame might look like, when exception's backtrace is set manually.
|
40
40
|
GENERIC_STACKFRAME_REGEXP = %r{\A
|
41
|
-
(?<file>.+)
|
41
|
+
(?<file>.+) # Matches '/foo/bar/baz.ext'
|
42
42
|
:
|
43
|
-
(?<line>\d+)
|
44
|
-
(?<function
|
43
|
+
(?<line>\d+)? # Matches '43' or nothing
|
44
|
+
(in\s`(?<function>.+)')? # Matches "in `func'" or nothing
|
45
45
|
\z}x
|
46
46
|
|
47
47
|
##
|
@@ -51,13 +51,15 @@ module Airbrake
|
|
51
51
|
# parse
|
52
52
|
# @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
|
53
53
|
def self.parse(exception)
|
54
|
+
return [] if exception.backtrace.nil? || exception.backtrace.none?
|
55
|
+
|
54
56
|
regexp = if java_exception?(exception)
|
55
57
|
JAVA_STACKFRAME_REGEXP
|
56
58
|
else
|
57
59
|
RUBY_STACKFRAME_REGEXP
|
58
60
|
end
|
59
61
|
|
60
|
-
|
62
|
+
exception.backtrace.map do |stackframe|
|
61
63
|
stack_frame(match_frame(regexp, stackframe))
|
62
64
|
end
|
63
65
|
end
|
@@ -80,16 +82,16 @@ module Airbrake
|
|
80
82
|
line: (Integer(match[:line]) if match[:line]),
|
81
83
|
function: match[:function] }
|
82
84
|
end
|
83
|
-
end
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
|
86
|
+
def match_frame(regexp, stackframe)
|
87
|
+
match = regexp.match(stackframe)
|
88
|
+
return match if match
|
88
89
|
|
89
|
-
|
90
|
-
|
90
|
+
match = GENERIC_STACKFRAME_REGEXP.match(stackframe)
|
91
|
+
return match if match
|
91
92
|
|
92
|
-
|
93
|
+
raise Airbrake::Error, "can't parse '#{stackframe}'"
|
94
|
+
end
|
93
95
|
end
|
94
96
|
end
|
95
97
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# A class that is capable of unwinding nested exceptions and representing them
|
4
|
+
# as JSON-like hash.
|
5
|
+
class NestedException
|
6
|
+
##
|
7
|
+
# @return [Integer] the maximum number of nested exceptions that a notice
|
8
|
+
# can unwrap. Exceptions that have a longer cause chain will be ignored
|
9
|
+
MAX_NESTED_EXCEPTIONS = 3
|
10
|
+
|
11
|
+
def initialize(exception)
|
12
|
+
@exception = exception
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json
|
16
|
+
unwind_exceptions.map do |exception|
|
17
|
+
{ type: exception.class.name,
|
18
|
+
message: exception.message,
|
19
|
+
backtrace: Backtrace.parse(exception) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def unwind_exceptions
|
26
|
+
exception_list = []
|
27
|
+
exception = @exception
|
28
|
+
|
29
|
+
while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
|
30
|
+
exception_list << exception
|
31
|
+
exception = (exception.cause if exception.respond_to?(:cause))
|
32
|
+
end
|
33
|
+
|
34
|
+
exception_list
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/airbrake-ruby/notice.rb
CHANGED
@@ -23,11 +23,6 @@ module Airbrake
|
|
23
23
|
# @return [Integer] the maxium size of the JSON payload in bytes
|
24
24
|
MAX_NOTICE_SIZE = 64000
|
25
25
|
|
26
|
-
##
|
27
|
-
# @return [Integer] the maximum number of nested exceptions that a notice
|
28
|
-
# can unwrap. Exceptions that have a longer cause chain will be ignored
|
29
|
-
MAX_NESTED_EXCEPTIONS = 3
|
30
|
-
|
31
26
|
##
|
32
27
|
# @return [Integer] the maximum size of hashes, arrays and strings in the
|
33
28
|
# notice.
|
@@ -41,7 +36,7 @@ module Airbrake
|
|
41
36
|
NotImplementedError,
|
42
37
|
JSON::GeneratorError,
|
43
38
|
Encoding::UndefinedConversionError
|
44
|
-
]
|
39
|
+
].freeze
|
45
40
|
|
46
41
|
# @return [Array<Symbol>] the list of keys that can be be overwritten with
|
47
42
|
# {Airbrake::Notice#[]=}
|
@@ -51,7 +46,7 @@ module Airbrake
|
|
51
46
|
:environment,
|
52
47
|
:session,
|
53
48
|
:params
|
54
|
-
]
|
49
|
+
].freeze
|
55
50
|
|
56
51
|
def initialize(config, exception, params = {})
|
57
52
|
@config = config
|
@@ -61,7 +56,7 @@ module Airbrake
|
|
61
56
|
}.freeze
|
62
57
|
|
63
58
|
@modifiable_payload = {
|
64
|
-
errors:
|
59
|
+
errors: NestedException.new(exception).as_json,
|
65
60
|
context: context(params),
|
66
61
|
environment: {},
|
67
62
|
session: {},
|
@@ -155,7 +150,7 @@ module Airbrake
|
|
155
150
|
end
|
156
151
|
|
157
152
|
def raise_if_ignored
|
158
|
-
return unless
|
153
|
+
return unless ignored?
|
159
154
|
raise Airbrake::Error, 'cannot access ignored notice'
|
160
155
|
end
|
161
156
|
|
@@ -174,24 +169,6 @@ module Airbrake
|
|
174
169
|
@modifiable_payload.merge(@private_payload)
|
175
170
|
end
|
176
171
|
|
177
|
-
def errors(exception)
|
178
|
-
exception_list = []
|
179
|
-
|
180
|
-
while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
|
181
|
-
exception_list << exception
|
182
|
-
|
183
|
-
exception = if exception.respond_to?(:cause) && exception.cause
|
184
|
-
exception.cause
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
exception_list.map do |e|
|
189
|
-
{ type: e.class.name,
|
190
|
-
message: e.message,
|
191
|
-
backtrace: Backtrace.parse(e) }
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
172
|
def truncate_payload
|
196
173
|
@modifiable_payload[:errors].each do |error|
|
197
174
|
@truncator.truncate_error(error)
|
@@ -127,19 +127,23 @@ module Airbrake
|
|
127
127
|
end
|
128
128
|
|
129
129
|
def default_sender
|
130
|
-
if @async_sender.has_workers?
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
@sync_sender
|
138
|
-
end
|
130
|
+
return @async_sender if @async_sender.has_workers?
|
131
|
+
|
132
|
+
@config.logger.warn(
|
133
|
+
"#{LOG_LABEL} falling back to sync delivery because there are no " \
|
134
|
+
"running async workers"
|
135
|
+
)
|
136
|
+
@sync_sender
|
139
137
|
end
|
140
138
|
|
141
139
|
def clean_backtrace
|
142
|
-
|
140
|
+
caller_copy = Kernel.caller
|
141
|
+
clean_bt = caller_copy.drop_while { |frame| frame.include?('/lib/airbrake') }
|
142
|
+
|
143
|
+
# If true, then it's likely an internal library error. In this case return
|
144
|
+
# at least some backtrace to simplify debugging.
|
145
|
+
return caller_copy if clean_bt.empty?
|
146
|
+
clean_bt
|
143
147
|
end
|
144
148
|
end
|
145
149
|
end
|
data/spec/async_sender_spec.rb
CHANGED
@@ -3,52 +3,23 @@ require 'spec_helper'
|
|
3
3
|
RSpec.describe Airbrake::AsyncSender do
|
4
4
|
before do
|
5
5
|
stub_request(:post, /.*/).to_return(status: 201, body: '{}')
|
6
|
-
@sender = described_class.new(Airbrake::Config.new)
|
7
|
-
@workers = @sender.instance_variable_get(:@workers)
|
8
6
|
end
|
9
7
|
|
10
|
-
describe "#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
expect(@workers.list.size).to eq(1)
|
23
|
-
|
24
|
-
sender = described_class.new(config)
|
25
|
-
workers = sender.instance_variable_get(:@workers)
|
26
|
-
|
27
|
-
expect(workers.list.size).to eq(new_workers)
|
28
|
-
sender.close
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
context "queue" do
|
33
|
-
before do
|
34
|
-
@stdout = StringIO.new
|
35
|
-
end
|
36
|
-
|
37
|
-
let(:notices) { 1000 }
|
38
|
-
|
39
|
-
let(:config) do
|
40
|
-
Airbrake::Config.new(logger: Logger.new(@stdout), workers: 3, queue_size: 10)
|
41
|
-
end
|
42
|
-
|
43
|
-
it "limits the size of the queue, but still sends all notices" do
|
44
|
-
sender = described_class.new(config)
|
45
|
-
|
46
|
-
notices.times { |i| sender.send(i) }
|
47
|
-
sender.close
|
8
|
+
describe "#send" do
|
9
|
+
it "limits the size of the queue, but still sends all notices" do
|
10
|
+
stdout = StringIO.new
|
11
|
+
notices_count = 1000
|
12
|
+
config = Airbrake::Config.new(
|
13
|
+
logger: Logger.new(stdout), workers: 3, queue_size: 10
|
14
|
+
)
|
15
|
+
sender = described_class.new(config)
|
16
|
+
expect(sender).to have_workers
|
17
|
+
|
18
|
+
notices_count.times { |i| sender.send(i) }
|
19
|
+
sender.close
|
48
20
|
|
49
|
-
|
50
|
-
|
51
|
-
end
|
21
|
+
log = stdout.string.split("\n")
|
22
|
+
expect(log.grep(/\*\*Airbrake: \{\}/).size).to eq(notices_count)
|
52
23
|
end
|
53
24
|
end
|
54
25
|
|
@@ -57,14 +28,16 @@ RSpec.describe Airbrake::AsyncSender do
|
|
57
28
|
@stderr = StringIO.new
|
58
29
|
config = Airbrake::Config.new(logger: Logger.new(@stderr))
|
59
30
|
@sender = described_class.new(config)
|
60
|
-
@
|
31
|
+
expect(@sender).to have_workers
|
61
32
|
end
|
62
33
|
|
63
34
|
context "when there are no unsent notices" do
|
64
35
|
it "joins the spawned thread" do
|
65
|
-
|
36
|
+
workers = @sender.instance_variable_get(:@workers).list
|
37
|
+
|
38
|
+
expect(workers).to all(be_alive)
|
66
39
|
@sender.close
|
67
|
-
expect(
|
40
|
+
expect(workers).to all(be_stop)
|
68
41
|
end
|
69
42
|
end
|
70
43
|
|
@@ -91,31 +64,75 @@ RSpec.describe Airbrake::AsyncSender do
|
|
91
64
|
|
92
65
|
context "when it was already closed" do
|
93
66
|
it "doesn't increase the unsent queue size" do
|
94
|
-
|
67
|
+
begin
|
68
|
+
@sender.close
|
69
|
+
rescue Airbrake::Error
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
95
73
|
expect(@sender.instance_variable_get(:@unsent).size).to be_zero
|
74
|
+
end
|
96
75
|
|
76
|
+
it "raises error" do
|
77
|
+
@sender.close
|
78
|
+
|
79
|
+
expect(@sender).to be_closed
|
97
80
|
expect { @sender.close }.
|
98
81
|
to raise_error(Airbrake::Error, 'attempted to close already closed sender')
|
99
82
|
end
|
100
83
|
end
|
84
|
+
|
85
|
+
context "when workers were not spawned" do
|
86
|
+
it "correctly closes the notifier nevertheless" do
|
87
|
+
sender = described_class.new(Airbrake::Config.new)
|
88
|
+
sender.close
|
89
|
+
|
90
|
+
expect(sender).to be_closed
|
91
|
+
end
|
92
|
+
end
|
101
93
|
end
|
102
94
|
|
103
95
|
describe "#has_workers?" do
|
104
|
-
|
105
|
-
sender = described_class.new(Airbrake::Config.new)
|
106
|
-
expect(sender
|
96
|
+
before do
|
97
|
+
@sender = described_class.new(Airbrake::Config.new)
|
98
|
+
expect(@sender).to have_workers
|
99
|
+
end
|
107
100
|
|
108
|
-
|
101
|
+
it "returns false when the sender is not closed, but has 0 workers" do
|
102
|
+
@sender.instance_variable_get(:@workers).list.each(&:kill)
|
109
103
|
sleep 1
|
110
|
-
expect(sender
|
104
|
+
expect(@sender).not_to have_workers
|
111
105
|
end
|
112
106
|
|
113
107
|
it "returns false when the sender is closed" do
|
108
|
+
@sender.close
|
109
|
+
expect(@sender).not_to have_workers
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#spawn_workers" do
|
114
|
+
it "spawns alive threads in an enclosed ThreadGroup" do
|
114
115
|
sender = described_class.new(Airbrake::Config.new)
|
115
|
-
expect(sender
|
116
|
+
expect(sender).to have_workers
|
117
|
+
|
118
|
+
workers = sender.instance_variable_get(:@workers)
|
119
|
+
|
120
|
+
expect(workers).to be_a(ThreadGroup)
|
121
|
+
expect(workers.list).to all(be_alive)
|
122
|
+
expect(workers).to be_enclosed
|
123
|
+
|
124
|
+
sender.close
|
125
|
+
end
|
126
|
+
|
127
|
+
it "spawns exactly config.workers workers" do
|
128
|
+
workers_count = 5
|
129
|
+
sender = described_class.new(Airbrake::Config.new(workers: workers_count))
|
130
|
+
expect(sender).to have_workers
|
131
|
+
|
132
|
+
workers = sender.instance_variable_get(:@workers)
|
116
133
|
|
134
|
+
expect(workers.list.size).to eq(workers_count)
|
117
135
|
sender.close
|
118
|
-
expect(sender.has_workers?).to be_falsey
|
119
136
|
end
|
120
137
|
end
|
121
138
|
end
|
data/spec/backtrace_spec.rb
CHANGED
@@ -75,26 +75,46 @@ RSpec.describe Airbrake::Backtrace do
|
|
75
75
|
end
|
76
76
|
|
77
77
|
context "generic backtrace" do
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
context "when function is absent" do
|
79
|
+
# rubocop:disable Metrics/LineLength
|
80
|
+
let(:generic_bt) do
|
81
|
+
["/home/bingo/bango/assets/stylesheets/error_pages.scss:139:in `animation'",
|
82
|
+
"/home/bingo/bango/assets/stylesheets/error_pages.scss:139",
|
83
|
+
"/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb:349:in `block in visit_mixin'"]
|
84
|
+
end
|
85
|
+
# rubocop:enable Metrics/LineLength
|
86
|
+
|
87
|
+
let(:ex) { AirbrakeTestError.new.tap { |e| e.set_backtrace(generic_bt) } }
|
88
|
+
|
89
|
+
let(:parsed_backtrace) do
|
90
|
+
# rubocop:disable Metrics/LineLength, Style/HashSyntax, Style/SpaceInsideHashLiteralBraces, Style/SpaceAroundOperators
|
91
|
+
[{:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>"animation"},
|
92
|
+
{:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>nil},
|
93
|
+
{:file=>"/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb", :line=>349, :function=>"block in visit_mixin"}]
|
94
|
+
# rubocop:enable Metrics/LineLength, Style/HashSyntax, Style/SpaceInsideHashLiteralBraces, Style/SpaceAroundOperators
|
95
|
+
end
|
96
|
+
|
97
|
+
it "returns a properly formatted array of hashes" do
|
98
|
+
expect(described_class.parse(ex)).to eq(parsed_backtrace)
|
99
|
+
end
|
83
100
|
end
|
84
|
-
# rubocop:enable Metrics/LineLength
|
85
101
|
|
86
|
-
|
102
|
+
context "when line is absent" do
|
103
|
+
let(:generic_bt) do
|
104
|
+
["/Users/grammakov/repositories/weintervene/config.ru:in `new'"]
|
105
|
+
end
|
87
106
|
|
88
|
-
|
89
|
-
# rubocop:disable Metrics/LineLength, Style/HashSyntax, Style/SpaceInsideHashLiteralBraces, Style/SpaceAroundOperators
|
90
|
-
[{:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>"animation"},
|
91
|
-
{:file=>"/home/bingo/bango/assets/stylesheets/error_pages.scss", :line=>139, :function=>""},
|
92
|
-
{:file=>"/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb", :line=>349, :function=>"block in visit_mixin"}]
|
93
|
-
# rubocop:enable Metrics/LineLength, Style/HashSyntax, Style/SpaceInsideHashLiteralBraces, Style/SpaceAroundOperators
|
94
|
-
end
|
107
|
+
let(:ex) { AirbrakeTestError.new.tap { |e| e.set_backtrace(generic_bt) } }
|
95
108
|
|
96
|
-
|
97
|
-
|
109
|
+
let(:parsed_backtrace) do
|
110
|
+
[{ file: '/Users/grammakov/repositories/weintervene/config.ru',
|
111
|
+
line: nil,
|
112
|
+
function: 'new' }]
|
113
|
+
end
|
114
|
+
|
115
|
+
it "returns a properly formatted array of hashes" do
|
116
|
+
expect(described_class.parse(ex)).to eq(parsed_backtrace)
|
117
|
+
end
|
98
118
|
end
|
99
119
|
end
|
100
120
|
|
@@ -108,5 +128,23 @@ RSpec.describe Airbrake::Backtrace do
|
|
108
128
|
to raise_error(Airbrake::Error, /can't parse/)
|
109
129
|
end
|
110
130
|
end
|
131
|
+
|
132
|
+
context "given a backtrace with an empty function" do
|
133
|
+
let(:bt) do
|
134
|
+
["/airbrake-ruby/vendor/jruby/1.9/gems/rspec-core-3.4.1/exe/rspec:3:in `'"]
|
135
|
+
end
|
136
|
+
|
137
|
+
let(:ex) { AirbrakeTestError.new.tap { |e| e.set_backtrace(bt) } }
|
138
|
+
|
139
|
+
let(:parsed_backtrace) do
|
140
|
+
[{ file: '/airbrake-ruby/vendor/jruby/1.9/gems/rspec-core-3.4.1/exe/rspec',
|
141
|
+
line: 3,
|
142
|
+
function: '' }]
|
143
|
+
end
|
144
|
+
|
145
|
+
it "returns a properly formatted array of hashes" do
|
146
|
+
expect(described_class.parse(ex)).to eq(parsed_backtrace)
|
147
|
+
end
|
148
|
+
end
|
111
149
|
end
|
112
150
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::NestedException do
|
4
|
+
let(:config) { Airbrake::Config.new }
|
5
|
+
|
6
|
+
describe "#as_json" do
|
7
|
+
context "given exceptions with backtraces" do
|
8
|
+
it "unwinds nested exceptions" do
|
9
|
+
begin
|
10
|
+
begin
|
11
|
+
raise AirbrakeTestError
|
12
|
+
rescue AirbrakeTestError
|
13
|
+
Ruby21Error.raise_error('bingo')
|
14
|
+
end
|
15
|
+
rescue Ruby21Error => ex
|
16
|
+
nested_exception = described_class.new(ex)
|
17
|
+
exceptions = nested_exception.as_json
|
18
|
+
|
19
|
+
expect(exceptions.size).to eq(2)
|
20
|
+
expect(exceptions[0][:message]).to eq('bingo')
|
21
|
+
expect(exceptions[1][:message]).to eq('App crashed!')
|
22
|
+
expect(exceptions[0][:backtrace]).not_to be_empty
|
23
|
+
expect(exceptions[1][:backtrace]).not_to be_empty
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "unwinds no more than 3 nested exceptions" do
|
28
|
+
begin
|
29
|
+
begin
|
30
|
+
raise AirbrakeTestError
|
31
|
+
rescue AirbrakeTestError
|
32
|
+
begin
|
33
|
+
Ruby21Error.raise_error('bongo')
|
34
|
+
rescue Ruby21Error
|
35
|
+
begin
|
36
|
+
Ruby21Error.raise_error('bango')
|
37
|
+
rescue Ruby21Error
|
38
|
+
Ruby21Error.raise_error('bingo')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue Ruby21Error => ex
|
43
|
+
nested_exception = described_class.new(ex)
|
44
|
+
exceptions = nested_exception.as_json
|
45
|
+
|
46
|
+
expect(exceptions.size).to eq(3)
|
47
|
+
expect(exceptions[0][:message]).to eq('bingo')
|
48
|
+
expect(exceptions[1][:message]).to eq('bango')
|
49
|
+
expect(exceptions[2][:message]).to eq('bongo')
|
50
|
+
expect(exceptions[0][:backtrace]).not_to be_empty
|
51
|
+
expect(exceptions[1][:backtrace]).not_to be_empty
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "given exceptions without backtraces" do
|
57
|
+
it "sets backtrace to nil" do
|
58
|
+
begin
|
59
|
+
begin
|
60
|
+
raise AirbrakeTestError
|
61
|
+
rescue AirbrakeTestError => ex2
|
62
|
+
ex2.set_backtrace([])
|
63
|
+
Ruby21Error.raise_error('bingo')
|
64
|
+
end
|
65
|
+
rescue Ruby21Error => ex1
|
66
|
+
ex1.set_backtrace([])
|
67
|
+
nested_exception = described_class.new(ex1)
|
68
|
+
exceptions = nested_exception.as_json
|
69
|
+
|
70
|
+
expect(exceptions.size).to eq(2)
|
71
|
+
expect(exceptions[0][:backtrace]).to be_empty
|
72
|
+
expect(exceptions[1][:backtrace]).to be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/spec/notice_spec.rb
CHANGED
@@ -5,51 +5,6 @@ RSpec.describe Airbrake::Notice do
|
|
5
5
|
described_class.new(Airbrake::Config.new, AirbrakeTestError.new, bingo: '1')
|
6
6
|
end
|
7
7
|
|
8
|
-
describe "#new" do
|
9
|
-
context "nested exceptions" do
|
10
|
-
it "unwinds nested exceptions" do
|
11
|
-
begin
|
12
|
-
begin
|
13
|
-
raise AirbrakeTestError
|
14
|
-
rescue AirbrakeTestError
|
15
|
-
Ruby21Error.raise_error('bingo')
|
16
|
-
end
|
17
|
-
rescue Ruby21Error => ex
|
18
|
-
notice = described_class.new(Airbrake::Config.new, ex)
|
19
|
-
|
20
|
-
expect(notice[:errors].size).to eq(2)
|
21
|
-
expect(notice[:errors][0][:message]).to eq('bingo')
|
22
|
-
expect(notice[:errors][1][:message]).to eq('App crashed!')
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
it "unwinds no more than 3 nested exceptions" do
|
27
|
-
begin
|
28
|
-
begin
|
29
|
-
raise AirbrakeTestError
|
30
|
-
rescue AirbrakeTestError
|
31
|
-
begin
|
32
|
-
Ruby21Error.raise_error('bongo')
|
33
|
-
rescue Ruby21Error
|
34
|
-
begin
|
35
|
-
Ruby21Error.raise_error('bango')
|
36
|
-
rescue Ruby21Error
|
37
|
-
Ruby21Error.raise_error('bingo')
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
rescue Ruby21Error => ex
|
42
|
-
notice = described_class.new(Airbrake::Config.new, ex)
|
43
|
-
|
44
|
-
expect(notice[:errors].size).to eq(3)
|
45
|
-
expect(notice[:errors][0][:message]).to eq('bingo')
|
46
|
-
expect(notice[:errors][1][:message]).to eq('bango')
|
47
|
-
expect(notice[:errors][2][:message]).to eq('bongo')
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
8
|
describe "#to_json" do
|
54
9
|
context "app_version" do
|
55
10
|
context "when missing" do
|
data/spec/notifier_spec.rb
CHANGED
@@ -375,18 +375,40 @@ RSpec.describe Airbrake::Notifier do
|
|
375
375
|
|
376
376
|
it "falls back to synchronous delivery when the async sender is dead" do
|
377
377
|
out = StringIO.new
|
378
|
+
notifier = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
|
379
|
+
async_sender = notifier.instance_variable_get(:@async_sender)
|
378
380
|
|
379
|
-
|
380
|
-
|
381
|
-
instance_variable_get(:@async_sender).
|
382
|
-
instance_variable_get(:@workers).
|
383
|
-
list.
|
384
|
-
each(&:kill)
|
385
|
-
|
381
|
+
expect(async_sender).to have_workers
|
382
|
+
async_sender.instance_variable_get(:@workers).list.each(&:kill)
|
386
383
|
sleep 1
|
384
|
+
expect(async_sender).not_to have_workers
|
387
385
|
|
388
|
-
|
386
|
+
notifier.notify('bango')
|
389
387
|
expect(out.string).to match(/falling back to sync delivery/)
|
388
|
+
|
389
|
+
notifier.close
|
390
|
+
end
|
391
|
+
|
392
|
+
it "respawns workers on fork()", skip: %w(jruby rbx).include?(RUBY_ENGINE) do
|
393
|
+
out = StringIO.new
|
394
|
+
notifier = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
|
395
|
+
|
396
|
+
notifier.notify('bingo', bingo: 'bango')
|
397
|
+
sleep 1
|
398
|
+
expect(out.string).not_to match(/falling back to sync delivery/)
|
399
|
+
expect_a_request_with_body(/"bingo":"bango"/)
|
400
|
+
|
401
|
+
pid = fork do
|
402
|
+
expect(notifier.instance_variable_get(:@async_sender)).to have_workers
|
403
|
+
notifier.notify('bango', bongo: 'bish')
|
404
|
+
sleep 1
|
405
|
+
expect(out.string).not_to match(/falling back to sync delivery/)
|
406
|
+
expect_a_request_with_body(/"bingo":"bango"/)
|
407
|
+
end
|
408
|
+
|
409
|
+
Process.wait(pid)
|
410
|
+
notifier.close
|
411
|
+
expect(notifier.instance_variable_get(:@async_sender)).not_to have_workers
|
390
412
|
end
|
391
413
|
end
|
392
414
|
|
@@ -645,28 +667,42 @@ RSpec.describe Airbrake::Notifier do
|
|
645
667
|
it "builds a notice from exception" do
|
646
668
|
expect(@airbrake.build_notice(ex)).to be_an Airbrake::Notice
|
647
669
|
end
|
670
|
+
|
671
|
+
context "given a non-exception with calculated internal frames only" do
|
672
|
+
it "returns the internal frames nevertheless" do
|
673
|
+
backtrace = [
|
674
|
+
"/airbrake-ruby/lib/airbrake-ruby/notifier.rb:84:in `build_notice'",
|
675
|
+
"/airbrake-ruby/lib/airbrake-ruby/notifier.rb:124:in `send_notice'",
|
676
|
+
"/airbrake-ruby/lib/airbrake-ruby/notifier.rb:52:in `notify_sync'"
|
677
|
+
]
|
678
|
+
|
679
|
+
# rubocop:disable Metrics/LineLength
|
680
|
+
parsed_backtrace = [
|
681
|
+
{ file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 84, function: 'build_notice' },
|
682
|
+
{ file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 124, function: 'send_notice' },
|
683
|
+
{ file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 52, function: 'notify_sync' }
|
684
|
+
]
|
685
|
+
# rubocop:enable Metrics/LineLength
|
686
|
+
|
687
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
688
|
+
|
689
|
+
notice = @airbrake.build_notice('bingo')
|
690
|
+
expect(notice[:errors][0][:backtrace]).to eq(parsed_backtrace)
|
691
|
+
end
|
692
|
+
end
|
648
693
|
end
|
649
694
|
|
650
695
|
describe "#close" do
|
651
|
-
|
696
|
+
context "when using #notify on a closed notifier" do
|
652
697
|
it "raises error" do
|
653
|
-
|
654
|
-
|
698
|
+
notifier = described_class.new(airbrake_params)
|
699
|
+
notifier.close
|
700
|
+
|
701
|
+
expect { notifier.notify(AirbrakeTestError.new) }.
|
655
702
|
to raise_error(Airbrake::Error, /closed Airbrake instance/)
|
656
703
|
end
|
657
704
|
end
|
658
705
|
|
659
|
-
context "when using #notify" do
|
660
|
-
include_examples 'close', proc { |a| a.notify(AirbrakeTestError.new) }
|
661
|
-
end
|
662
|
-
|
663
|
-
context "when using #send_notice" do
|
664
|
-
include_examples 'close', proc { |a|
|
665
|
-
notice = a.build_notice(AirbrakeTestError.new)
|
666
|
-
a.send_notice(notice)
|
667
|
-
}
|
668
|
-
end
|
669
|
-
|
670
706
|
context "at program exit when it was closed manually" do
|
671
707
|
it "doesn't raise error", skip: RUBY_ENGINE == 'jruby' do
|
672
708
|
expect do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: airbrake-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Airbrake Technologies, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/airbrake-ruby/filters/keys_blacklist.rb
|
105
105
|
- lib/airbrake-ruby/filters/keys_filter.rb
|
106
106
|
- lib/airbrake-ruby/filters/keys_whitelist.rb
|
107
|
+
- lib/airbrake-ruby/nested_exception.rb
|
107
108
|
- lib/airbrake-ruby/notice.rb
|
108
109
|
- lib/airbrake-ruby/notifier.rb
|
109
110
|
- lib/airbrake-ruby/payload_truncator.rb
|
@@ -115,6 +116,7 @@ files:
|
|
115
116
|
- spec/backtrace_spec.rb
|
116
117
|
- spec/config_spec.rb
|
117
118
|
- spec/filter_chain_spec.rb
|
119
|
+
- spec/nested_exception_spec.rb
|
118
120
|
- spec/notice_spec.rb
|
119
121
|
- spec/notifier_spec.rb
|
120
122
|
- spec/notifier_spec/options_spec.rb
|
@@ -140,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
142
|
version: '0'
|
141
143
|
requirements: []
|
142
144
|
rubyforge_project:
|
143
|
-
rubygems_version: 2.
|
145
|
+
rubygems_version: 2.5.1
|
144
146
|
signing_key:
|
145
147
|
specification_version: 4
|
146
148
|
summary: Ruby notifier for https://airbrake.io
|
@@ -152,7 +154,7 @@ test_files:
|
|
152
154
|
- spec/payload_truncator_spec.rb
|
153
155
|
- spec/airbrake_spec.rb
|
154
156
|
- spec/backtrace_spec.rb
|
157
|
+
- spec/nested_exception_spec.rb
|
155
158
|
- spec/notice_spec.rb
|
156
159
|
- spec/config_spec.rb
|
157
160
|
- spec/filter_chain_spec.rb
|
158
|
-
has_rdoc:
|