airbrake-ruby 3.2.2-java
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 +554 -0
- data/lib/airbrake-ruby/async_sender.rb +119 -0
- data/lib/airbrake-ruby/backtrace.rb +194 -0
- data/lib/airbrake-ruby/code_hunk.rb +53 -0
- data/lib/airbrake-ruby/config.rb +238 -0
- data/lib/airbrake-ruby/config/validator.rb +63 -0
- data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
- data/lib/airbrake-ruby/file_cache.rb +48 -0
- data/lib/airbrake-ruby/filter_chain.rb +95 -0
- data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
- data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
- data/lib/airbrake-ruby/hash_keyable.rb +37 -0
- data/lib/airbrake-ruby/ignorable.rb +44 -0
- data/lib/airbrake-ruby/nested_exception.rb +39 -0
- data/lib/airbrake-ruby/notice.rb +165 -0
- data/lib/airbrake-ruby/notice_notifier.rb +228 -0
- data/lib/airbrake-ruby/performance_notifier.rb +161 -0
- data/lib/airbrake-ruby/promise.rb +99 -0
- data/lib/airbrake-ruby/response.rb +71 -0
- data/lib/airbrake-ruby/stat.rb +56 -0
- data/lib/airbrake-ruby/sync_sender.rb +111 -0
- data/lib/airbrake-ruby/tdigest.rb +393 -0
- data/lib/airbrake-ruby/time_truncate.rb +17 -0
- data/lib/airbrake-ruby/truncator.rb +115 -0
- data/lib/airbrake-ruby/version.rb +6 -0
- data/spec/airbrake_spec.rb +171 -0
- data/spec/async_sender_spec.rb +154 -0
- data/spec/backtrace_spec.rb +438 -0
- data/spec/code_hunk_spec.rb +118 -0
- data/spec/config/validator_spec.rb +189 -0
- data/spec/config_spec.rb +281 -0
- data/spec/deploy_notifier_spec.rb +41 -0
- data/spec/file_cache.rb +36 -0
- data/spec/filter_chain_spec.rb +83 -0
- data/spec/filters/context_filter_spec.rb +25 -0
- data/spec/filters/dependency_filter_spec.rb +14 -0
- data/spec/filters/exception_attributes_filter_spec.rb +63 -0
- data/spec/filters/gem_root_filter_spec.rb +44 -0
- data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
- data/spec/filters/git_repository_filter.rb +53 -0
- data/spec/filters/git_revision_filter_spec.rb +126 -0
- data/spec/filters/keys_blacklist_spec.rb +236 -0
- data/spec/filters/keys_whitelist_spec.rb +205 -0
- data/spec/filters/root_directory_filter_spec.rb +42 -0
- data/spec/filters/sql_filter_spec.rb +219 -0
- data/spec/filters/system_exit_filter_spec.rb +14 -0
- data/spec/filters/thread_filter_spec.rb +279 -0
- data/spec/fixtures/notroot.txt +7 -0
- data/spec/fixtures/project_root/code.rb +221 -0
- data/spec/fixtures/project_root/empty_file.rb +0 -0
- data/spec/fixtures/project_root/long_line.txt +1 -0
- data/spec/fixtures/project_root/short_file.rb +3 -0
- data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
- data/spec/helpers.rb +9 -0
- data/spec/ignorable_spec.rb +14 -0
- data/spec/nested_exception_spec.rb +75 -0
- data/spec/notice_notifier_spec.rb +436 -0
- data/spec/notice_notifier_spec/options_spec.rb +266 -0
- data/spec/notice_spec.rb +297 -0
- data/spec/performance_notifier_spec.rb +287 -0
- data/spec/promise_spec.rb +165 -0
- data/spec/response_spec.rb +82 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/stat_spec.rb +35 -0
- data/spec/sync_sender_spec.rb +140 -0
- data/spec/tdigest_spec.rb +230 -0
- data/spec/time_truncate_spec.rb +13 -0
- data/spec/truncator_spec.rb +238 -0
- metadata +278 -0
@@ -0,0 +1,221 @@
|
|
1
|
+
module Airbrake
|
2
|
+
##
|
3
|
+
# Represents a chunk of information that is meant to be either sent to
|
4
|
+
# Airbrake or ignored completely.
|
5
|
+
#
|
6
|
+
# @since v1.0.0
|
7
|
+
class Notice
|
8
|
+
##
|
9
|
+
# @return [Hash{Symbol=>String}] the information about the notifier library
|
10
|
+
NOTIFIER = {
|
11
|
+
name: 'airbrake-ruby'.freeze,
|
12
|
+
version: Airbrake::AIRBRAKE_RUBY_VERSION,
|
13
|
+
url: 'https://github.com/airbrake/airbrake-ruby'.freeze
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
##
|
17
|
+
# @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
|
18
|
+
# Context tab in the dashboard
|
19
|
+
CONTEXT = {
|
20
|
+
os: RUBY_PLATFORM,
|
21
|
+
language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
|
22
|
+
notifier: NOTIFIER
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
##
|
26
|
+
# @return [Integer] the maxium size of the JSON payload in bytes
|
27
|
+
MAX_NOTICE_SIZE = 64000
|
28
|
+
|
29
|
+
##
|
30
|
+
# @return [Integer] the maximum size of hashes, arrays and strings in the
|
31
|
+
# notice.
|
32
|
+
PAYLOAD_MAX_SIZE = 10000
|
33
|
+
|
34
|
+
##
|
35
|
+
# @return [Array<StandardError>] the list of possible exceptions that might
|
36
|
+
# be raised when an object is converted to JSON
|
37
|
+
JSON_EXCEPTIONS = [
|
38
|
+
IOError,
|
39
|
+
NotImplementedError,
|
40
|
+
JSON::GeneratorError,
|
41
|
+
Encoding::UndefinedConversionError
|
42
|
+
].freeze
|
43
|
+
|
44
|
+
# @return [Array<Symbol>] the list of keys that can be be overwritten with
|
45
|
+
# {Airbrake::Notice#[]=}
|
46
|
+
WRITABLE_KEYS = %i[notifier context environment session params].freeze
|
47
|
+
|
48
|
+
##
|
49
|
+
# @return [Array<Symbol>] parts of a Notice's payload that can be modified
|
50
|
+
# by the truncator
|
51
|
+
TRUNCATABLE_KEYS = %i[errors environment session params].freeze
|
52
|
+
|
53
|
+
##
|
54
|
+
# @return [String] the name of the host machine
|
55
|
+
HOSTNAME = Socket.gethostname.freeze
|
56
|
+
|
57
|
+
##
|
58
|
+
# @return [String]
|
59
|
+
DEFAULT_SEVERITY = 'error'.freeze
|
60
|
+
|
61
|
+
##
|
62
|
+
# @since v1.7.0
|
63
|
+
# @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
|
64
|
+
# in filters
|
65
|
+
attr_reader :stash
|
66
|
+
|
67
|
+
def initialize(config, exception, params = {})
|
68
|
+
@config = config
|
69
|
+
|
70
|
+
@payload = {
|
71
|
+
errors: NestedException.new(config, exception).as_json,
|
72
|
+
context: context,
|
73
|
+
environment: {
|
74
|
+
program_name: $PROGRAM_NAME
|
75
|
+
},
|
76
|
+
session: {},
|
77
|
+
params: params
|
78
|
+
}
|
79
|
+
@stash = { exception: exception }
|
80
|
+
@truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
|
81
|
+
|
82
|
+
extract_custom_attributes(exception)
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Converts the notice to JSON. Calls +to_json+ on each object inside
|
87
|
+
# notice's payload. Truncates notices, JSON representation of which is
|
88
|
+
# bigger than {MAX_NOTICE_SIZE}.
|
89
|
+
#
|
90
|
+
# @return [Hash{String=>String}, nil]
|
91
|
+
def to_json
|
92
|
+
loop do
|
93
|
+
begin
|
94
|
+
json = @payload.to_json
|
95
|
+
rescue *JSON_EXCEPTIONS => ex
|
96
|
+
@config.logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
|
97
|
+
else
|
98
|
+
return json if json && json.bytesize <= MAX_NOTICE_SIZE
|
99
|
+
end
|
100
|
+
|
101
|
+
break if truncate == 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Ignores a notice. Ignored notices never reach the Airbrake dashboard.
|
107
|
+
#
|
108
|
+
# @return [void]
|
109
|
+
# @see #ignored?
|
110
|
+
# @note Ignored noticed can't be unignored
|
111
|
+
def ignore!
|
112
|
+
@payload = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Checks whether the notice was ignored.
|
117
|
+
#
|
118
|
+
# @return [Boolean]
|
119
|
+
# @see #ignore!
|
120
|
+
def ignored?
|
121
|
+
@payload.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Reads a value from notice's payload.
|
126
|
+
# @return [Object]
|
127
|
+
#
|
128
|
+
# @raise [Airbrake::Error] if the notice is ignored
|
129
|
+
def [](key)
|
130
|
+
raise_if_ignored
|
131
|
+
@payload[key]
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Writes a value to the payload hash. Restricts unrecognized
|
136
|
+
# writes.
|
137
|
+
# @example
|
138
|
+
# notice[:params][:my_param] = 'foobar'
|
139
|
+
#
|
140
|
+
# @return [void]
|
141
|
+
# @raise [Airbrake::Error] if the notice is ignored
|
142
|
+
# @raise [Airbrake::Error] if the +key+ is not recognized
|
143
|
+
# @raise [Airbrake::Error] if the root value is not a Hash
|
144
|
+
def []=(key, value)
|
145
|
+
raise_if_ignored
|
146
|
+
|
147
|
+
unless WRITABLE_KEYS.include?(key)
|
148
|
+
raise Airbrake::Error,
|
149
|
+
":#{key} is not recognized among #{WRITABLE_KEYS}"
|
150
|
+
end
|
151
|
+
|
152
|
+
unless value.respond_to?(:to_hash)
|
153
|
+
raise Airbrake::Error, "Got #{value.class} value, wanted a Hash"
|
154
|
+
end
|
155
|
+
|
156
|
+
@payload[key] = value.to_hash
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def context
|
162
|
+
{
|
163
|
+
version: @config.app_version,
|
164
|
+
# We ensure that root_directory is always a String, so it can always be
|
165
|
+
# converted to JSON in a predictable manner (when it's a Pathname and in
|
166
|
+
# Rails environment, it converts to unexpected JSON).
|
167
|
+
rootDirectory: @config.root_directory.to_s,
|
168
|
+
environment: @config.environment,
|
169
|
+
|
170
|
+
# Make sure we always send hostname.
|
171
|
+
hostname: HOSTNAME,
|
172
|
+
|
173
|
+
severity: DEFAULT_SEVERITY
|
174
|
+
}.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
|
175
|
+
end
|
176
|
+
|
177
|
+
def raise_if_ignored
|
178
|
+
return unless ignored?
|
179
|
+
raise Airbrake::Error, 'cannot access ignored notice'
|
180
|
+
end
|
181
|
+
|
182
|
+
def truncate
|
183
|
+
TRUNCATABLE_KEYS.each { |key| @truncator.truncate(self[key]) }
|
184
|
+
|
185
|
+
new_max_size = @truncator.reduce_max_size
|
186
|
+
if new_max_size == 0
|
187
|
+
@config.logger.error(
|
188
|
+
"#{LOG_LABEL} truncation failed. File an issue at " \
|
189
|
+
"https://github.com/airbrake/airbrake-ruby " \
|
190
|
+
"and attach the following payload: #{@payload}"
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
new_max_size
|
195
|
+
end
|
196
|
+
|
197
|
+
def extract_custom_attributes(exception)
|
198
|
+
return unless exception.respond_to?(:to_airbrake)
|
199
|
+
attributes = nil
|
200
|
+
|
201
|
+
begin
|
202
|
+
attributes = exception.to_airbrake
|
203
|
+
rescue StandardError => ex
|
204
|
+
@config.logger.error(
|
205
|
+
"#{LOG_LABEL} #{exception.class}#to_airbrake failed: #{ex.class}: #{ex}"
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
return unless attributes
|
210
|
+
|
211
|
+
begin
|
212
|
+
@payload.merge!(attributes)
|
213
|
+
rescue TypeError
|
214
|
+
@config.logger.error(
|
215
|
+
"#{LOG_LABEL} #{exception.class}#to_airbrake failed:" \
|
216
|
+
" #{attributes} must be a Hash"
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec.describe Airbrake::Ignorable do
|
2
|
+
let(:klass) do
|
3
|
+
mod = subject
|
4
|
+
Class.new { include(mod) }
|
5
|
+
end
|
6
|
+
|
7
|
+
it "ignores includee" do
|
8
|
+
instance = klass.new
|
9
|
+
expect(instance).not_to be_ignored
|
10
|
+
|
11
|
+
instance.ignore!
|
12
|
+
expect(instance).to be_ignored
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
RSpec.describe Airbrake::NestedException do
|
2
|
+
let(:config) { Airbrake::Config.new }
|
3
|
+
|
4
|
+
describe "#as_json" do
|
5
|
+
context "given exceptions with backtraces" do
|
6
|
+
it "unwinds nested exceptions" do
|
7
|
+
begin
|
8
|
+
begin
|
9
|
+
raise AirbrakeTestError
|
10
|
+
rescue AirbrakeTestError
|
11
|
+
Ruby21Error.raise_error('bingo')
|
12
|
+
end
|
13
|
+
rescue Ruby21Error => ex
|
14
|
+
nested_exception = described_class.new(config, ex)
|
15
|
+
exceptions = nested_exception.as_json
|
16
|
+
|
17
|
+
expect(exceptions.size).to eq(2)
|
18
|
+
expect(exceptions[0][:message]).to eq('bingo')
|
19
|
+
expect(exceptions[1][:message]).to eq('App crashed!')
|
20
|
+
expect(exceptions[0][:backtrace]).not_to be_empty
|
21
|
+
expect(exceptions[1][:backtrace]).not_to be_empty
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "unwinds no more than 3 nested exceptions" do
|
26
|
+
begin
|
27
|
+
begin
|
28
|
+
raise AirbrakeTestError
|
29
|
+
rescue AirbrakeTestError
|
30
|
+
begin
|
31
|
+
Ruby21Error.raise_error('bongo')
|
32
|
+
rescue Ruby21Error
|
33
|
+
begin
|
34
|
+
Ruby21Error.raise_error('bango')
|
35
|
+
rescue Ruby21Error
|
36
|
+
Ruby21Error.raise_error('bingo')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue Ruby21Error => ex
|
41
|
+
nested_exception = described_class.new(config, ex)
|
42
|
+
exceptions = nested_exception.as_json
|
43
|
+
|
44
|
+
expect(exceptions.size).to eq(3)
|
45
|
+
expect(exceptions[0][:message]).to eq('bingo')
|
46
|
+
expect(exceptions[1][:message]).to eq('bango')
|
47
|
+
expect(exceptions[2][:message]).to eq('bongo')
|
48
|
+
expect(exceptions[0][:backtrace]).not_to be_empty
|
49
|
+
expect(exceptions[1][:backtrace]).not_to be_empty
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "given exceptions without backtraces" do
|
55
|
+
it "sets backtrace to nil" do
|
56
|
+
begin
|
57
|
+
begin
|
58
|
+
raise AirbrakeTestError
|
59
|
+
rescue AirbrakeTestError => ex2
|
60
|
+
ex2.set_backtrace([])
|
61
|
+
Ruby21Error.raise_error('bingo')
|
62
|
+
end
|
63
|
+
rescue Ruby21Error => ex1
|
64
|
+
ex1.set_backtrace([])
|
65
|
+
nested_exception = described_class.new(config, ex1)
|
66
|
+
exceptions = nested_exception.as_json
|
67
|
+
|
68
|
+
expect(exceptions.size).to eq(2)
|
69
|
+
expect(exceptions[0][:backtrace]).to be_empty
|
70
|
+
expect(exceptions[1][:backtrace]).to be_empty
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,436 @@
|
|
1
|
+
# rubocop:disable Layout/DotPosition
|
2
|
+
RSpec.describe Airbrake::NoticeNotifier do
|
3
|
+
let(:user_params) do
|
4
|
+
{
|
5
|
+
project_id: 1,
|
6
|
+
project_key: 'abc',
|
7
|
+
logger: Logger.new('/dev/null'),
|
8
|
+
performance_stats: true
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:params) { {} }
|
13
|
+
let(:config) { Airbrake::Config.new(user_params.merge(params)) }
|
14
|
+
subject { described_class.new(config) }
|
15
|
+
|
16
|
+
describe "#new" do
|
17
|
+
describe "default filter addition" do
|
18
|
+
before { allow_any_instance_of(Airbrake::FilterChain).to receive(:add_filter) }
|
19
|
+
|
20
|
+
it "appends the context filter" do
|
21
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
22
|
+
.with(instance_of(Airbrake::Filters::ContextFilter))
|
23
|
+
subject
|
24
|
+
end
|
25
|
+
|
26
|
+
it "appends the exception attributes filter" do
|
27
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
28
|
+
.with(instance_of(Airbrake::Filters::ExceptionAttributesFilter))
|
29
|
+
subject
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when user config has some whitelist keys" do
|
33
|
+
let(:params) { { whitelist_keys: %w[foo] } }
|
34
|
+
|
35
|
+
it "appends the whitelist filter" do
|
36
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
37
|
+
.with(instance_of(Airbrake::Filters::KeysWhitelist))
|
38
|
+
described_class.new(config)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when user config doesn't have any whitelist keys" do
|
43
|
+
it "doesn't append the whitelist filter" do
|
44
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
45
|
+
.with(instance_of(Airbrake::Filters::KeysWhitelist))
|
46
|
+
described_class.new(config)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when user config has some blacklist keys" do
|
51
|
+
let(:params) { { blacklist_keys: %w[bar] } }
|
52
|
+
|
53
|
+
it "appends the blacklist filter" do
|
54
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
55
|
+
.with(instance_of(Airbrake::Filters::KeysBlacklist))
|
56
|
+
described_class.new(config)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when user config doesn't have any blacklist keys" do
|
61
|
+
it "doesn't append the blacklist filter" do
|
62
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
63
|
+
.with(instance_of(Airbrake::Filters::KeysBlacklist))
|
64
|
+
described_class.new(config)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when user config specifies a root directory" do
|
69
|
+
let(:params) { { root_directory: '/foo' } }
|
70
|
+
|
71
|
+
it "appends the root directory filter" do
|
72
|
+
expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
|
73
|
+
.with(instance_of(Airbrake::Filters::RootDirectoryFilter))
|
74
|
+
described_class.new(config)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when user config doesn't specify a root directory" do
|
79
|
+
it "doesn't append the root directory filter" do
|
80
|
+
expect_any_instance_of(Airbrake::Config).to receive(:root_directory)
|
81
|
+
.and_return(nil)
|
82
|
+
expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
|
83
|
+
.with(instance_of(Airbrake::Filters::RootDirectoryFilter))
|
84
|
+
described_class.new(config)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#notify" do
|
91
|
+
let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
|
92
|
+
|
93
|
+
subject { described_class.new(Airbrake::Config.new(user_params)) }
|
94
|
+
|
95
|
+
let(:body) do
|
96
|
+
{
|
97
|
+
'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
|
98
|
+
'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
|
99
|
+
}.to_json
|
100
|
+
end
|
101
|
+
|
102
|
+
before { stub_request(:post, endpoint).to_return(status: 201, body: body) }
|
103
|
+
|
104
|
+
it "returns a promise" do
|
105
|
+
expect(subject.notify('ex')).to be_an(Airbrake::Promise)
|
106
|
+
sleep 1
|
107
|
+
end
|
108
|
+
|
109
|
+
it "refines the notice object" do
|
110
|
+
subject.add_filter { |n| n[:params] = { foo: 'bar' } }
|
111
|
+
notice = subject.build_notice('ex')
|
112
|
+
subject.notify(notice)
|
113
|
+
expect(notice[:params]).to eq(foo: 'bar')
|
114
|
+
sleep 1
|
115
|
+
end
|
116
|
+
|
117
|
+
context "when a notice is not ignored" do
|
118
|
+
it "yields the notice" do
|
119
|
+
expect { |b| subject.notify('ex', &b) }
|
120
|
+
.to yield_with_args(Airbrake::Notice)
|
121
|
+
sleep 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "when a notice is ignored via a filter" do
|
126
|
+
before { subject.add_filter(&:ignore!) }
|
127
|
+
|
128
|
+
it "yields the notice" do
|
129
|
+
expect { |b| subject.notify('ex', &b) }
|
130
|
+
.to yield_with_args(Airbrake::Notice)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "returns a rejected promise" do
|
134
|
+
value = subject.notify('ex').value
|
135
|
+
expect(value['error']).to match(/was marked as ignored/)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "when a notice is ignored via an inline filter" do
|
140
|
+
before { subject.add_filter { raise AirbrakeTestError } }
|
141
|
+
|
142
|
+
it "doesn't invoke regular filters" do
|
143
|
+
expect { subject.notify('ex', &:ignore!) }.not_to raise_error
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when async sender has workers" do
|
148
|
+
it "sends an exception asynchronously" do
|
149
|
+
expect_any_instance_of(Airbrake::AsyncSender).to receive(:send)
|
150
|
+
subject.notify('foo', bingo: 'bango')
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "when async sender doesn't have workers" do
|
155
|
+
it "sends an exception synchronously" do
|
156
|
+
expect_any_instance_of(Airbrake::AsyncSender)
|
157
|
+
.to receive(:has_workers?).and_return(false)
|
158
|
+
expect_any_instance_of(Airbrake::SyncSender).to receive(:send)
|
159
|
+
subject.notify('foo', bingo: 'bango')
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "when the provided environment is ignored" do
|
164
|
+
let(:user_params) { { environment: 'test', ignore_environments: %w[test] } }
|
165
|
+
|
166
|
+
it "doesn't send an notice" do
|
167
|
+
expect_any_instance_of(Airbrake::AsyncSender).not_to receive(:send)
|
168
|
+
subject.notify('foo', bingo: 'bango')
|
169
|
+
end
|
170
|
+
|
171
|
+
it "returns a rejected promise" do
|
172
|
+
promise = subject.notify('foo', bingo: 'bango')
|
173
|
+
expect(promise.value).to eq('error' => "The 'test' environment is ignored")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "#notify_sync" do
|
179
|
+
let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
|
180
|
+
|
181
|
+
let(:user_params) do
|
182
|
+
{ project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
|
183
|
+
end
|
184
|
+
|
185
|
+
let(:body) do
|
186
|
+
{
|
187
|
+
'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
|
188
|
+
'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
before { stub_request(:post, endpoint).to_return(status: 201, body: body.to_json) }
|
193
|
+
|
194
|
+
it "returns a reponse hash" do
|
195
|
+
expect(subject.notify_sync('ex')).to eq(body)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "refines the notice object" do
|
199
|
+
subject.add_filter { |n| n[:params] = { foo: 'bar' } }
|
200
|
+
notice = subject.build_notice('ex')
|
201
|
+
subject.notify_sync(notice)
|
202
|
+
expect(notice[:params]).to eq(foo: 'bar')
|
203
|
+
end
|
204
|
+
|
205
|
+
it "sends an exception synchronously" do
|
206
|
+
subject.notify_sync('foo', bingo: 'bango')
|
207
|
+
expect(
|
208
|
+
a_request(:post, endpoint).with(
|
209
|
+
body: /"params":{.*"bingo":"bango".*}/
|
210
|
+
)
|
211
|
+
).to have_been_made.once
|
212
|
+
end
|
213
|
+
|
214
|
+
context "when a notice is not ignored" do
|
215
|
+
it "yields the notice" do
|
216
|
+
expect { |b| subject.notify_sync('ex', &b) }
|
217
|
+
.to yield_with_args(Airbrake::Notice)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context "when a notice is ignored via a filter" do
|
222
|
+
before { subject.add_filter(&:ignore!) }
|
223
|
+
|
224
|
+
it "yields the notice" do
|
225
|
+
expect { |b| subject.notify_sync('ex', &b) }
|
226
|
+
.to yield_with_args(Airbrake::Notice)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "returns an error hash" do
|
230
|
+
response = subject.notify_sync('ex')
|
231
|
+
expect(response['error']).to match(/was marked as ignored/)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context "when a notice is ignored via an inline filter" do
|
236
|
+
before { subject.add_filter { raise AirbrakeTestError } }
|
237
|
+
|
238
|
+
it "doesn't invoke regular filters" do
|
239
|
+
expect { subject.notify('ex', &:ignore!) }.not_to raise_error
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
context "when the provided environment is ignored" do
|
244
|
+
let(:params) { { environment: 'test', ignore_environments: %w[test] } }
|
245
|
+
|
246
|
+
it "doesn't send an notice" do
|
247
|
+
expect_any_instance_of(Airbrake::SyncSender).not_to receive(:send)
|
248
|
+
subject.notify_sync('foo', bingo: 'bango')
|
249
|
+
end
|
250
|
+
|
251
|
+
it "returns an error hash" do
|
252
|
+
expect(subject.notify_sync('foo'))
|
253
|
+
.to eq('error' => "The 'test' environment is ignored")
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe "#add_filter" do
|
259
|
+
context "given a block" do
|
260
|
+
it "appends a new filter to the filter chain" do
|
261
|
+
notifier = subject
|
262
|
+
b = proc {}
|
263
|
+
expect_any_instance_of(Airbrake::FilterChain)
|
264
|
+
.to receive(:add_filter) { |*args| expect(args.last).to be(b) }
|
265
|
+
notifier.add_filter(&b)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
context "given a class" do
|
270
|
+
it "appends a new filter to the filter chain" do
|
271
|
+
notifier = subject
|
272
|
+
klass = Class.new
|
273
|
+
expect_any_instance_of(Airbrake::FilterChain)
|
274
|
+
.to receive(:add_filter).with(klass)
|
275
|
+
notifier.add_filter(klass)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
describe "#build_notice" do
|
281
|
+
context "when given exception is another notice" do
|
282
|
+
it "merges params with the notice" do
|
283
|
+
notice = subject.build_notice('ex')
|
284
|
+
other = subject.build_notice(notice, foo: 'bar')
|
285
|
+
expect(other[:params]).to eq(foo: 'bar')
|
286
|
+
end
|
287
|
+
|
288
|
+
it "it returns the provided notice" do
|
289
|
+
notice = subject.build_notice('ex')
|
290
|
+
other = subject.build_notice(notice, foo: 'bar')
|
291
|
+
expect(other).to eq(notice)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
context "when given exception is an Exception" do
|
296
|
+
it "prevents mutation of passed-in params hash" do
|
297
|
+
params = { immutable: true }
|
298
|
+
notice = subject.build_notice('ex', params)
|
299
|
+
notice[:params][:mutable] = true
|
300
|
+
expect(params).to eq(immutable: true)
|
301
|
+
end
|
302
|
+
|
303
|
+
context "and also when it doesn't have own backtrace" do
|
304
|
+
context "and when the generated backtrace consists only of library frames" do
|
305
|
+
it "returns the full generated backtrace" do
|
306
|
+
backtrace = [
|
307
|
+
"/lib/airbrake-ruby/a.rb:84:in `build_notice'",
|
308
|
+
"/lib/airbrake-ruby/b.rb:124:in `send_notice'"
|
309
|
+
]
|
310
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
311
|
+
|
312
|
+
notice = subject.build_notice(Exception.new)
|
313
|
+
|
314
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
315
|
+
[
|
316
|
+
{ file: '/lib/airbrake-ruby/a.rb', line: 84, function: 'build_notice' },
|
317
|
+
{ file: '/lib/airbrake-ruby/b.rb', line: 124, function: 'send_notice' }
|
318
|
+
]
|
319
|
+
)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
context "and when the generated backtrace consists of mixed frames" do
|
324
|
+
it "returns the filtered backtrace" do
|
325
|
+
backtrace = [
|
326
|
+
"/airbrake-ruby/lib/airbrake-ruby/a.rb:84:in `b'",
|
327
|
+
"/airbrake-ruby/lib/foo/b.rb:84:in `build'",
|
328
|
+
"/airbrake-ruby/lib/bar/c.rb:124:in `send'"
|
329
|
+
]
|
330
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
331
|
+
|
332
|
+
notice = subject.build_notice(Exception.new)
|
333
|
+
|
334
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
335
|
+
[
|
336
|
+
{ file: '/airbrake-ruby/lib/foo/b.rb', line: 84, function: 'build' },
|
337
|
+
{ file: '/airbrake-ruby/lib/bar/c.rb', line: 124, function: 'send' }
|
338
|
+
]
|
339
|
+
)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# TODO: this seems to be bugged. Fix later.
|
346
|
+
context "when given exception is a Java exception", skip: true do
|
347
|
+
before do
|
348
|
+
expect(Airbrake::Backtrace).to receive(:java_exception?).and_return(true)
|
349
|
+
end
|
350
|
+
|
351
|
+
it "automatically generates the backtrace" do
|
352
|
+
backtrace = [
|
353
|
+
"org/jruby/RubyKernel.java:998:in `eval'",
|
354
|
+
"/ruby/stdlib/irb/workspace.rb:87:in `evaluate'",
|
355
|
+
"/ruby/stdlib/irb.rb:489:in `block in eval_input'"
|
356
|
+
]
|
357
|
+
allow(Kernel).to receive(:caller).and_return(backtrace)
|
358
|
+
|
359
|
+
notice = subject.build_notice(Exception.new)
|
360
|
+
|
361
|
+
# rubocop:disable Metrics/LineLength
|
362
|
+
expect(notice[:errors].first[:backtrace]).to eq(
|
363
|
+
[
|
364
|
+
{ file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
|
365
|
+
{ file: '/ruby/stdlib/irb/workspace.rb', line: 87, function: 'evaluate' },
|
366
|
+
{ file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' }
|
367
|
+
]
|
368
|
+
)
|
369
|
+
# rubocop:enable Metrics/LineLength
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
context "when async sender is closed" do
|
374
|
+
before do
|
375
|
+
expect_any_instance_of(Airbrake::AsyncSender)
|
376
|
+
.to receive(:closed?).and_return(true)
|
377
|
+
end
|
378
|
+
|
379
|
+
it "raises error" do
|
380
|
+
expect { subject.build_notice(Exception.new) }.to raise_error(
|
381
|
+
Airbrake::Error,
|
382
|
+
'attempted to build Exception with closed Airbrake instance'
|
383
|
+
)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
describe "#close" do
|
389
|
+
it "sends the close message to async sender" do
|
390
|
+
expect_any_instance_of(Airbrake::AsyncSender).to receive(:close)
|
391
|
+
subject.close
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
describe "#configured?" do
|
396
|
+
it { is_expected.to be_configured }
|
397
|
+
end
|
398
|
+
|
399
|
+
describe "#merge_context" do
|
400
|
+
it "merges the provided context with the notice object" do
|
401
|
+
expect_any_instance_of(Hash).to receive(:merge!).with(apples: 'oranges')
|
402
|
+
subject.merge_context(apples: 'oranges')
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
describe "#inspect" do
|
407
|
+
it "displays object information" do
|
408
|
+
expect(subject.inspect).to match(/
|
409
|
+
#<Airbrake::NoticeNotifier:0x\w+\s
|
410
|
+
project_id="\d+"\s
|
411
|
+
project_key=".+"\s
|
412
|
+
host="http.+"\s
|
413
|
+
filter_chain=\[.+\]>
|
414
|
+
/x)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
describe "#pretty_print" do
|
419
|
+
it "displays object information in a beautiful way" do
|
420
|
+
q = PP.new
|
421
|
+
|
422
|
+
# Guarding is needed to fix JRuby failure:
|
423
|
+
# NoMethodError: undefined method `[]' for nil:NilClass
|
424
|
+
q.guard_inspect_key { subject.pretty_print(q) }
|
425
|
+
|
426
|
+
expect(q.output).to match(/
|
427
|
+
#<Airbrake::NoticeNotifier:0x\w+\s
|
428
|
+
project_id="\d+"\s
|
429
|
+
project_key=".+"\s
|
430
|
+
host="http.+"\s
|
431
|
+
filter_chain=\[\n\s\s
|
432
|
+
/x)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
# rubocop:enable Layout/DotPosition
|