airbrake-ruby 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|