airbrake-ruby 4.6.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +513 -0
  3. data/lib/airbrake-ruby/async_sender.rb +142 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +48 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +45 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +125 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +53 -0
  38. data/lib/airbrake-ruby/request.rb +45 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  45. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  46. data/lib/airbrake-ruby/truncator.rb +115 -0
  47. data/lib/airbrake-ruby/version.rb +6 -0
  48. data/spec/airbrake_spec.rb +324 -0
  49. data/spec/async_sender_spec.rb +155 -0
  50. data/spec/backtrace_spec.rb +427 -0
  51. data/spec/benchmark_spec.rb +33 -0
  52. data/spec/code_hunk_spec.rb +115 -0
  53. data/spec/config/validator_spec.rb +184 -0
  54. data/spec/config_spec.rb +154 -0
  55. data/spec/deploy_notifier_spec.rb +48 -0
  56. data/spec/file_cache.rb +36 -0
  57. data/spec/filter_chain_spec.rb +92 -0
  58. data/spec/filters/context_filter_spec.rb +23 -0
  59. data/spec/filters/dependency_filter_spec.rb +12 -0
  60. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  61. data/spec/filters/gem_root_filter_spec.rb +41 -0
  62. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  63. data/spec/filters/git_repository_filter.rb +61 -0
  64. data/spec/filters/git_revision_filter_spec.rb +126 -0
  65. data/spec/filters/keys_blacklist_spec.rb +225 -0
  66. data/spec/filters/keys_whitelist_spec.rb +194 -0
  67. data/spec/filters/root_directory_filter_spec.rb +39 -0
  68. data/spec/filters/sql_filter_spec.rb +219 -0
  69. data/spec/filters/system_exit_filter_spec.rb +14 -0
  70. data/spec/filters/thread_filter_spec.rb +277 -0
  71. data/spec/fixtures/notroot.txt +7 -0
  72. data/spec/fixtures/project_root/code.rb +221 -0
  73. data/spec/fixtures/project_root/empty_file.rb +0 -0
  74. data/spec/fixtures/project_root/long_line.txt +1 -0
  75. data/spec/fixtures/project_root/short_file.rb +3 -0
  76. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  77. data/spec/helpers.rb +9 -0
  78. data/spec/ignorable_spec.rb +14 -0
  79. data/spec/inspectable_spec.rb +45 -0
  80. data/spec/monotonic_time_spec.rb +12 -0
  81. data/spec/nested_exception_spec.rb +73 -0
  82. data/spec/notice_notifier_spec.rb +356 -0
  83. data/spec/notice_notifier_spec/options_spec.rb +259 -0
  84. data/spec/notice_spec.rb +296 -0
  85. data/spec/performance_breakdown_spec.rb +12 -0
  86. data/spec/performance_notifier_spec.rb +435 -0
  87. data/spec/promise_spec.rb +197 -0
  88. data/spec/query_spec.rb +11 -0
  89. data/spec/request_spec.rb +11 -0
  90. data/spec/response_spec.rb +88 -0
  91. data/spec/spec_helper.rb +100 -0
  92. data/spec/stashable_spec.rb +23 -0
  93. data/spec/stat_spec.rb +47 -0
  94. data/spec/sync_sender_spec.rb +133 -0
  95. data/spec/tdigest_spec.rb +230 -0
  96. data/spec/time_truncate_spec.rb +13 -0
  97. data/spec/timed_trace_spec.rb +125 -0
  98. data/spec/truncator_spec.rb +238 -0
  99. metadata +213 -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
@@ -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
@@ -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,45 @@
1
+ RSpec.describe Airbrake::Inspectable do
2
+ let(:klass) do
3
+ mod = subject
4
+ Class.new do
5
+ include(mod)
6
+
7
+ def initialize
8
+ @config = Airbrake::Config.new
9
+ @filter_chain = nil
10
+ end
11
+ end
12
+ end
13
+
14
+ describe "#inspect" do
15
+ it "displays object information" do
16
+ instance = klass.new
17
+ expect(instance.inspect).to match(/
18
+ #<:0x\w+\s
19
+ project_id=""\s
20
+ project_key=""\s
21
+ host="http.+"\s
22
+ filter_chain=nil>
23
+ /x)
24
+ end
25
+ end
26
+
27
+ describe "#pretty_print" do
28
+ it "displays object information in a beautiful way" do
29
+ q = PP.new
30
+
31
+ instance = klass.new
32
+ # Guarding is needed to fix JRuby failure:
33
+ # NoMethodError: undefined method `[]' for nil:NilClass
34
+ q.guard_inspect_key { instance.pretty_print(q) }
35
+
36
+ expect(q.output).to match(/
37
+ #<:0x\w+\s
38
+ project_id=""\s
39
+ project_key=""\s
40
+ host="http.+"\s
41
+ filter_chain=nil
42
+ /x)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ RSpec.describe Airbrake::MonotonicTime do
2
+ describe ".time_in_ms" do
3
+ it "returns monotonic time in milliseconds" do
4
+ expect(subject.time_in_ms).to be_a(Float)
5
+ end
6
+
7
+ it "always returns time in the future" do
8
+ old_time = subject.time_in_ms
9
+ expect(subject.time_in_ms).to be > old_time
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,73 @@
1
+ RSpec.describe Airbrake::NestedException do
2
+ describe "#as_json" do
3
+ context "given exceptions with backtraces" do
4
+ it "unwinds nested exceptions" do
5
+ begin
6
+ begin
7
+ raise AirbrakeTestError
8
+ rescue AirbrakeTestError
9
+ Ruby21Error.raise_error('bingo')
10
+ end
11
+ rescue Ruby21Error => ex
12
+ nested_exception = described_class.new(ex)
13
+ exceptions = nested_exception.as_json
14
+
15
+ expect(exceptions.size).to eq(2)
16
+ expect(exceptions[0][:message]).to eq('bingo')
17
+ expect(exceptions[1][:message]).to eq('App crashed!')
18
+ expect(exceptions[0][:backtrace]).not_to be_empty
19
+ expect(exceptions[1][:backtrace]).not_to be_empty
20
+ end
21
+ end
22
+
23
+ it "unwinds no more than 3 nested exceptions" do
24
+ begin
25
+ begin
26
+ raise AirbrakeTestError
27
+ rescue AirbrakeTestError
28
+ begin
29
+ Ruby21Error.raise_error('bongo')
30
+ rescue Ruby21Error
31
+ begin
32
+ Ruby21Error.raise_error('bango')
33
+ rescue Ruby21Error
34
+ Ruby21Error.raise_error('bingo')
35
+ end
36
+ end
37
+ end
38
+ rescue Ruby21Error => ex
39
+ nested_exception = described_class.new(ex)
40
+ exceptions = nested_exception.as_json
41
+
42
+ expect(exceptions.size).to eq(3)
43
+ expect(exceptions[0][:message]).to eq('bingo')
44
+ expect(exceptions[1][:message]).to eq('bango')
45
+ expect(exceptions[2][:message]).to eq('bongo')
46
+ expect(exceptions[0][:backtrace]).not_to be_empty
47
+ expect(exceptions[1][:backtrace]).not_to be_empty
48
+ end
49
+ end
50
+ end
51
+
52
+ context "given exceptions without backtraces" do
53
+ it "sets backtrace to nil" do
54
+ begin
55
+ begin
56
+ raise AirbrakeTestError
57
+ rescue AirbrakeTestError => ex2
58
+ ex2.set_backtrace([])
59
+ Ruby21Error.raise_error('bingo')
60
+ end
61
+ rescue Ruby21Error => ex1
62
+ ex1.set_backtrace([])
63
+ nested_exception = described_class.new(ex1)
64
+ exceptions = nested_exception.as_json
65
+
66
+ expect(exceptions.size).to eq(2)
67
+ expect(exceptions[0][:backtrace]).to be_empty
68
+ expect(exceptions[1][:backtrace]).to be_empty
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,356 @@
1
+ RSpec.describe Airbrake::NoticeNotifier do
2
+ before do
3
+ Airbrake::Config.instance = Airbrake::Config.new(
4
+ project_id: 1,
5
+ project_key: 'abc',
6
+ logger: Logger.new('/dev/null'),
7
+ performance_stats: true
8
+ )
9
+ end
10
+
11
+ describe "#new" do
12
+ describe "default filter addition" do
13
+ before { allow_any_instance_of(Airbrake::FilterChain).to receive(:add_filter) }
14
+
15
+ it "appends the context filter" do
16
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
17
+ .with(instance_of(Airbrake::Filters::ContextFilter))
18
+ subject
19
+ end
20
+
21
+ it "appends the exception attributes filter" do
22
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
23
+ .with(instance_of(Airbrake::Filters::ExceptionAttributesFilter))
24
+ subject
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "#notify" do
30
+ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
31
+
32
+ let(:body) do
33
+ {
34
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
35
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
36
+ }.to_json
37
+ end
38
+
39
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body) }
40
+
41
+ it "returns a promise" do
42
+ expect(subject.notify('ex')).to be_an(Airbrake::Promise)
43
+ sleep 1
44
+ end
45
+
46
+ it "refines the notice object" do
47
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
48
+ notice = subject.build_notice('ex')
49
+ subject.notify(notice)
50
+ expect(notice[:params]).to eq(foo: 'bar')
51
+ sleep 1
52
+ end
53
+
54
+ context "when config is invalid" do
55
+ before { Airbrake::Config.instance.merge(project_id: nil) }
56
+
57
+ it "returns a rejected promise" do
58
+ promise = subject.notify({})
59
+ expect(promise).to be_rejected
60
+ end
61
+ end
62
+
63
+ context "when a notice is not ignored" do
64
+ it "yields the notice" do
65
+ expect { |b| subject.notify('ex', &b) }
66
+ .to yield_with_args(Airbrake::Notice)
67
+ sleep 1
68
+ end
69
+ end
70
+
71
+ context "when a notice is ignored via a filter" do
72
+ before { subject.add_filter(&:ignore!) }
73
+
74
+ it "yields the notice" do
75
+ expect { |b| subject.notify('ex', &b) }
76
+ .to yield_with_args(Airbrake::Notice)
77
+ end
78
+
79
+ it "returns a rejected promise" do
80
+ value = subject.notify('ex').value
81
+ expect(value['error']).to match(/was marked as ignored/)
82
+ end
83
+ end
84
+
85
+ context "when a notice is ignored via an inline filter" do
86
+ before { subject.add_filter { raise AirbrakeTestError } }
87
+
88
+ it "doesn't invoke regular filters" do
89
+ expect { subject.notify('ex', &:ignore!) }.not_to raise_error
90
+ end
91
+ end
92
+
93
+ context "when async sender has workers" do
94
+ it "sends an exception asynchronously" do
95
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:send)
96
+ subject.notify('foo', bingo: 'bango')
97
+ end
98
+ end
99
+
100
+ context "when async sender doesn't have workers" do
101
+ it "sends an exception synchronously" do
102
+ expect_any_instance_of(Airbrake::AsyncSender)
103
+ .to receive(:has_workers?).and_return(false)
104
+ expect_any_instance_of(Airbrake::SyncSender).to receive(:send)
105
+ subject.notify('foo', bingo: 'bango')
106
+ end
107
+ end
108
+
109
+ context "when the provided environment is ignored" do
110
+ before do
111
+ Airbrake::Config.instance.merge(
112
+ environment: 'test',
113
+ ignore_environments: %w[test]
114
+ )
115
+ end
116
+
117
+ it "doesn't send an notice" do
118
+ expect_any_instance_of(Airbrake::AsyncSender).not_to receive(:send)
119
+ subject.notify('foo', bingo: 'bango')
120
+ end
121
+
122
+ it "returns a rejected promise" do
123
+ promise = subject.notify('foo', bingo: 'bango')
124
+ expect(promise.value).to eq('error' => "current environment 'test' is ignored")
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#notify_sync" do
130
+ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
131
+
132
+ let(:body) do
133
+ {
134
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
135
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
136
+ }
137
+ end
138
+
139
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body.to_json) }
140
+
141
+ it "returns a reponse hash" do
142
+ expect(subject.notify_sync('ex')).to eq(body)
143
+ end
144
+
145
+ it "refines the notice object" do
146
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
147
+ notice = subject.build_notice('ex')
148
+ subject.notify_sync(notice)
149
+ expect(notice[:params]).to eq(foo: 'bar')
150
+ end
151
+
152
+ it "sends an exception synchronously" do
153
+ subject.notify_sync('foo', bingo: 'bango')
154
+ expect(
155
+ a_request(:post, endpoint).with(
156
+ body: /"params":{.*"bingo":"bango".*}/
157
+ )
158
+ ).to have_been_made.once
159
+ end
160
+
161
+ context "when a notice is not ignored" do
162
+ it "yields the notice" do
163
+ expect { |b| subject.notify_sync('ex', &b) }
164
+ .to yield_with_args(Airbrake::Notice)
165
+ end
166
+ end
167
+
168
+ context "when a notice is ignored via a filter" do
169
+ before { subject.add_filter(&:ignore!) }
170
+
171
+ it "yields the notice" do
172
+ expect { |b| subject.notify_sync('ex', &b) }
173
+ .to yield_with_args(Airbrake::Notice)
174
+ end
175
+
176
+ it "returns an error hash" do
177
+ response = subject.notify_sync('ex')
178
+ expect(response['error']).to match(/was marked as ignored/)
179
+ end
180
+ end
181
+
182
+ context "when a notice is ignored via an inline filter" do
183
+ before { subject.add_filter { raise AirbrakeTestError } }
184
+
185
+ it "doesn't invoke regular filters" do
186
+ expect { subject.notify('ex', &:ignore!) }.not_to raise_error
187
+ end
188
+ end
189
+
190
+ context "when the provided environment is ignored" do
191
+ before do
192
+ Airbrake::Config.instance.merge(
193
+ environment: 'test', ignore_environments: %w[test]
194
+ )
195
+ end
196
+
197
+ it "doesn't send an notice" do
198
+ expect_any_instance_of(Airbrake::SyncSender).not_to receive(:send)
199
+ subject.notify_sync('foo', bingo: 'bango')
200
+ end
201
+
202
+ it "returns an error hash" do
203
+ expect(subject.notify_sync('foo'))
204
+ .to eq('error' => "current environment 'test' is ignored")
205
+ end
206
+ end
207
+ end
208
+
209
+ describe "#add_filter" do
210
+ context "given a block" do
211
+ it "appends a new filter to the filter chain" do
212
+ notifier = subject
213
+ b = proc {}
214
+ expect_any_instance_of(Airbrake::FilterChain)
215
+ .to receive(:add_filter) { |*args| expect(args.last).to be(b) }
216
+ notifier.add_filter(&b)
217
+ end
218
+ end
219
+
220
+ context "given a class" do
221
+ it "appends a new filter to the filter chain" do
222
+ notifier = subject
223
+ klass = Class.new
224
+ expect_any_instance_of(Airbrake::FilterChain)
225
+ .to receive(:add_filter).with(klass)
226
+ notifier.add_filter(klass)
227
+ end
228
+ end
229
+ end
230
+
231
+ describe "#build_notice" do
232
+ context "when given exception is another notice" do
233
+ it "merges params with the notice" do
234
+ notice = subject.build_notice('ex')
235
+ other = subject.build_notice(notice, foo: 'bar')
236
+ expect(other[:params]).to eq(foo: 'bar')
237
+ end
238
+
239
+ it "it returns the provided notice" do
240
+ notice = subject.build_notice('ex')
241
+ other = subject.build_notice(notice, foo: 'bar')
242
+ expect(other).to eq(notice)
243
+ end
244
+ end
245
+
246
+ context "when given exception is an Exception" do
247
+ it "prevents mutation of passed-in params hash" do
248
+ params = { immutable: true }
249
+ notice = subject.build_notice('ex', params)
250
+ notice[:params][:mutable] = true
251
+ expect(params).to eq(immutable: true)
252
+ end
253
+
254
+ context "and also when it doesn't have own backtrace" do
255
+ context "and when the generated backtrace consists only of library frames" do
256
+ it "returns the full generated backtrace" do
257
+ backtrace = [
258
+ "/lib/airbrake-ruby/a.rb:84:in `build_notice'",
259
+ "/lib/airbrake-ruby/b.rb:124:in `send_notice'"
260
+ ]
261
+ allow(Kernel).to receive(:caller).and_return(backtrace)
262
+
263
+ notice = subject.build_notice(Exception.new)
264
+
265
+ expect(notice[:errors].first[:backtrace]).to eq(
266
+ [
267
+ { file: '/lib/airbrake-ruby/a.rb', line: 84, function: 'build_notice' },
268
+ { file: '/lib/airbrake-ruby/b.rb', line: 124, function: 'send_notice' }
269
+ ]
270
+ )
271
+ end
272
+ end
273
+
274
+ context "and when the generated backtrace consists of mixed frames" do
275
+ it "returns the filtered backtrace" do
276
+ backtrace = [
277
+ "/airbrake-ruby/lib/airbrake-ruby/a.rb:84:in `b'",
278
+ "/airbrake-ruby/lib/foo/b.rb:84:in `build'",
279
+ "/airbrake-ruby/lib/bar/c.rb:124:in `send'"
280
+ ]
281
+ allow(Kernel).to receive(:caller).and_return(backtrace)
282
+
283
+ notice = subject.build_notice(Exception.new)
284
+
285
+ expect(notice[:errors].first[:backtrace]).to eq(
286
+ [
287
+ { file: '/airbrake-ruby/lib/foo/b.rb', line: 84, function: 'build' },
288
+ { file: '/airbrake-ruby/lib/bar/c.rb', line: 124, function: 'send' }
289
+ ]
290
+ )
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ # TODO: this seems to be bugged. Fix later.
297
+ context "when given exception is a Java exception", skip: true do
298
+ before do
299
+ expect(Airbrake::Backtrace).to receive(:java_exception?).and_return(true)
300
+ end
301
+
302
+ it "automatically generates the backtrace" do
303
+ backtrace = [
304
+ "org/jruby/RubyKernel.java:998:in `eval'",
305
+ "/ruby/stdlib/irb/workspace.rb:87:in `evaluate'",
306
+ "/ruby/stdlib/irb.rb:489:in `block in eval_input'"
307
+ ]
308
+ allow(Kernel).to receive(:caller).and_return(backtrace)
309
+
310
+ notice = subject.build_notice(Exception.new)
311
+
312
+ # rubocop:disable Metrics/LineLength
313
+ expect(notice[:errors].first[:backtrace]).to eq(
314
+ [
315
+ { file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
316
+ { file: '/ruby/stdlib/irb/workspace.rb', line: 87, function: 'evaluate' },
317
+ { file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' }
318
+ ]
319
+ )
320
+ # rubocop:enable Metrics/LineLength
321
+ end
322
+ end
323
+
324
+ context "when async sender is closed" do
325
+ before do
326
+ expect_any_instance_of(Airbrake::AsyncSender)
327
+ .to receive(:closed?).and_return(true)
328
+ end
329
+
330
+ it "raises error" do
331
+ expect { subject.build_notice(Exception.new) }.to raise_error(
332
+ Airbrake::Error,
333
+ 'attempted to build Exception with closed Airbrake instance'
334
+ )
335
+ end
336
+ end
337
+ end
338
+
339
+ describe "#close" do
340
+ it "sends the close message to async sender" do
341
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:close)
342
+ subject.close
343
+ end
344
+ end
345
+
346
+ describe "#configured?" do
347
+ it { is_expected.to be_configured }
348
+ end
349
+
350
+ describe "#merge_context" do
351
+ it "merges the provided context with the notice object" do
352
+ expect_any_instance_of(Hash).to receive(:merge!).with(apples: 'oranges')
353
+ subject.merge_context(apples: 'oranges')
354
+ end
355
+ end
356
+ end