airbrake-ruby 2.3.2 → 2.4.0

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