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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c3ab998a044375a07a36e730b7f1876fbb98c1dd
4
- data.tar.gz: 0efc3b61148d9907a3a1e054a7193a1080348b78
3
+ metadata.gz: b51cb8b53a17fbe261374e06275f714d72dfe4c3
4
+ data.tar.gz: f3b407f5473a75890e56dde82fce32a2e13753c9
5
5
  SHA512:
6
- metadata.gz: cae2f77310888cb6f70a1839842f23d3488c3c81baf75f53a5455e32dab313827b589ba70fd40f090803a1ac8cee677090721ebdfed8c46f78dcb3259bb92630
7
- data.tar.gz: 9835f816359aeeab2bca4e234fa9c2156e1a37ddcbd6fd1eade10fc10f4f414ec2faf3f5b595abb445ca4f7e9c98f69b3dc9e76759a9f64d19a23998ee136bba
6
+ metadata.gz: 9778f6f6fcd5d027d78bd5b330c490648efe0af2fc9649dc4274d46e91c7eedd8e5fe31698a195f5246cd6113fdabcc4be986f4d1c150ae013a4fdc726d48c97
7
+ data.tar.gz: aedd2549376dc458b30511d2160b024f53f77b0836c5df66cd0f73330e5b49d5cbd2dc1f1e4ccc49c89618b392104a432eaf80c1fda6dc799dc2741df216cb9c
@@ -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
- if $ERROR_INFO && $ERROR_INFO.class != SystemExit
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. Another possible scenario is when you close the instance on
71
- # +at_exit+, but some other +at_exit+ hook prevents the process from
72
- # exiting.
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>.+)' # Matches "`block (3 levels) in <top (required)>'"
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>.+) # Matches '/foo/bar/baz.ext'
41
+ (?<file>.+) # Matches '/foo/bar/baz.ext'
42
42
  :
43
- (?<line>\d+) # Matches '43'
44
- (?<function>) # No-op
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
- (exception.backtrace || []).map do |stackframe|
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
- def self.match_frame(regexp, stackframe)
86
- match = regexp.match(stackframe)
87
- return match if match
86
+ def match_frame(regexp, stackframe)
87
+ match = regexp.match(stackframe)
88
+ return match if match
88
89
 
89
- match = GENERIC_STACKFRAME_REGEXP.match(stackframe)
90
- return match if match
90
+ match = GENERIC_STACKFRAME_REGEXP.match(stackframe)
91
+ return match if match
91
92
 
92
- raise Airbrake::Error, "can't parse '#{stackframe}'"
93
+ raise Airbrake::Error, "can't parse '#{stackframe}'"
94
+ end
93
95
  end
94
96
  end
95
97
  end
@@ -47,8 +47,8 @@ module Airbrake
47
47
  hash.each_key do |key|
48
48
  if should_filter?(key)
49
49
  hash[key] = '[Filtered]'.freeze
50
- else
51
- filter_hash(hash[key]) if hash[key].is_a?(Hash)
50
+ elsif hash[key].is_a?(Hash)
51
+ filter_hash(hash[key])
52
52
  end
53
53
  end
54
54
  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
@@ -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: errors(exception),
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 self.ignored?
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
- @async_sender
132
- else
133
- @config.logger.warn(
134
- "#{LOG_LABEL} falling back to sync delivery because there are no " \
135
- "running async workers"
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
- caller.drop_while { |frame| frame.include?('/lib/airbrake') }
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
@@ -18,7 +18,7 @@ module Airbrake
18
18
  Errno::ECONNREFUSED,
19
19
  EOFError,
20
20
  OpenSSL::SSL::SSLError
21
- ]
21
+ ].freeze
22
22
 
23
23
  ##
24
24
  # @param [Airbrake::Config] config
@@ -3,5 +3,5 @@
3
3
  module Airbrake
4
4
  ##
5
5
  # @return [String] the library version
6
- AIRBRAKE_RUBY_VERSION = '1.0.2'.freeze
6
+ AIRBRAKE_RUBY_VERSION = '1.0.3'.freeze
7
7
  end
@@ -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 "#new" do
11
- context "workers_count parameter" do
12
- let(:new_workers) { 5 }
13
- let(:config) { Airbrake::Config.new(workers: new_workers) }
14
-
15
- it "spawns alive threads in an enclosed ThreadGroup" do
16
- expect(@workers).to be_a(ThreadGroup)
17
- expect(@workers.list).to all(be_alive)
18
- expect(@workers).to be_enclosed
19
- end
20
-
21
- it "controls the number of spawned threads" do
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
- log = @stdout.string.split("\n")
50
- expect(log.grep(/\*\*Airbrake: \{\}/).size).to eq(notices)
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
- @workers = @sender.instance_variable_get(:@workers).list
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
- expect(@workers).to all(be_alive)
36
+ workers = @sender.instance_variable_get(:@workers).list
37
+
38
+ expect(workers).to all(be_alive)
66
39
  @sender.close
67
- expect(@workers).to all(be_stop)
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
- @sender.close
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
- it "returns false when the sender is not closed, but has 0 workers" do
105
- sender = described_class.new(Airbrake::Config.new)
106
- expect(sender.has_workers?).to be_truthy
96
+ before do
97
+ @sender = described_class.new(Airbrake::Config.new)
98
+ expect(@sender).to have_workers
99
+ end
107
100
 
108
- sender.instance_variable_get(:@workers).list.each(&:kill)
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.has_workers?).to be_falsey
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.has_workers?).to be_truthy
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
@@ -75,26 +75,46 @@ RSpec.describe Airbrake::Backtrace do
75
75
  end
76
76
 
77
77
  context "generic backtrace" do
78
- # rubocop:disable Metrics/LineLength
79
- let(:generic_bt) do
80
- ["/home/bingo/bango/assets/stylesheets/error_pages.scss:139:in `animation'",
81
- "/home/bingo/bango/assets/stylesheets/error_pages.scss:139",
82
- "/home/bingo/.gem/ruby/2.2.2/gems/sass-3.4.20/lib/sass/tree/visitors/perform.rb:349:in `block in visit_mixin'"]
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
- let(:ex) { AirbrakeTestError.new.tap { |e| e.set_backtrace(generic_bt) } }
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
- let(:parsed_backtrace) do
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
- it "returns a properly formatted array of hashes" do
97
- expect(described_class.parse(ex)).to eq(parsed_backtrace)
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
@@ -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
@@ -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
- airbrake = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
380
- airbrake.
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
- expect(airbrake.notify('bingo')).to be_nil
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
- shared_examples 'close' do |method|
696
+ context "when using #notify on a closed notifier" do
652
697
  it "raises error" do
653
- @airbrake.close
654
- expect { method.call(@airbrake) }.
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.2
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-03 00:00:00.000000000 Z
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.4.5
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: