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