airbrake-ruby 2.3.2 → 2.4.0

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: 8fedcdefd7e46e06454e468865a2a9a6a5c6d54a
4
- data.tar.gz: 870a24b542bd2503c83f62230c4627742bb7891c
3
+ metadata.gz: c845fb70bdbfade4ce4922090cf89ddb1d31dc8b
4
+ data.tar.gz: 322b65cb8938b1f8d102736cf0f95ee0a68bb3e6
5
5
  SHA512:
6
- metadata.gz: a956495373f17953f7b2f587dfdf68e1e4cc1d51719903acde59f35bd8b38f0a7183da2c15e713d9bf13b252f355f54d83b3826e367731003312511d9a4b369a
7
- data.tar.gz: 7193c4cbb25da97ed895c83309d3fc9cef392adec7ace13b38097e6caa3d609bb36d06ebc0c16a169668264da6011ccf27a3e0b9efb0c52803e4c4e06558b443
6
+ metadata.gz: 833738116b0076013905ebe90ac3fe582a3302682fc72037bdf17b1bd7e1eef4879ae3f58c1570b4bb8aa36b548805706e734a92532cc5622647affbfc25258f
7
+ data.tar.gz: fc6b031950da579efea3de3ea1bd78e08fef5dbf9793a12e39ed6318f8d17d578820c35bee1a99f9248c3a2153effd07ab16ef7f2349ee9e72a0334ed211901f
@@ -5,7 +5,10 @@ module Airbrake
5
5
  # list of parameters in the payload of a notice.
6
6
  #
7
7
  # @example
8
- # filter = Airbrake::Filters::KeysBlacklist.new(:email, /credit/i, 'password')
8
+ # filter = Airbrake::Filters::KeysBlacklist.new(
9
+ # Logger.new(STDOUT),
10
+ # [:email, /credit/i, 'password']
11
+ # )
9
12
  # airbrake.add_filter(filter)
10
13
  # airbrake.notify(StandardError.new('App crashed!'), {
11
14
  # user: 'John'
@@ -31,6 +31,7 @@ module Airbrake
31
31
  # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
32
32
  # +patterns+ for filtering a notice's payload.
33
33
  #
34
+ # @param [Logger, #error] logger
34
35
  # @param [Array<String,Regexp,Symbol>] patterns
35
36
  def initialize(logger, patterns)
36
37
  @logger = logger
@@ -5,7 +5,10 @@ module Airbrake
5
5
  # notice, but specified keys.
6
6
  #
7
7
  # @example
8
- # filter = Airbrake::Filters::KeysWhitelist.new(:email, /user/i, 'account_id')
8
+ # filter = Airbrake::Filters::KeysBlacklist.new(
9
+ # Logger.new(STDOUT),
10
+ # [:email, /credit/i, 'password']
11
+ # )
9
12
  # airbrake.add_filter(filter)
10
13
  # airbrake.notify(StandardError.new('App crashed!'), {
11
14
  # user: 'John',
@@ -70,7 +70,9 @@ module Airbrake
70
70
  @payload = {
71
71
  errors: NestedException.new(exception, @config.logger).as_json,
72
72
  context: context,
73
- environment: {},
73
+ environment: {
74
+ program_name: $PROGRAM_NAME
75
+ },
74
76
  session: {},
75
77
  params: params
76
78
  }
@@ -198,7 +200,7 @@ module Airbrake
198
200
 
199
201
  begin
200
202
  attributes = exception.to_airbrake
201
- rescue => ex
203
+ rescue StandardError => ex
202
204
  @config.logger.error(
203
205
  "#{LOG_LABEL} #{exception.class}#to_airbrake failed: #{ex.class}: #{ex}"
204
206
  )
@@ -10,12 +10,17 @@ module Airbrake
10
10
  # @return [Integer] the limit of the response body
11
11
  TRUNCATE_LIMIT = 100
12
12
 
13
+ ##
14
+ # @return [Integer] HTTP code returned when an IP sends over 10k/min notices
15
+ TOO_MANY_REQUESTS = 429
16
+
13
17
  ##
14
18
  # Parses HTTP responses from the Airbrake API.
15
19
  #
16
20
  # @param [Net::HTTPResponse] response
17
21
  # @param [Logger] logger
18
22
  # @return [Hash{String=>String}] parsed response
23
+ # rubocop:disable Metrics/MethodLength
19
24
  def self.parse(response, logger)
20
25
  code = response.code.to_i
21
26
  body = response.body
@@ -26,21 +31,27 @@ module Airbrake
26
31
  parsed_body = JSON.parse(body)
27
32
  logger.debug("#{LOG_LABEL} #{parsed_body}")
28
33
  parsed_body
29
- when 400, 401, 403, 429
34
+ when 400, 401, 403, 420
30
35
  parsed_body = JSON.parse(body)
31
36
  logger.error("#{LOG_LABEL} #{parsed_body['message']}")
32
37
  parsed_body
38
+ when TOO_MANY_REQUESTS
39
+ parsed_body = JSON.parse(body)
40
+ msg = "#{LOG_LABEL} #{parsed_body['message']}"
41
+ logger.error(msg)
42
+ { 'error' => msg, 'rate_limit_reset' => rate_limit_reset(response) }
33
43
  else
34
44
  body_msg = truncated_body(body)
35
45
  logger.error("#{LOG_LABEL} unexpected code (#{code}). Body: #{body_msg}")
36
46
  { 'error' => body_msg }
37
47
  end
38
- rescue => ex
48
+ rescue StandardError => ex
39
49
  body_msg = truncated_body(body)
40
50
  logger.error("#{LOG_LABEL} error while parsing body (#{ex}). Body: #{body_msg}")
41
51
  { 'error' => ex.inspect }
42
52
  end
43
53
  end
54
+ # rubocop:enable Metrics/MethodLength
44
55
 
45
56
  def self.truncated_body(body)
46
57
  if body.nil?
@@ -52,5 +63,10 @@ module Airbrake
52
63
  end
53
64
  end
54
65
  private_class_method :truncated_body
66
+
67
+ def self.rate_limit_reset(response)
68
+ Time.now + response['X-RateLimit-Delay'].to_i
69
+ end
70
+ private_class_method :rate_limit_reset
55
71
  end
56
72
  end
@@ -14,6 +14,7 @@ module Airbrake
14
14
  # @param [Airbrake::Config] config
15
15
  def initialize(config)
16
16
  @config = config
17
+ @rate_limit_reset = Time.now
17
18
  end
18
19
 
19
20
  ##
@@ -23,26 +24,28 @@ module Airbrake
23
24
  # @param [Airbrake::Notice] endpoint
24
25
  # @return [Hash{String=>String}] the parsed HTTP response
25
26
  def send(notice, promise, endpoint = @config.endpoint)
27
+ return promise if rate_limited_ip?(promise)
28
+
26
29
  response = nil
27
30
  req = build_post_request(endpoint, notice)
28
31
 
29
- if req.body.nil?
30
- reason = "#{LOG_LABEL} notice was not sent because of missing body"
31
- @config.logger.error(reason)
32
- return promise.reject(reason)
33
- end
32
+ return promise if missing_body?(req, promise)
34
33
 
35
34
  https = build_https(endpoint)
36
35
 
37
36
  begin
38
37
  response = https.request(req)
39
- rescue => ex
38
+ rescue StandardError => ex
40
39
  reason = "#{LOG_LABEL} HTTP error: #{ex}"
41
40
  @config.logger.error(reason)
42
41
  return promise.reject(reason)
43
42
  end
44
43
 
45
44
  parsed_resp = Response.parse(response, @config.logger)
45
+ if parsed_resp.key?('rate_limit_reset')
46
+ @rate_limit_reset = parsed_resp['rate_limit_reset']
47
+ end
48
+
46
49
  return promise.reject(parsed_resp['error']) if parsed_resp.key?('error')
47
50
  promise.resolve(parsed_resp)
48
51
  end
@@ -76,5 +79,23 @@ module Airbrake
76
79
  [@config.proxy[:host], @config.proxy[:port], @config.proxy[:user],
77
80
  @config.proxy[:password]]
78
81
  end
82
+
83
+ def rate_limited_ip?(promise)
84
+ rate_limited = Time.now < @rate_limit_reset
85
+ promise.reject("#{LOG_LABEL} IP is rate limited") if rate_limited
86
+ rate_limited
87
+ end
88
+
89
+ def missing_body?(req, promise)
90
+ missing = req.body.nil?
91
+
92
+ if missing
93
+ reason = "#{LOG_LABEL} notice was not sent because of missing body"
94
+ @config.logger.error(reason)
95
+ promise.reject(reason)
96
+ end
97
+
98
+ missing
99
+ end
79
100
  end
80
101
  end
@@ -4,5 +4,5 @@
4
4
  module Airbrake
5
5
  ##
6
6
  # @return [String] the library version
7
- AIRBRAKE_RUBY_VERSION = '2.3.2'.freeze
7
+ AIRBRAKE_RUBY_VERSION = '2.4.0'.freeze
8
8
  end
@@ -204,6 +204,11 @@ RSpec.describe Airbrake::Notice do
204
204
  it "defaults to the error severity" do
205
205
  expect(notice.to_json).to match(/"context":{.*"severity":"error".*}/)
206
206
  end
207
+
208
+ it "always contains environment/program_name" do
209
+ expect(notice.to_json).
210
+ to match(%r|"environment":{"program_name":.+/exe/rspec.*|)
211
+ end
207
212
  end
208
213
 
209
214
  describe "#[]" do
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  RSpec.describe Airbrake::Notifier do
@@ -225,9 +223,15 @@ RSpec.describe Airbrake::Notifier do
225
223
  /ERROR -- : .+ Project not found or access denied./
226
224
  end
227
225
 
228
- context "the rate-limit message" do
229
- include_examples 'HTTP codes', 429, '{"message": "Project is rate limited."}',
230
- /ERROR -- : .+ Project is rate limited.+/
226
+ context "error 420" do
227
+ include_examples 'HTTP codes', 420,
228
+ '{"message":"Project is rate limited."}',
229
+ /ERROR -- : .+ Project is rate limited./
230
+ end
231
+
232
+ context "the IP rate limit message" do
233
+ include_examples 'HTTP codes', 429, '{"message": "IP is rate limited"}',
234
+ /ERROR -- : .+ IP is rate limited/
231
235
  end
232
236
 
233
237
  context "the internal server error" do
@@ -43,11 +43,11 @@ class AirbrakeTestError < RuntimeError
43
43
  # rubocop:enable Metrics/LineLength
44
44
  end
45
45
 
46
- # rubocop:disable Style/AccessorMethodName
46
+ # rubocop:disable Naming/AccessorMethodName
47
47
  def set_backtrace(backtrace)
48
48
  @backtrace = backtrace
49
49
  end
50
- # rubocop:enable Style/AccessorMethodName
50
+ # rubocop:enable Naming/AccessorMethodName
51
51
 
52
52
  def message
53
53
  'App crashed!'
@@ -90,9 +90,9 @@ class Ruby21Error < RuntimeError
90
90
  end
91
91
  end
92
92
 
93
- puts <<EOS
93
+ puts <<BANNER
94
94
  #{'#' * 80}
95
95
  # RUBY_VERSION: #{RUBY_VERSION}
96
96
  # RUBY_ENGINE: #{RUBY_ENGINE}
97
97
  #{'#' * 80}
98
- EOS
98
+ BANNER
@@ -13,12 +13,12 @@ RSpec.describe Airbrake::SyncSender do
13
13
 
14
14
  describe "#send" do
15
15
  let(:promise) { Airbrake::Promise.new }
16
+ let(:stdout) { StringIO.new }
17
+ let(:config) { Airbrake::Config.new(logger: Logger.new(stdout)) }
18
+ let(:sender) { described_class.new(config) }
19
+ let(:notice) { Airbrake::Notice.new(config, AirbrakeTestError.new) }
16
20
 
17
21
  it "catches exceptions raised while sending" do
18
- stdout = StringIO.new
19
- config = Airbrake::Config.new(logger: Logger.new(stdout))
20
- sender = described_class.new(config)
21
- notice = Airbrake::Notice.new(config, AirbrakeTestError.new)
22
22
  https = double("foo")
23
23
  allow(sender).to receive(:build_https).and_return(https)
24
24
  allow(https).to receive(:request).and_raise(StandardError.new('foo'))
@@ -40,10 +40,6 @@ RSpec.describe Airbrake::SyncSender do
40
40
  10.times { backtrace << "bin/rails:3:in `<#{bad_string}>'" }
41
41
  ex.set_backtrace(backtrace)
42
42
 
43
- stdout = StringIO.new
44
- config = Airbrake::Config.new(logger: Logger.new(stdout))
45
-
46
- sender = described_class.new(config)
47
43
  notice = Airbrake::Notice.new(config, ex)
48
44
 
49
45
  expect(sender.send(notice, promise)).to be_an(Airbrake::Promise)
@@ -52,5 +48,37 @@ RSpec.describe Airbrake::SyncSender do
52
48
  expect(stdout.string).to match(/ERROR -- : .+ notice was not sent/)
53
49
  end
54
50
  end
51
+
52
+ context "when IP is rate limited" do
53
+ let(:endpoint) { %r{https://airbrake.io/api/v3/projects/notices} }
54
+
55
+ before do
56
+ stub_request(:post, endpoint).to_return(
57
+ status: 429,
58
+ body: '{"message":"IP is rate limited"}',
59
+ headers: { 'X-RateLimit-Delay' => '1' }
60
+ )
61
+ end
62
+
63
+ it "returns error" do
64
+ p1 = Airbrake::Promise.new
65
+ sender.send(notice, p1)
66
+ expect(p1.value).to match('error' => '**Airbrake: IP is rate limited')
67
+
68
+ p2 = Airbrake::Promise.new
69
+ sender.send(notice, p2)
70
+ expect(p2.value).to match('error' => '**Airbrake: IP is rate limited')
71
+
72
+ # Wait for X-RateLimit-Delay and then make a new request to make sure p2
73
+ # was ignored (no request made for it).
74
+ sleep 1
75
+
76
+ p3 = Airbrake::Promise.new
77
+ sender.send(notice, p3)
78
+ expect(p3.value).to match('error' => '**Airbrake: IP is rate limited')
79
+
80
+ expect(a_request(:post, endpoint)).to have_been_made.twice
81
+ end
82
+ end
55
83
  end
56
84
  end
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  RSpec.describe Airbrake::Truncator 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: 2.3.2
4
+ version: 2.4.0
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-07-26 00:00:00.000000000 Z
11
+ date: 2017-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -86,14 +86,34 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.47'
89
+ version: '0.50'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.47'
96
+ version: '0.50'
97
+ - !ruby/object:Gem::Dependency
98
+ name: public_suffix
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: '3.0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '2.0'
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
97
117
  description: |
98
118
  Airbrake Ruby is a plain Ruby notifier for Airbrake (https://airbrake.io), the
99
119
  leading exception reporting service. Airbrake Ruby provides minimalist API that