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 +4 -4
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +4 -1
- data/lib/airbrake-ruby/filters/keys_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +4 -1
- data/lib/airbrake-ruby/notice.rb +4 -2
- data/lib/airbrake-ruby/response.rb +18 -2
- data/lib/airbrake-ruby/sync_sender.rb +27 -6
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/notice_spec.rb +5 -0
- data/spec/notifier_spec.rb +9 -5
- data/spec/spec_helper.rb +4 -4
- data/spec/sync_sender_spec.rb +36 -8
- data/spec/truncator_spec.rb +0 -2
- metadata +24 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c845fb70bdbfade4ce4922090cf89ddb1d31dc8b
|
4
|
+
data.tar.gz: 322b65cb8938b1f8d102736cf0f95ee0a68bb3e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
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::
|
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',
|
data/lib/airbrake-ruby/notice.rb
CHANGED
@@ -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,
|
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
|
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
|
data/spec/notice_spec.rb
CHANGED
@@ -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
|
data/spec/notifier_spec.rb
CHANGED
@@ -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 "
|
229
|
-
include_examples 'HTTP codes',
|
230
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -43,11 +43,11 @@ class AirbrakeTestError < RuntimeError
|
|
43
43
|
# rubocop:enable Metrics/LineLength
|
44
44
|
end
|
45
45
|
|
46
|
-
# rubocop:disable
|
46
|
+
# rubocop:disable Naming/AccessorMethodName
|
47
47
|
def set_backtrace(backtrace)
|
48
48
|
@backtrace = backtrace
|
49
49
|
end
|
50
|
-
# rubocop:enable
|
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 <<
|
93
|
+
puts <<BANNER
|
94
94
|
#{'#' * 80}
|
95
95
|
# RUBY_VERSION: #{RUBY_VERSION}
|
96
96
|
# RUBY_ENGINE: #{RUBY_ENGINE}
|
97
97
|
#{'#' * 80}
|
98
|
-
|
98
|
+
BANNER
|
data/spec/sync_sender_spec.rb
CHANGED
@@ -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
|
data/spec/truncator_spec.rb
CHANGED
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
|
+
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-
|
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.
|
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.
|
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
|