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 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: