airbrake-ruby 2.4.0 → 2.4.1
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 +2 -0
- data/lib/airbrake-ruby/backtrace.rb +16 -6
- data/lib/airbrake-ruby/code_hunk.rb +54 -0
- data/lib/airbrake-ruby/config.rb +7 -0
- data/lib/airbrake-ruby/file_cache.rb +54 -0
- data/lib/airbrake-ruby/nested_exception.rb +3 -3
- data/lib/airbrake-ruby/notice.rb +1 -1
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/backtrace_spec.rb +52 -33
- data/spec/code_hunk_spec.rb +108 -0
- data/spec/file_cache.rb +38 -0
- data/spec/fixtures/code.rb +221 -0
- data/spec/fixtures/empty_file.rb +0 -0
- data/spec/fixtures/long_line.txt +1 -0
- data/spec/fixtures/short_file.rb +3 -0
- data/spec/helpers.rb +5 -0
- data/spec/nested_exception_spec.rb +3 -3
- data/spec/spec_helper.rb +3 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 485663114790c11fb2bce21e7d5fc9c89a2f5557
|
4
|
+
data.tar.gz: 4e6de160720983de3ba60294b2674329c1ffa9db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80d9aecb4ce06871347b40ee525b8b286199845096c3533e7a6109d55be1422d473f27f48ec33fa402d6760e95778c835485b1695836e67a893419921cbf170a
|
7
|
+
data.tar.gz: ef0440643d93d0bedc16d4bf3d8710467a7f5287dd5331b07f9b5f2f6c0ab42bc042b887d78d344a47df7c7f59a60b5f0699792832262a7776527b6c85331667
|
data/lib/airbrake-ruby.rb
CHANGED
@@ -25,6 +25,8 @@ require 'airbrake-ruby/filters/root_directory_filter'
|
|
25
25
|
require 'airbrake-ruby/filters/thread_filter'
|
26
26
|
require 'airbrake-ruby/filter_chain'
|
27
27
|
require 'airbrake-ruby/notifier'
|
28
|
+
require 'airbrake-ruby/code_hunk'
|
29
|
+
require 'airbrake-ruby/file_cache'
|
28
30
|
|
29
31
|
##
|
30
32
|
# This module defines the Airbrake API. The user is meant to interact with
|
@@ -102,7 +102,7 @@ module Airbrake
|
|
102
102
|
# @param [Exception] exception The exception, which contains a backtrace to
|
103
103
|
# parse
|
104
104
|
# @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
|
105
|
-
def self.parse(
|
105
|
+
def self.parse(config, exception)
|
106
106
|
return [] if exception.backtrace.nil? || exception.backtrace.none?
|
107
107
|
|
108
108
|
regexp = best_regexp_for(exception)
|
@@ -111,14 +111,14 @@ module Airbrake
|
|
111
111
|
frame = match_frame(regexp, stackframe)
|
112
112
|
|
113
113
|
unless frame
|
114
|
-
logger.error(
|
114
|
+
config.logger.error(
|
115
115
|
"can't parse '#{stackframe}' (please file an issue so we can fix " \
|
116
116
|
"it: https://github.com/airbrake/airbrake-ruby/issues/new)"
|
117
117
|
)
|
118
118
|
frame = { file: nil, line: nil, function: stackframe }
|
119
119
|
end
|
120
120
|
|
121
|
-
stack_frame(frame)
|
121
|
+
stack_frame(config, frame)
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
@@ -176,10 +176,15 @@ module Airbrake
|
|
176
176
|
end
|
177
177
|
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
178
178
|
|
179
|
-
def stack_frame(match)
|
180
|
-
|
179
|
+
def stack_frame(config, match)
|
180
|
+
frame = {
|
181
|
+
file: match[:file],
|
181
182
|
line: (Integer(match[:line]) if match[:line]),
|
182
|
-
function: match[:function]
|
183
|
+
function: match[:function]
|
184
|
+
}
|
185
|
+
|
186
|
+
populate_code(config, frame) if config.code_hunks
|
187
|
+
frame
|
183
188
|
end
|
184
189
|
|
185
190
|
def match_frame(regexp, stackframe)
|
@@ -188,6 +193,11 @@ module Airbrake
|
|
188
193
|
|
189
194
|
Patterns::GENERIC.match(stackframe)
|
190
195
|
end
|
196
|
+
|
197
|
+
def populate_code(config, frame)
|
198
|
+
code = Airbrake::CodeHunk.new(config).get(frame[:file], frame[:line])
|
199
|
+
frame[:code] = code if code
|
200
|
+
end
|
191
201
|
end
|
192
202
|
end
|
193
203
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Represents a small hunk of code consisting of a base line and a couple lines
|
4
|
+
# around it
|
5
|
+
# @api private
|
6
|
+
class CodeHunk
|
7
|
+
##
|
8
|
+
# @return [Integer] the maximum length of a line
|
9
|
+
MAX_LINE_LEN = 200
|
10
|
+
|
11
|
+
##
|
12
|
+
# @return [Integer] how many lines should be read around the base line
|
13
|
+
NLINES = 2
|
14
|
+
|
15
|
+
def initialize(config)
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param [String] file The file to read
|
21
|
+
# @param [Integer] line The base line in the file
|
22
|
+
# @return [Hash{Integer=>String}, nil] lines of code around the base line
|
23
|
+
def get(file, line)
|
24
|
+
return unless File.exist?(file)
|
25
|
+
|
26
|
+
start_line = [line - NLINES, 1].max
|
27
|
+
end_line = line + NLINES
|
28
|
+
lines = {}
|
29
|
+
|
30
|
+
begin
|
31
|
+
get_from_cache(file).with_index(1) do |l, i|
|
32
|
+
next if i < start_line
|
33
|
+
break if i > end_line
|
34
|
+
|
35
|
+
lines[i] = l[0...MAX_LINE_LEN].rstrip
|
36
|
+
end
|
37
|
+
rescue StandardError => ex
|
38
|
+
@config.logger.error(
|
39
|
+
"#{self.class.name}##{__method__}: can't read code hunk for " \
|
40
|
+
"#{file}:#{line}: #{ex}"
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
return { 1 => '' } if lines.empty?
|
45
|
+
lines
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def get_from_cache(file)
|
51
|
+
Airbrake::FileCache[file] ||= File.foreach(file)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/airbrake-ruby/config.rb
CHANGED
@@ -72,6 +72,12 @@ module Airbrake
|
|
72
72
|
# @since 1.2.0
|
73
73
|
attr_accessor :whitelist_keys
|
74
74
|
|
75
|
+
##
|
76
|
+
# @return [Boolean] true if the library should attach code hunks to each
|
77
|
+
# frame in a backtrace, false otherwise
|
78
|
+
# @since v3.0.0
|
79
|
+
attr_accessor :code_hunks
|
80
|
+
|
75
81
|
##
|
76
82
|
# @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
|
77
83
|
# config
|
@@ -81,6 +87,7 @@ module Airbrake
|
|
81
87
|
self.proxy = {}
|
82
88
|
self.queue_size = 100
|
83
89
|
self.workers = 1
|
90
|
+
self.code_hunks = false
|
84
91
|
|
85
92
|
self.logger = Logger.new(STDOUT)
|
86
93
|
logger.level = Logger::WARN
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Extremely simple global cache.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
# @since v2.4.1
|
7
|
+
module FileCache
|
8
|
+
##
|
9
|
+
# @return [Integer]
|
10
|
+
MAX_SIZE = 50
|
11
|
+
|
12
|
+
##
|
13
|
+
# @return [Mutex]
|
14
|
+
MUTEX = Mutex.new
|
15
|
+
|
16
|
+
##
|
17
|
+
# Associates the value given by +value+ with the key given by +key+. Deletes
|
18
|
+
# entries that exceed +MAX_SIZE+.
|
19
|
+
#
|
20
|
+
# @param [Object] key
|
21
|
+
# @param [Object] value
|
22
|
+
# @return [Object] the corresponding value
|
23
|
+
def self.[]=(key, value)
|
24
|
+
MUTEX.synchronize do
|
25
|
+
data[key] = value
|
26
|
+
data.delete(data.keys.first) if data.size > MAX_SIZE
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Retrieve an object from the cache.
|
32
|
+
#
|
33
|
+
# @param [Object] key
|
34
|
+
# @return [Object] the corresponding value
|
35
|
+
def self.[](key)
|
36
|
+
MUTEX.synchronize do
|
37
|
+
data[key]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Checks whether the cache is empty. Needed only for the test suite.
|
43
|
+
#
|
44
|
+
# @return [Boolean]
|
45
|
+
def self.empty?
|
46
|
+
data.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.data
|
50
|
+
@data ||= {}
|
51
|
+
end
|
52
|
+
private_class_method :data
|
53
|
+
end
|
54
|
+
end
|
@@ -11,16 +11,16 @@ module Airbrake
|
|
11
11
|
# can unwrap. Exceptions that have a longer cause chain will be ignored
|
12
12
|
MAX_NESTED_EXCEPTIONS = 3
|
13
13
|
|
14
|
-
def initialize(
|
14
|
+
def initialize(config, exception)
|
15
|
+
@config = config
|
15
16
|
@exception = exception
|
16
|
-
@logger = logger
|
17
17
|
end
|
18
18
|
|
19
19
|
def as_json
|
20
20
|
unwind_exceptions.map do |exception|
|
21
21
|
{ type: exception.class.name,
|
22
22
|
message: exception.message,
|
23
|
-
backtrace: Backtrace.parse(
|
23
|
+
backtrace: Backtrace.parse(@config, exception) }
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
data/lib/airbrake-ruby/notice.rb
CHANGED
data/spec/backtrace_spec.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe Airbrake::Backtrace do
|
4
|
+
let(:config) do
|
5
|
+
Airbrake::Config.new.tap { |c| c.logger = Logger.new('/dev/null') }
|
6
|
+
end
|
7
|
+
|
4
8
|
describe ".parse" do
|
5
9
|
context "UNIX backtrace" do
|
6
10
|
let(:parsed_backtrace) do
|
@@ -23,7 +27,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
23
27
|
|
24
28
|
it "returns a properly formatted array of hashes" do
|
25
29
|
expect(
|
26
|
-
described_class.parse(
|
30
|
+
described_class.parse(config, AirbrakeTestError.new)
|
27
31
|
).to eq(parsed_backtrace)
|
28
32
|
end
|
29
33
|
end
|
@@ -44,9 +48,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
44
48
|
end
|
45
49
|
|
46
50
|
it "returns a properly formatted array of hashes" do
|
47
|
-
expect(
|
48
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
49
|
-
).to eq(parsed_backtrace)
|
51
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
@@ -71,7 +73,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
71
73
|
allow(described_class).to receive(:java_exception?).and_return(true)
|
72
74
|
|
73
75
|
expect(
|
74
|
-
described_class.parse(
|
76
|
+
described_class.parse(config, JavaAirbrakeTestError.new)
|
75
77
|
).to eq(backtrace_array)
|
76
78
|
end
|
77
79
|
end
|
@@ -99,10 +101,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
99
101
|
|
100
102
|
it "returns a properly formatted array of hashes" do
|
101
103
|
allow(described_class).to receive(:java_exception?).and_return(true)
|
102
|
-
|
103
|
-
expect(
|
104
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
105
|
-
).to eq(parsed_backtrace)
|
104
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
106
105
|
end
|
107
106
|
end
|
108
107
|
|
@@ -126,9 +125,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
126
125
|
let(:ex) { AirbrakeTestError.new.tap { |e| e.set_backtrace(backtrace) } }
|
127
126
|
|
128
127
|
it "returns a properly formatted array of hashes" do
|
129
|
-
expect(
|
130
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
131
|
-
).to eq(parsed_backtrace)
|
128
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
132
129
|
end
|
133
130
|
end
|
134
131
|
|
@@ -153,9 +150,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
153
150
|
end
|
154
151
|
|
155
152
|
it "returns a properly formatted array of hashes" do
|
156
|
-
expect(
|
157
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
158
|
-
).to eq(parsed_backtrace)
|
153
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
159
154
|
end
|
160
155
|
end
|
161
156
|
|
@@ -173,9 +168,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
173
168
|
end
|
174
169
|
|
175
170
|
it "returns a properly formatted array of hashes" do
|
176
|
-
expect(
|
177
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
178
|
-
).to eq(parsed_backtrace)
|
171
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
179
172
|
end
|
180
173
|
end
|
181
174
|
end
|
@@ -187,15 +180,15 @@ RSpec.describe Airbrake::Backtrace do
|
|
187
180
|
|
188
181
|
it "returns array of hashes where each unknown frame is marked as 'function'" do
|
189
182
|
expect(
|
190
|
-
described_class.parse(
|
183
|
+
described_class.parse(config, ex)
|
191
184
|
).to eq([file: nil, line: nil, function: 'a b c 1 23 321 .rb'])
|
192
185
|
end
|
193
186
|
|
194
187
|
it "logs unknown frames as errors" do
|
195
188
|
out = StringIO.new
|
196
|
-
logger = Logger.new(out)
|
189
|
+
config.logger = Logger.new(out)
|
197
190
|
|
198
|
-
expect { described_class.parse(
|
191
|
+
expect { described_class.parse(config, ex) }.
|
199
192
|
to change { out.string }.
|
200
193
|
from('').
|
201
194
|
to(/ERROR -- : can't parse 'a b c 1 23 321 .rb'/)
|
@@ -216,9 +209,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
216
209
|
end
|
217
210
|
|
218
211
|
it "returns a properly formatted array of hashes" do
|
219
|
-
expect(
|
220
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
221
|
-
).to eq(parsed_backtrace)
|
212
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
222
213
|
end
|
223
214
|
end
|
224
215
|
|
@@ -241,9 +232,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
241
232
|
|
242
233
|
it "returns a properly formatted array of hashes" do
|
243
234
|
stub_const('OCIError', AirbrakeTestError)
|
244
|
-
expect(
|
245
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
246
|
-
).to eq(parsed_backtrace)
|
235
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
247
236
|
end
|
248
237
|
end
|
249
238
|
|
@@ -279,9 +268,7 @@ RSpec.describe Airbrake::Backtrace do
|
|
279
268
|
stub_const('ExecJS::RuntimeError', AirbrakeTestError)
|
280
269
|
stub_const('Airbrake::RUBY_20', false)
|
281
270
|
|
282
|
-
expect(
|
283
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
284
|
-
).to eq(parsed_backtrace)
|
271
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
285
272
|
end
|
286
273
|
end
|
287
274
|
|
@@ -296,12 +283,44 @@ RSpec.describe Airbrake::Backtrace do
|
|
296
283
|
stub_const('ExecJS::RuntimeError', NameError)
|
297
284
|
stub_const('Airbrake::RUBY_20', true)
|
298
285
|
|
299
|
-
expect(
|
300
|
-
described_class.parse(ex, Logger.new('/dev/null'))
|
301
|
-
).to eq(parsed_backtrace)
|
286
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
302
287
|
end
|
303
288
|
end
|
304
289
|
end
|
305
290
|
end
|
291
|
+
|
292
|
+
context "when code hunks are enabled" do
|
293
|
+
let(:config) do
|
294
|
+
config = Airbrake::Config.new
|
295
|
+
config.logger = Logger.new('/dev/null')
|
296
|
+
config.code_hunks = true
|
297
|
+
config
|
298
|
+
end
|
299
|
+
|
300
|
+
let(:parsed_backtrace) do
|
301
|
+
[
|
302
|
+
{
|
303
|
+
file: File.join(fixture_path('code.rb')),
|
304
|
+
line: 94,
|
305
|
+
function: 'to_json',
|
306
|
+
code: {
|
307
|
+
92 => ' loop do',
|
308
|
+
93 => ' begin',
|
309
|
+
94 => ' json = @payload.to_json',
|
310
|
+
95 => ' rescue *JSON_EXCEPTIONS => ex',
|
311
|
+
# rubocop:disable Metrics/LineLength,Lint/InterpolationCheck
|
312
|
+
96 => ' @config.logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")',
|
313
|
+
# rubocop:enable Metrics/LineLength,Lint/InterpolationCheck
|
314
|
+
}
|
315
|
+
}
|
316
|
+
]
|
317
|
+
end
|
318
|
+
|
319
|
+
it "attaches code to each frame" do
|
320
|
+
ex = RuntimeError.new
|
321
|
+
ex.set_backtrace([File.join(fixture_path('code.rb') + ":94:in `to_json'")])
|
322
|
+
expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
|
323
|
+
end
|
324
|
+
end
|
306
325
|
end
|
307
326
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::CodeHunk do
|
4
|
+
let(:config) { Airbrake::Config.new }
|
5
|
+
|
6
|
+
describe "#to_h" do
|
7
|
+
context "when a file is empty" do
|
8
|
+
subject do
|
9
|
+
described_class.new(config).get(fixture_path('empty_file.rb'), 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it { is_expected.to eq(1 => '') }
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when a file doesn't exist" do
|
16
|
+
subject { described_class.new(config).get(fixture_path('banana.rb'), 1) }
|
17
|
+
|
18
|
+
it { is_expected.to be_nil }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when a file has less than NLINES lines before start line" do
|
22
|
+
subject { described_class.new(config).get(fixture_path('code.rb'), 1) }
|
23
|
+
|
24
|
+
it do
|
25
|
+
is_expected.to(
|
26
|
+
eq(
|
27
|
+
1 => 'module Airbrake',
|
28
|
+
2 => ' ##',
|
29
|
+
# rubocop:disable Metrics/LineLength
|
30
|
+
3 => ' # Represents a chunk of information that is meant to be either sent to',
|
31
|
+
# rubocop:enable Metrics/LineLength
|
32
|
+
)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when a file has less than NLINES lines after end line" do
|
38
|
+
subject { described_class.new(config).get(fixture_path('code.rb'), 222) }
|
39
|
+
|
40
|
+
it do
|
41
|
+
is_expected.to(
|
42
|
+
eq(
|
43
|
+
220 => ' end',
|
44
|
+
221 => 'end'
|
45
|
+
)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when a file has less than NLINES lines before and after" do
|
51
|
+
subject do
|
52
|
+
described_class.new(config).get(fixture_path('short_file.rb'), 2)
|
53
|
+
end
|
54
|
+
|
55
|
+
it do
|
56
|
+
is_expected.to(
|
57
|
+
eq(
|
58
|
+
1 => 'module Banana',
|
59
|
+
2 => ' attr_reader :bingo',
|
60
|
+
3 => 'end'
|
61
|
+
)
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "when a file has enough lines before and after" do
|
67
|
+
subject { described_class.new(config).get(fixture_path('code.rb'), 100) }
|
68
|
+
|
69
|
+
it do
|
70
|
+
is_expected.to(
|
71
|
+
eq(
|
72
|
+
98 => ' return json if json && json.bytesize <= MAX_NOTICE_SIZE',
|
73
|
+
99 => ' end',
|
74
|
+
100 => '',
|
75
|
+
101 => ' break if truncate == 0',
|
76
|
+
102 => ' end'
|
77
|
+
)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when a line exceeds the length limit" do
|
83
|
+
subject do
|
84
|
+
described_class.new(config).get(fixture_path('long_line.txt'), 1)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "strips the line" do
|
88
|
+
expect(subject[1]).to eq('l' + 'o' * 196 + 'ng')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "when an error occurrs while fetching code" do
|
93
|
+
before do
|
94
|
+
expect(File).to receive(:foreach).and_raise(Errno::EACCES)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "logs error and returns nil" do
|
98
|
+
out = StringIO.new
|
99
|
+
config = Airbrake::Config.new
|
100
|
+
config.logger = Logger.new(out)
|
101
|
+
expect(described_class.new(config).get(fixture_path('code.rb'), 1)).to(
|
102
|
+
eq(1 => '')
|
103
|
+
)
|
104
|
+
expect(out.string).to match(/can't read code hunk.+Permission denied/)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/spec/file_cache.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::FileCache do
|
4
|
+
after do
|
5
|
+
%i[banana mango].each { |k| described_class.delete(k) }
|
6
|
+
expect(described_class).to be_empty
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ".[]=" do
|
10
|
+
context "when cache limit isn't reached" do
|
11
|
+
before do
|
12
|
+
stub_const("#{described_class.name}::MAX_SIZE", 10)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "adds objects" do
|
16
|
+
described_class[:banana] = 1
|
17
|
+
described_class[:mango] = 2
|
18
|
+
|
19
|
+
expect(described_class[:banana]).to eq(1)
|
20
|
+
expect(described_class[:mango]).to eq(2)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when cache limit is reached" do
|
25
|
+
before do
|
26
|
+
stub_const("#{described_class.name}::MAX_SIZE", 1)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "replaces old objects with new ones" do
|
30
|
+
described_class[:banana] = 1
|
31
|
+
described_class[:mango] = 2
|
32
|
+
|
33
|
+
expect(described_class[:banana]).to be_nil
|
34
|
+
expect(described_class[:mango]).to eq(2)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Represents a chunk of information that is meant to be either sent to
|
4
|
+
# Airbrake or ignored completely.
|
5
|
+
#
|
6
|
+
# @since v1.0.0
|
7
|
+
class Notice
|
8
|
+
##
|
9
|
+
# @return [Hash{Symbol=>String}] the information about the notifier library
|
10
|
+
NOTIFIER = {
|
11
|
+
name: 'airbrake-ruby'.freeze,
|
12
|
+
version: Airbrake::AIRBRAKE_RUBY_VERSION,
|
13
|
+
url: 'https://github.com/airbrake/airbrake-ruby'.freeze
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
##
|
17
|
+
# @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
|
18
|
+
# Context tab in the dashboard
|
19
|
+
CONTEXT = {
|
20
|
+
os: RUBY_PLATFORM,
|
21
|
+
language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
|
22
|
+
notifier: NOTIFIER
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
##
|
26
|
+
# @return [Integer] the maxium size of the JSON payload in bytes
|
27
|
+
MAX_NOTICE_SIZE = 64000
|
28
|
+
|
29
|
+
##
|
30
|
+
# @return [Integer] the maximum size of hashes, arrays and strings in the
|
31
|
+
# notice.
|
32
|
+
PAYLOAD_MAX_SIZE = 10000
|
33
|
+
|
34
|
+
##
|
35
|
+
# @return [Array<StandardError>] the list of possible exceptions that might
|
36
|
+
# be raised when an object is converted to JSON
|
37
|
+
JSON_EXCEPTIONS = [
|
38
|
+
IOError,
|
39
|
+
NotImplementedError,
|
40
|
+
JSON::GeneratorError,
|
41
|
+
Encoding::UndefinedConversionError
|
42
|
+
].freeze
|
43
|
+
|
44
|
+
# @return [Array<Symbol>] the list of keys that can be be overwritten with
|
45
|
+
# {Airbrake::Notice#[]=}
|
46
|
+
WRITABLE_KEYS = %i[notifier context environment session params].freeze
|
47
|
+
|
48
|
+
##
|
49
|
+
# @return [Array<Symbol>] parts of a Notice's payload that can be modified
|
50
|
+
# by the truncator
|
51
|
+
TRUNCATABLE_KEYS = %i[errors environment session params].freeze
|
52
|
+
|
53
|
+
##
|
54
|
+
# @return [String] the name of the host machine
|
55
|
+
HOSTNAME = Socket.gethostname.freeze
|
56
|
+
|
57
|
+
##
|
58
|
+
# @return [String]
|
59
|
+
DEFAULT_SEVERITY = 'error'.freeze
|
60
|
+
|
61
|
+
##
|
62
|
+
# @since v1.7.0
|
63
|
+
# @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
|
64
|
+
# in filters
|
65
|
+
attr_reader :stash
|
66
|
+
|
67
|
+
def initialize(config, exception, params = {})
|
68
|
+
@config = config
|
69
|
+
|
70
|
+
@payload = {
|
71
|
+
errors: NestedException.new(config, exception).as_json,
|
72
|
+
context: context,
|
73
|
+
environment: {
|
74
|
+
program_name: $PROGRAM_NAME
|
75
|
+
},
|
76
|
+
session: {},
|
77
|
+
params: params
|
78
|
+
}
|
79
|
+
@stash = { exception: exception }
|
80
|
+
@truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
|
81
|
+
|
82
|
+
extract_custom_attributes(exception)
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Converts the notice to JSON. Calls +to_json+ on each object inside
|
87
|
+
# notice's payload. Truncates notices, JSON representation of which is
|
88
|
+
# bigger than {MAX_NOTICE_SIZE}.
|
89
|
+
#
|
90
|
+
# @return [Hash{String=>String}, nil]
|
91
|
+
def to_json
|
92
|
+
loop do
|
93
|
+
begin
|
94
|
+
json = @payload.to_json
|
95
|
+
rescue *JSON_EXCEPTIONS => ex
|
96
|
+
@config.logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
|
97
|
+
else
|
98
|
+
return json if json && json.bytesize <= MAX_NOTICE_SIZE
|
99
|
+
end
|
100
|
+
|
101
|
+
break if truncate == 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Ignores a notice. Ignored notices never reach the Airbrake dashboard.
|
107
|
+
#
|
108
|
+
# @return [void]
|
109
|
+
# @see #ignored?
|
110
|
+
# @note Ignored noticed can't be unignored
|
111
|
+
def ignore!
|
112
|
+
@payload = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Checks whether the notice was ignored.
|
117
|
+
#
|
118
|
+
# @return [Boolean]
|
119
|
+
# @see #ignore!
|
120
|
+
def ignored?
|
121
|
+
@payload.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Reads a value from notice's payload.
|
126
|
+
# @return [Object]
|
127
|
+
#
|
128
|
+
# @raise [Airbrake::Error] if the notice is ignored
|
129
|
+
def [](key)
|
130
|
+
raise_if_ignored
|
131
|
+
@payload[key]
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Writes a value to the payload hash. Restricts unrecognized
|
136
|
+
# writes.
|
137
|
+
# @example
|
138
|
+
# notice[:params][:my_param] = 'foobar'
|
139
|
+
#
|
140
|
+
# @return [void]
|
141
|
+
# @raise [Airbrake::Error] if the notice is ignored
|
142
|
+
# @raise [Airbrake::Error] if the +key+ is not recognized
|
143
|
+
# @raise [Airbrake::Error] if the root value is not a Hash
|
144
|
+
def []=(key, value)
|
145
|
+
raise_if_ignored
|
146
|
+
|
147
|
+
unless WRITABLE_KEYS.include?(key)
|
148
|
+
raise Airbrake::Error,
|
149
|
+
":#{key} is not recognized among #{WRITABLE_KEYS}"
|
150
|
+
end
|
151
|
+
|
152
|
+
unless value.respond_to?(:to_hash)
|
153
|
+
raise Airbrake::Error, "Got #{value.class} value, wanted a Hash"
|
154
|
+
end
|
155
|
+
|
156
|
+
@payload[key] = value.to_hash
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def context
|
162
|
+
{
|
163
|
+
version: @config.app_version,
|
164
|
+
# We ensure that root_directory is always a String, so it can always be
|
165
|
+
# converted to JSON in a predictable manner (when it's a Pathname and in
|
166
|
+
# Rails environment, it converts to unexpected JSON).
|
167
|
+
rootDirectory: @config.root_directory.to_s,
|
168
|
+
environment: @config.environment,
|
169
|
+
|
170
|
+
# Make sure we always send hostname.
|
171
|
+
hostname: HOSTNAME,
|
172
|
+
|
173
|
+
severity: DEFAULT_SEVERITY
|
174
|
+
}.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
|
175
|
+
end
|
176
|
+
|
177
|
+
def raise_if_ignored
|
178
|
+
return unless ignored?
|
179
|
+
raise Airbrake::Error, 'cannot access ignored notice'
|
180
|
+
end
|
181
|
+
|
182
|
+
def truncate
|
183
|
+
TRUNCATABLE_KEYS.each { |key| @truncator.truncate_object(self[key]) }
|
184
|
+
|
185
|
+
new_max_size = @truncator.reduce_max_size
|
186
|
+
if new_max_size == 0
|
187
|
+
@config.logger.error(
|
188
|
+
"#{LOG_LABEL} truncation failed. File an issue at " \
|
189
|
+
"https://github.com/airbrake/airbrake-ruby " \
|
190
|
+
"and attach the following payload: #{@payload}"
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
new_max_size
|
195
|
+
end
|
196
|
+
|
197
|
+
def extract_custom_attributes(exception)
|
198
|
+
return unless exception.respond_to?(:to_airbrake)
|
199
|
+
attributes = nil
|
200
|
+
|
201
|
+
begin
|
202
|
+
attributes = exception.to_airbrake
|
203
|
+
rescue StandardError => ex
|
204
|
+
@config.logger.error(
|
205
|
+
"#{LOG_LABEL} #{exception.class}#to_airbrake failed: #{ex.class}: #{ex}"
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
return unless attributes
|
210
|
+
|
211
|
+
begin
|
212
|
+
@payload.merge!(attributes)
|
213
|
+
rescue TypeError
|
214
|
+
@config.logger.error(
|
215
|
+
"#{LOG_LABEL} #{exception.class}#to_airbrake failed:" \
|
216
|
+
" #{attributes} must be a Hash"
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
|
data/spec/helpers.rb
ADDED
@@ -13,7 +13,7 @@ RSpec.describe Airbrake::NestedException do
|
|
13
13
|
Ruby21Error.raise_error('bingo')
|
14
14
|
end
|
15
15
|
rescue Ruby21Error => ex
|
16
|
-
nested_exception = described_class.new(
|
16
|
+
nested_exception = described_class.new(config, ex)
|
17
17
|
exceptions = nested_exception.as_json
|
18
18
|
|
19
19
|
expect(exceptions.size).to eq(2)
|
@@ -40,7 +40,7 @@ RSpec.describe Airbrake::NestedException do
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
rescue Ruby21Error => ex
|
43
|
-
nested_exception = described_class.new(
|
43
|
+
nested_exception = described_class.new(config, ex)
|
44
44
|
exceptions = nested_exception.as_json
|
45
45
|
|
46
46
|
expect(exceptions.size).to eq(3)
|
@@ -64,7 +64,7 @@ RSpec.describe Airbrake::NestedException do
|
|
64
64
|
end
|
65
65
|
rescue Ruby21Error => ex1
|
66
66
|
ex1.set_backtrace([])
|
67
|
-
nested_exception = described_class.new(
|
67
|
+
nested_exception = described_class.new(config, ex1)
|
68
68
|
exceptions = nested_exception.as_json
|
69
69
|
|
70
70
|
expect(exceptions.size).to eq(2)
|
data/spec/spec_helper.rb
CHANGED
@@ -9,10 +9,13 @@ require 'webrick'
|
|
9
9
|
require 'English'
|
10
10
|
require 'base64'
|
11
11
|
|
12
|
+
require 'helpers'
|
13
|
+
|
12
14
|
RSpec.configure do |c|
|
13
15
|
c.order = 'random'
|
14
16
|
c.color = true
|
15
17
|
c.disable_monkey_patching!
|
18
|
+
c.include Helpers
|
16
19
|
end
|
17
20
|
|
18
21
|
Thread.abort_on_exception = true
|
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: 2.4.
|
4
|
+
version: 2.4.1
|
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: 2017-
|
11
|
+
date: 2017-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -132,8 +132,10 @@ files:
|
|
132
132
|
- lib/airbrake-ruby.rb
|
133
133
|
- lib/airbrake-ruby/async_sender.rb
|
134
134
|
- lib/airbrake-ruby/backtrace.rb
|
135
|
+
- lib/airbrake-ruby/code_hunk.rb
|
135
136
|
- lib/airbrake-ruby/config.rb
|
136
137
|
- lib/airbrake-ruby/config/validator.rb
|
138
|
+
- lib/airbrake-ruby/file_cache.rb
|
137
139
|
- lib/airbrake-ruby/filter_chain.rb
|
138
140
|
- lib/airbrake-ruby/filters/gem_root_filter.rb
|
139
141
|
- lib/airbrake-ruby/filters/keys_blacklist.rb
|
@@ -153,8 +155,10 @@ files:
|
|
153
155
|
- spec/airbrake_spec.rb
|
154
156
|
- spec/async_sender_spec.rb
|
155
157
|
- spec/backtrace_spec.rb
|
158
|
+
- spec/code_hunk_spec.rb
|
156
159
|
- spec/config/validator_spec.rb
|
157
160
|
- spec/config_spec.rb
|
161
|
+
- spec/file_cache.rb
|
158
162
|
- spec/filter_chain_spec.rb
|
159
163
|
- spec/filters/gem_root_filter_spec.rb
|
160
164
|
- spec/filters/keys_blacklist_spec.rb
|
@@ -162,6 +166,11 @@ files:
|
|
162
166
|
- spec/filters/root_directory_filter_spec.rb
|
163
167
|
- spec/filters/system_exit_filter_spec.rb
|
164
168
|
- spec/filters/thread_filter_spec.rb
|
169
|
+
- spec/fixtures/code.rb
|
170
|
+
- spec/fixtures/empty_file.rb
|
171
|
+
- spec/fixtures/long_line.txt
|
172
|
+
- spec/fixtures/short_file.rb
|
173
|
+
- spec/helpers.rb
|
165
174
|
- spec/nested_exception_spec.rb
|
166
175
|
- spec/notice_spec.rb
|
167
176
|
- spec/notifier_spec.rb
|
@@ -190,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
199
|
version: '0'
|
191
200
|
requirements: []
|
192
201
|
rubyforge_project:
|
193
|
-
rubygems_version: 2.6.
|
202
|
+
rubygems_version: 2.6.13
|
194
203
|
signing_key:
|
195
204
|
specification_version: 4
|
196
205
|
summary: Ruby notifier for https://airbrake.io
|
@@ -198,8 +207,10 @@ test_files:
|
|
198
207
|
- spec/airbrake_spec.rb
|
199
208
|
- spec/async_sender_spec.rb
|
200
209
|
- spec/backtrace_spec.rb
|
210
|
+
- spec/code_hunk_spec.rb
|
201
211
|
- spec/config/validator_spec.rb
|
202
212
|
- spec/config_spec.rb
|
213
|
+
- spec/file_cache.rb
|
203
214
|
- spec/filter_chain_spec.rb
|
204
215
|
- spec/filters/gem_root_filter_spec.rb
|
205
216
|
- spec/filters/keys_blacklist_spec.rb
|
@@ -207,6 +218,11 @@ test_files:
|
|
207
218
|
- spec/filters/root_directory_filter_spec.rb
|
208
219
|
- spec/filters/system_exit_filter_spec.rb
|
209
220
|
- spec/filters/thread_filter_spec.rb
|
221
|
+
- spec/fixtures/code.rb
|
222
|
+
- spec/fixtures/empty_file.rb
|
223
|
+
- spec/fixtures/long_line.txt
|
224
|
+
- spec/fixtures/short_file.rb
|
225
|
+
- spec/helpers.rb
|
210
226
|
- spec/nested_exception_spec.rb
|
211
227
|
- spec/notice_spec.rb
|
212
228
|
- spec/notifier_spec/options_spec.rb
|