airbrake-ruby 3.2.2-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +554 -0
  3. data/lib/airbrake-ruby/async_sender.rb +119 -0
  4. data/lib/airbrake-ruby/backtrace.rb +194 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +53 -0
  6. data/lib/airbrake-ruby/config.rb +238 -0
  7. data/lib/airbrake-ruby/config/validator.rb +63 -0
  8. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  9. data/lib/airbrake-ruby/file_cache.rb +48 -0
  10. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  11. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  18. data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
  19. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  20. data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  25. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  26. data/lib/airbrake-ruby/ignorable.rb +44 -0
  27. data/lib/airbrake-ruby/nested_exception.rb +39 -0
  28. data/lib/airbrake-ruby/notice.rb +165 -0
  29. data/lib/airbrake-ruby/notice_notifier.rb +228 -0
  30. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  31. data/lib/airbrake-ruby/promise.rb +99 -0
  32. data/lib/airbrake-ruby/response.rb +71 -0
  33. data/lib/airbrake-ruby/stat.rb +56 -0
  34. data/lib/airbrake-ruby/sync_sender.rb +111 -0
  35. data/lib/airbrake-ruby/tdigest.rb +393 -0
  36. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  37. data/lib/airbrake-ruby/truncator.rb +115 -0
  38. data/lib/airbrake-ruby/version.rb +6 -0
  39. data/spec/airbrake_spec.rb +171 -0
  40. data/spec/async_sender_spec.rb +154 -0
  41. data/spec/backtrace_spec.rb +438 -0
  42. data/spec/code_hunk_spec.rb +118 -0
  43. data/spec/config/validator_spec.rb +189 -0
  44. data/spec/config_spec.rb +281 -0
  45. data/spec/deploy_notifier_spec.rb +41 -0
  46. data/spec/file_cache.rb +36 -0
  47. data/spec/filter_chain_spec.rb +83 -0
  48. data/spec/filters/context_filter_spec.rb +25 -0
  49. data/spec/filters/dependency_filter_spec.rb +14 -0
  50. data/spec/filters/exception_attributes_filter_spec.rb +63 -0
  51. data/spec/filters/gem_root_filter_spec.rb +44 -0
  52. data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
  53. data/spec/filters/git_repository_filter.rb +53 -0
  54. data/spec/filters/git_revision_filter_spec.rb +126 -0
  55. data/spec/filters/keys_blacklist_spec.rb +236 -0
  56. data/spec/filters/keys_whitelist_spec.rb +205 -0
  57. data/spec/filters/root_directory_filter_spec.rb +42 -0
  58. data/spec/filters/sql_filter_spec.rb +219 -0
  59. data/spec/filters/system_exit_filter_spec.rb +14 -0
  60. data/spec/filters/thread_filter_spec.rb +279 -0
  61. data/spec/fixtures/notroot.txt +7 -0
  62. data/spec/fixtures/project_root/code.rb +221 -0
  63. data/spec/fixtures/project_root/empty_file.rb +0 -0
  64. data/spec/fixtures/project_root/long_line.txt +1 -0
  65. data/spec/fixtures/project_root/short_file.rb +3 -0
  66. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  67. data/spec/helpers.rb +9 -0
  68. data/spec/ignorable_spec.rb +14 -0
  69. data/spec/nested_exception_spec.rb +75 -0
  70. data/spec/notice_notifier_spec.rb +436 -0
  71. data/spec/notice_notifier_spec/options_spec.rb +266 -0
  72. data/spec/notice_spec.rb +297 -0
  73. data/spec/performance_notifier_spec.rb +287 -0
  74. data/spec/promise_spec.rb +165 -0
  75. data/spec/response_spec.rb +82 -0
  76. data/spec/spec_helper.rb +102 -0
  77. data/spec/stat_spec.rb +35 -0
  78. data/spec/sync_sender_spec.rb +140 -0
  79. data/spec/tdigest_spec.rb +230 -0
  80. data/spec/time_truncate_spec.rb +13 -0
  81. data/spec/truncator_spec.rb +238 -0
  82. metadata +278 -0
@@ -0,0 +1,7 @@
1
+ This
2
+ file
3
+ is
4
+ not
5
+ inside
6
+ root
7
+ directory
@@ -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
@@ -0,0 +1,3 @@
1
+ module Banana
2
+ attr_reader :bingo
3
+ end
@@ -0,0 +1,5 @@
1
+ module IgnoredFile
2
+ def ignore_me
3
+ puts 'Anybody here?'
4
+ end
5
+ end
data/spec/helpers.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Helpers
2
+ def fixture_path(filename)
3
+ File.expand_path(File.join('spec', 'fixtures', filename))
4
+ end
5
+
6
+ def project_root_path(filename)
7
+ fixture_path(File.join('project_root', filename))
8
+ end
9
+ end
@@ -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