airbrake-ruby 1.0.0.rc.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 +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
|