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.
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