airbrake-ruby 1.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/airbrake-ruby.rb +292 -0
- data/lib/airbrake-ruby/async_sender.rb +90 -0
- data/lib/airbrake-ruby/backtrace.rb +75 -0
- data/lib/airbrake-ruby/config.rb +120 -0
- data/lib/airbrake-ruby/filter_chain.rb +86 -0
- data/lib/airbrake-ruby/filters.rb +10 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +37 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +65 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +37 -0
- data/lib/airbrake-ruby/notice.rb +207 -0
- data/lib/airbrake-ruby/notifier.rb +145 -0
- data/lib/airbrake-ruby/payload_truncator.rb +141 -0
- data/lib/airbrake-ruby/response.rb +53 -0
- data/lib/airbrake-ruby/sync_sender.rb +76 -0
- data/lib/airbrake-ruby/version.rb +7 -0
- data/spec/airbrake_spec.rb +177 -0
- data/spec/async_sender_spec.rb +121 -0
- data/spec/backtrace_spec.rb +77 -0
- data/spec/config_spec.rb +67 -0
- data/spec/filter_chain_spec.rb +157 -0
- data/spec/notice_spec.rb +190 -0
- data/spec/notifier_spec.rb +690 -0
- data/spec/notifier_spec/options_spec.rb +217 -0
- data/spec/payload_truncator_spec.rb +458 -0
- data/spec/spec_helper.rb +98 -0
- metadata +158 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# This class is reponsible for sending notices to Airbrake. It supports
|
4
|
+
# synchronous and asynchronous delivery.
|
5
|
+
#
|
6
|
+
# @see Airbrake::Config The list of options
|
7
|
+
# @api private
|
8
|
+
# @since v5.0.0
|
9
|
+
class Notifier
|
10
|
+
##
|
11
|
+
# Creates a new Airbrake notifier with the given config options.
|
12
|
+
#
|
13
|
+
# @example Configuring with a Hash
|
14
|
+
# airbrake = Airbrake.new(project_id: 123, project_key: '321')
|
15
|
+
#
|
16
|
+
# @example Configuring with an Airbrake::Config
|
17
|
+
# config = Airbrake::Config.new
|
18
|
+
# config.project_id = 123
|
19
|
+
# config.project_key = '321'
|
20
|
+
# airbake = Airbrake.new(config)
|
21
|
+
#
|
22
|
+
# @param [Hash, Airbrake::Config] user_config The config that contains
|
23
|
+
# information about how the notifier should operate
|
24
|
+
# @raise [Airbrake::Error] when either +project_id+ or +project_key+
|
25
|
+
# is missing (or both)
|
26
|
+
def initialize(user_config)
|
27
|
+
@config = (user_config.is_a?(Config) ? user_config : Config.new(user_config))
|
28
|
+
|
29
|
+
unless [@config.project_id, @config.project_key].all?
|
30
|
+
raise Airbrake::Error, 'both :project_id and :project_key are required'
|
31
|
+
end
|
32
|
+
|
33
|
+
@filter_chain = FilterChain.new(@config)
|
34
|
+
@async_sender = AsyncSender.new(@config)
|
35
|
+
@sync_sender = SyncSender.new(@config)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @!macro see_public_api_method
|
40
|
+
# @see Airbrake.$0
|
41
|
+
|
42
|
+
##
|
43
|
+
# @macro see_public_api_method
|
44
|
+
def notify(exception, params = {})
|
45
|
+
send_notice(exception, params)
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# @macro see_public_api_method
|
51
|
+
def notify_sync(exception, params = {})
|
52
|
+
send_notice(exception, params, @sync_sender)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# @macro see_public_api_method
|
57
|
+
def add_filter(filter = nil, &block)
|
58
|
+
@filter_chain.add_filter(block_given? ? block : filter)
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# @macro see_public_api_method
|
63
|
+
def whitelist_keys(keys)
|
64
|
+
add_filter(Filters::KeysWhitelist.new(*keys))
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# @macro see_public_api_method
|
69
|
+
def blacklist_keys(keys)
|
70
|
+
add_filter(Filters::KeysBlacklist.new(*keys))
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# @macro see_public_api_method
|
75
|
+
def build_notice(exception, params = {})
|
76
|
+
if @async_sender.closed?
|
77
|
+
raise Airbrake::Error,
|
78
|
+
"attempted to build #{exception} with closed Airbrake instance"
|
79
|
+
end
|
80
|
+
|
81
|
+
if exception.is_a?(Airbrake::Notice)
|
82
|
+
exception
|
83
|
+
else
|
84
|
+
Notice.new(@config, convert_to_exception(exception), params)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# @macro see_public_api_method
|
90
|
+
def close
|
91
|
+
@async_sender.close
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# @macro see_public_api_method
|
96
|
+
def create_deploy(deploy_params)
|
97
|
+
deploy_params[:environment] ||= @config.environment
|
98
|
+
|
99
|
+
host = @config.endpoint.to_s.split(@config.endpoint.path).first
|
100
|
+
path = "/api/v4/projects/#{@config.project_id}/deploys?key=#{@config.project_key}"
|
101
|
+
|
102
|
+
@sync_sender.send(deploy_params, URI.join(host, path))
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def convert_to_exception(ex)
|
108
|
+
if ex.is_a?(Exception) || Backtrace.java_exception?(ex)
|
109
|
+
# Manually created exceptions don't have backtraces, so we create a fake
|
110
|
+
# one, whose first frame points to the place where Airbrake was called
|
111
|
+
# (normally via `notify`).
|
112
|
+
ex.set_backtrace(clean_backtrace) unless ex.backtrace
|
113
|
+
return ex
|
114
|
+
end
|
115
|
+
|
116
|
+
e = RuntimeError.new(ex.to_s)
|
117
|
+
e.set_backtrace(clean_backtrace)
|
118
|
+
e
|
119
|
+
end
|
120
|
+
|
121
|
+
def send_notice(exception, params, sender = default_sender)
|
122
|
+
notice = build_notice(exception, params)
|
123
|
+
@filter_chain.refine(notice)
|
124
|
+
return if notice.ignored?
|
125
|
+
|
126
|
+
sender.send(notice)
|
127
|
+
end
|
128
|
+
|
129
|
+
def default_sender
|
130
|
+
if @async_sender.has_workers?
|
131
|
+
@async_sender
|
132
|
+
else
|
133
|
+
@config.logger.warn(
|
134
|
+
"#{LOG_LABEL} falling back to sync delivery because there are no " \
|
135
|
+
"running async workers"
|
136
|
+
)
|
137
|
+
@sync_sender
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def clean_backtrace
|
142
|
+
caller.drop_while { |frame| frame.include?('/lib/airbrake') }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# This class is responsible for truncation of too big objects. Mainly, you
|
4
|
+
# should use it for simple objects such as strings, hashes, & arrays.
|
5
|
+
class PayloadTruncator
|
6
|
+
##
|
7
|
+
# @return [Hash] the options for +String#encode+
|
8
|
+
ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
|
9
|
+
|
10
|
+
##
|
11
|
+
# @return [String] the temporary encoding to be used when fixing invalid
|
12
|
+
# strings with +ENCODING_OPTIONS+
|
13
|
+
TEMP_ENCODING = (RUBY_VERSION == '1.9.2' ? 'iso-8859-1' : 'utf-16')
|
14
|
+
|
15
|
+
##
|
16
|
+
# @param [Integer] max_size maximum size of hashes, arrays and strings
|
17
|
+
# @param [Logger] logger the logger object
|
18
|
+
def initialize(max_size, logger)
|
19
|
+
@max_size = max_size
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Truncates errors (not exceptions) to fit the limit.
|
25
|
+
#
|
26
|
+
# @param [Hash] error
|
27
|
+
# @option error [Symbol] :message
|
28
|
+
# @option error [Array<String>] :backtrace
|
29
|
+
# @return [void]
|
30
|
+
def truncate_error(error)
|
31
|
+
if error[:message].length > @max_size
|
32
|
+
error[:message] = truncate_string(error[:message])
|
33
|
+
@logger.info("#{LOG_LABEL} truncated the message of #{error[:type]}")
|
34
|
+
end
|
35
|
+
|
36
|
+
return if (dropped_frames = error[:backtrace].size - @max_size) < 0
|
37
|
+
|
38
|
+
error[:backtrace] = error[:backtrace].slice(0, @max_size)
|
39
|
+
@logger.info("#{LOG_LABEL} dropped #{dropped_frames} frame(s) from #{error[:type]}")
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Performs deep truncation of arrays, hashes and sets. Uses a
|
44
|
+
# placeholder for recursive objects (`[Circular]`).
|
45
|
+
#
|
46
|
+
# @param [Hash,Array] object The object to truncate
|
47
|
+
# @param [Hash] seen The hash that helps to detect recursion
|
48
|
+
# @return [void]
|
49
|
+
def truncate_object(object, seen = {})
|
50
|
+
return seen[object] if seen[object]
|
51
|
+
|
52
|
+
seen[object] = '[Circular]'.freeze
|
53
|
+
truncated = if object.is_a?(Hash)
|
54
|
+
truncate_hash(object, seen)
|
55
|
+
elsif object.is_a?(Array)
|
56
|
+
truncate_array(object, seen)
|
57
|
+
elsif object.is_a?(Set)
|
58
|
+
truncate_set(object, seen)
|
59
|
+
else
|
60
|
+
raise Airbrake::Error,
|
61
|
+
"cannot truncate object: #{object} (#{object.class})"
|
62
|
+
end
|
63
|
+
seen[object] = truncated
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Reduces maximum allowed size of the truncated object.
|
68
|
+
# @return [void]
|
69
|
+
def reduce_max_size
|
70
|
+
@max_size /= 2
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def truncate(val, seen)
|
76
|
+
case val
|
77
|
+
when String
|
78
|
+
truncate_string(val)
|
79
|
+
when Array, Hash, Set
|
80
|
+
truncate_object(val, seen)
|
81
|
+
when Numeric, TrueClass, FalseClass, Symbol, NilClass
|
82
|
+
val
|
83
|
+
else
|
84
|
+
stringified_val = begin
|
85
|
+
val.to_json
|
86
|
+
rescue *Notice::JSON_EXCEPTIONS
|
87
|
+
val.to_s
|
88
|
+
end
|
89
|
+
truncate_string(stringified_val)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def truncate_string(str)
|
94
|
+
replace_invalid_characters!(str)
|
95
|
+
return str if str.length <= @max_size
|
96
|
+
str.slice(0, @max_size) + '[Truncated]'.freeze
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Replaces invalid characters in string with arbitrary encoding.
|
101
|
+
#
|
102
|
+
# For Ruby 1.9.2 the method converts encoding of +str+ to +iso-8859-1+ to
|
103
|
+
# avoid a bug when encoding options are no-op, when `#encode` is given the
|
104
|
+
# same encoding as the receiver's encoding.
|
105
|
+
#
|
106
|
+
# For modern Rubies we use UTF-16 as a safe alternative.
|
107
|
+
#
|
108
|
+
# @param [String] str The string to replace characters
|
109
|
+
# @return [void]
|
110
|
+
# @note This method mutates +str+ for speed
|
111
|
+
# @see https://github.com/flori/json/commit/3e158410e81f94dbbc3da6b7b35f4f64983aa4e3
|
112
|
+
def replace_invalid_characters!(str)
|
113
|
+
encoding = str.encoding
|
114
|
+
utf8_string = (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII)
|
115
|
+
return str if utf8_string && str.valid_encoding?
|
116
|
+
|
117
|
+
str.encode!(TEMP_ENCODING, ENCODING_OPTIONS) if utf8_string
|
118
|
+
str.encode!('utf-8', ENCODING_OPTIONS)
|
119
|
+
end
|
120
|
+
|
121
|
+
def truncate_hash(hash, seen)
|
122
|
+
hash.each_with_index do |(key, val), idx|
|
123
|
+
if idx < @max_size
|
124
|
+
hash[key] = truncate(val, seen)
|
125
|
+
else
|
126
|
+
hash.delete(key)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def truncate_array(array, seen)
|
132
|
+
array.slice(0, @max_size).map! { |val| truncate(val, seen) }
|
133
|
+
end
|
134
|
+
|
135
|
+
def truncate_set(set, seen)
|
136
|
+
set.keep_if.with_index { |_val, idx| idx < @max_size }.map! do |val|
|
137
|
+
truncate(val, seen)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Parses responses coming from the Airbrake API. Handles HTTP errors by
|
4
|
+
# logging them.
|
5
|
+
module Response
|
6
|
+
##
|
7
|
+
# @return [Integer] the limit of the response body
|
8
|
+
TRUNCATE_LIMIT = 100
|
9
|
+
|
10
|
+
##
|
11
|
+
# Parses HTTP responses from the Airbrake API.
|
12
|
+
#
|
13
|
+
# @param [Net::HTTPResponse] response
|
14
|
+
# @param [Logger] logger
|
15
|
+
# @return [Hash{String=>String}] parsed response
|
16
|
+
def self.parse(response, logger)
|
17
|
+
code = response.code.to_i
|
18
|
+
body = response.body
|
19
|
+
|
20
|
+
begin
|
21
|
+
case code
|
22
|
+
when 201
|
23
|
+
parsed_body = JSON.parse(body)
|
24
|
+
logger.debug("#{LOG_LABEL} #{parsed_body}")
|
25
|
+
parsed_body
|
26
|
+
when 400, 401, 403, 429
|
27
|
+
parsed_body = JSON.parse(body)
|
28
|
+
logger.error("#{LOG_LABEL} #{parsed_body['error']}")
|
29
|
+
parsed_body
|
30
|
+
else
|
31
|
+
body_msg = truncated_body(body)
|
32
|
+
logger.error("#{LOG_LABEL} unexpected code (#{code}). Body: #{body_msg}")
|
33
|
+
{ 'error' => body_msg }
|
34
|
+
end
|
35
|
+
rescue => ex
|
36
|
+
body_msg = truncated_body(body)
|
37
|
+
logger.error("#{LOG_LABEL} error while parsing body (#{ex}). Body: #{body_msg}")
|
38
|
+
{ 'error' => ex }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.truncated_body(body)
|
43
|
+
if body.nil?
|
44
|
+
'[EMPTY_BODY]'.freeze
|
45
|
+
elsif body.length > TRUNCATE_LIMIT
|
46
|
+
body[0..TRUNCATE_LIMIT] << '...'
|
47
|
+
else
|
48
|
+
body
|
49
|
+
end
|
50
|
+
end
|
51
|
+
private_class_method :truncated_body
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Responsible for sending notices to Airbrake synchronously. Supports proxies.
|
4
|
+
#
|
5
|
+
# @see AsyncSender
|
6
|
+
class SyncSender
|
7
|
+
##
|
8
|
+
# @return [String] body for HTTP requests
|
9
|
+
CONTENT_TYPE = 'application/json'.freeze
|
10
|
+
|
11
|
+
##
|
12
|
+
# @return [Array] the errors to be rescued and logged during an HTTP request
|
13
|
+
HTTP_ERRORS = [
|
14
|
+
Timeout::Error,
|
15
|
+
Net::HTTPBadResponse,
|
16
|
+
Net::HTTPHeaderSyntaxError,
|
17
|
+
Errno::ECONNRESET,
|
18
|
+
Errno::ECONNREFUSED,
|
19
|
+
EOFError,
|
20
|
+
OpenSSL::SSL::SSLError
|
21
|
+
]
|
22
|
+
|
23
|
+
##
|
24
|
+
# @param [Airbrake::Config] config
|
25
|
+
def initialize(config)
|
26
|
+
@config = config
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Sends a POST request to the given +endpoint+ with the +notice+ payload.
|
31
|
+
#
|
32
|
+
# @param [Airbrake::Notice] notice
|
33
|
+
# @param [Airbrake::Notice] endpoint
|
34
|
+
# @return [Hash{String=>String}] the parsed HTTP response
|
35
|
+
def send(notice, endpoint = @config.endpoint)
|
36
|
+
response = nil
|
37
|
+
req = build_post_request(endpoint, notice)
|
38
|
+
https = build_https(endpoint)
|
39
|
+
|
40
|
+
begin
|
41
|
+
response = https.request(req)
|
42
|
+
rescue *HTTP_ERRORS => ex
|
43
|
+
@config.logger.error("#{LOG_LABEL} HTTP error: #{ex}")
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
Response.parse(response, @config.logger)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def build_https(uri)
|
53
|
+
Net::HTTP.new(uri.host, uri.port, *proxy_params).tap do |https|
|
54
|
+
https.use_ssl = uri.is_a?(URI::HTTPS)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_post_request(uri, notice)
|
59
|
+
Net::HTTP::Post.new(uri.request_uri).tap do |req|
|
60
|
+
req.body = notice.to_json
|
61
|
+
|
62
|
+
req['Content-Type'] = CONTENT_TYPE
|
63
|
+
req['User-Agent'] =
|
64
|
+
"#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
|
65
|
+
" Ruby/#{RUBY_VERSION}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def proxy_params
|
70
|
+
[@config.proxy[:host],
|
71
|
+
@config.proxy[:port],
|
72
|
+
@config.proxy[:user],
|
73
|
+
@config.proxy[:password]]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake do
|
4
|
+
let(:endpoint) do
|
5
|
+
'https://airbrake.io/api/v3/projects/113743/notices?key=fd04e13d806a90f96614ad8e529b2822'
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
described_class.configure do |c|
|
10
|
+
c.project_id = 113743
|
11
|
+
c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
|
12
|
+
end
|
13
|
+
|
14
|
+
stub_request(:post, endpoint).to_return(status: 201, body: '{}')
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
described_class.instance_variable_set(:@notifiers, {})
|
19
|
+
end
|
20
|
+
|
21
|
+
shared_examples 'error handling' do |method|
|
22
|
+
it "raises error if there is no notifier when using #{method}" do
|
23
|
+
described_class.instance_variable_set(:@notifiers, {})
|
24
|
+
|
25
|
+
expect { described_class.__send__(method, 'bingo') }.
|
26
|
+
to raise_error(Airbrake::Error,
|
27
|
+
"the 'default' notifier isn't configured")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe ".notify" do
|
32
|
+
include_examples 'error handling', :notify
|
33
|
+
|
34
|
+
it "sends exceptions asynchronously" do
|
35
|
+
described_class.notify('bingo')
|
36
|
+
sleep 2
|
37
|
+
expect(a_request(:post, endpoint)).to have_been_made.once
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe ".notify_sync" do
|
42
|
+
include_examples 'error handling', :notify_sync
|
43
|
+
|
44
|
+
it "sends exceptions synchronously" do
|
45
|
+
expect(described_class.notify_sync('bingo')).to be_a(Hash)
|
46
|
+
expect(a_request(:post, endpoint)).to have_been_made.once
|
47
|
+
end
|
48
|
+
|
49
|
+
context "given the notifier argument" do
|
50
|
+
it "sends exceptions via that notifier, ignoring other ones" do
|
51
|
+
bingo_string = StringIO.new
|
52
|
+
bango_string = StringIO.new
|
53
|
+
|
54
|
+
described_class.configure(:bingo) do |c|
|
55
|
+
c.project_id = 113743
|
56
|
+
c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
|
57
|
+
c.logger = Logger.new(bingo_string)
|
58
|
+
end
|
59
|
+
|
60
|
+
described_class.configure(:bango) do |c|
|
61
|
+
c.project_id = 113743
|
62
|
+
c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
|
63
|
+
c.logger = Logger.new(bango_string)
|
64
|
+
end
|
65
|
+
|
66
|
+
stub_request(:post, endpoint).to_return(status: 201, body: '{"id":1}')
|
67
|
+
|
68
|
+
described_class.notify_sync('bango', {}, :bango)
|
69
|
+
expect(bingo_string.string).to be_empty
|
70
|
+
expect(bango_string.string).to match(/\*\*Airbrake: {"id"=>1}/)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "clean backtrace" do
|
75
|
+
shared_examples 'backtrace building' do |msg, argument|
|
76
|
+
it(msg) do
|
77
|
+
described_class.notify_sync(argument)
|
78
|
+
|
79
|
+
# rubocop:disable Metrics/LineLength
|
80
|
+
expected_body = %r|
|
81
|
+
{"errors":\[{"type":"RuntimeError","message":"bingo","backtrace":\[
|
82
|
+
{"file":"[\w/\-\.]+spec/airbrake_spec.rb","line":\d+,"function":"[\w/\s\(\)<>]+"},
|
83
|
+
{"file":"\[GEM_ROOT\]/gems/rspec-core-.+/.+","line":\d+,"function":"[\w/\s\(\)<>]+"}
|
84
|
+
|x
|
85
|
+
# rubocop:enable Metrics/LineLength
|
86
|
+
|
87
|
+
expect(
|
88
|
+
a_request(:post, endpoint).
|
89
|
+
with(body: expected_body)
|
90
|
+
).to have_been_made.once
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "given a String" do
|
95
|
+
include_examples(
|
96
|
+
'backtrace building',
|
97
|
+
'converts it to a RuntimeException and builds a fake backtrace',
|
98
|
+
'bingo')
|
99
|
+
end
|
100
|
+
|
101
|
+
context "given an Exception with missing backtrace" do
|
102
|
+
include_examples(
|
103
|
+
'backtrace building',
|
104
|
+
'builds a backtrace for it and sends the notice',
|
105
|
+
RuntimeError.new('bingo'))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "special params" do
|
110
|
+
it "sends context/component and doesn't contain params/component" do
|
111
|
+
described_class.notify_sync('bingo', component: 'bango')
|
112
|
+
|
113
|
+
expect(
|
114
|
+
a_request(:post, endpoint).
|
115
|
+
with(body: /"context":{.*"component":"bango".+"params":{}/)
|
116
|
+
).to have_been_made.once
|
117
|
+
end
|
118
|
+
|
119
|
+
it "sends context/action and doesn't contain params/action" do
|
120
|
+
described_class.notify_sync('bingo', action: 'bango')
|
121
|
+
|
122
|
+
expect(
|
123
|
+
a_request(:post, endpoint).
|
124
|
+
with(body: /"context":{.*"action":"bango".+"params":{}/)
|
125
|
+
).to have_been_made.once
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe ".configure" do
|
131
|
+
context "given an argument" do
|
132
|
+
it "configures a notifier with the given name" do
|
133
|
+
described_class.configure(:bingo) do |c|
|
134
|
+
c.project_id = 123
|
135
|
+
c.project_key = '321'
|
136
|
+
end
|
137
|
+
|
138
|
+
notifiers = described_class.instance_variable_get(:@notifiers)
|
139
|
+
|
140
|
+
expect(notifiers).to be_a(Hash)
|
141
|
+
expect(notifiers.keys).to eq([:default, :bingo])
|
142
|
+
expect(notifiers.values).to all(satisfy { |v| v.is_a?(Airbrake::Notifier) })
|
143
|
+
end
|
144
|
+
|
145
|
+
it "raises error when a notifier of the given type was already configured" do
|
146
|
+
described_class.configure(:bingo) do |c|
|
147
|
+
c.project_id = 123
|
148
|
+
c.project_key = '321'
|
149
|
+
end
|
150
|
+
|
151
|
+
expect do
|
152
|
+
described_class.configure(:bingo) do |c|
|
153
|
+
c.project_id = 123
|
154
|
+
c.project_key = '321'
|
155
|
+
end
|
156
|
+
end.to raise_error(Airbrake::Error,
|
157
|
+
"the 'bingo' notifier was already configured")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe ".add_filter" do
|
163
|
+
include_examples 'error handling', :add_filter
|
164
|
+
end
|
165
|
+
|
166
|
+
describe ".whitelist_keys" do
|
167
|
+
include_examples 'error handling', :whitelist_keys
|
168
|
+
end
|
169
|
+
|
170
|
+
describe ".blacklist_keys" do
|
171
|
+
include_examples 'error handling', :blacklist_keys
|
172
|
+
end
|
173
|
+
|
174
|
+
describe ".build_notice" do
|
175
|
+
include_examples 'error handling', :build_notice
|
176
|
+
end
|
177
|
+
end
|