airbrake-ruby 2.4.0 → 2.4.1

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: c845fb70bdbfade4ce4922090cf89ddb1d31dc8b
4
- data.tar.gz: 322b65cb8938b1f8d102736cf0f95ee0a68bb3e6
3
+ metadata.gz: 485663114790c11fb2bce21e7d5fc9c89a2f5557
4
+ data.tar.gz: 4e6de160720983de3ba60294b2674329c1ffa9db
5
5
  SHA512:
6
- metadata.gz: 833738116b0076013905ebe90ac3fe582a3302682fc72037bdf17b1bd7e1eef4879ae3f58c1570b4bb8aa36b548805706e734a92532cc5622647affbfc25258f
7
- data.tar.gz: fc6b031950da579efea3de3ea1bd78e08fef5dbf9793a12e39ed6318f8d17d578820c35bee1a99f9248c3a2153effd07ab16ef7f2349ee9e72a0334ed211901f
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(exception, logger)
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
- { file: match[:file],
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
@@ -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(exception, logger)
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(exception, @logger) }
23
+ backtrace: Backtrace.parse(@config, exception) }
24
24
  end
25
25
  end
26
26
 
@@ -68,7 +68,7 @@ module Airbrake
68
68
  @config = config
69
69
 
70
70
  @payload = {
71
- errors: NestedException.new(exception, @config.logger).as_json,
71
+ errors: NestedException.new(config, exception).as_json,
72
72
  context: context,
73
73
  environment: {
74
74
  program_name: $PROGRAM_NAME
@@ -4,5 +4,5 @@
4
4
  module Airbrake
5
5
  ##
6
6
  # @return [String] the library version
7
- AIRBRAKE_RUBY_VERSION = '2.4.0'.freeze
7
+ AIRBRAKE_RUBY_VERSION = '2.4.1'.freeze
8
8
  end
@@ -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(AirbrakeTestError.new, Logger.new('/dev/null'))
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(JavaAirbrakeTestError.new, Logger.new('/dev/null'))
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(ex, Logger.new('/dev/null'))
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(ex, logger) }.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Banana
2
+ attr_reader :bingo
3
+ end
data/spec/helpers.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Helpers
2
+ def fixture_path(filename)
3
+ File.expand_path(File.join('spec', 'fixtures', filename))
4
+ end
5
+ end
@@ -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(ex, Logger.new('/dev/null'))
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(ex, Logger.new('/dev/null'))
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(ex1, Logger.new('/dev/null'))
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.0
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-09-20 00:00:00.000000000 Z
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.8
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