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,17 @@
1
+ module Airbrake
2
+ # TimeTruncate contains methods for truncating time.
3
+ #
4
+ # @api private
5
+ # @since v3.2.0
6
+ module TimeTruncate
7
+ # Truncate +time+ to floor minute and turn it into an RFC3339 timestamp.
8
+ #
9
+ # @param [Time] time
10
+ # @return [String]
11
+ def self.utc_truncate_minutes(time)
12
+ tm = time.getutc
13
+
14
+ Time.utc(tm.year, tm.month, tm.day, tm.hour, tm.min).to_datetime.rfc3339
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ module Airbrake
2
+ # TimedTrace represents a chunk of code performance of which was measured and
3
+ # stored under a label. The chunk is called a "span".
4
+ #
5
+ # @example
6
+ # timed_trace = TimedTrace.new
7
+ # timed_trace.span('http request') do
8
+ # http.get('example.com')
9
+ # end
10
+ # timed_trace.spans #=> { 'http request' => 0.123 }
11
+ #
12
+ # @api public
13
+ # @since v4.3.0
14
+ class TimedTrace
15
+ # @param [String] label
16
+ # @return [Airbrake::TimedTrace]
17
+ def self.span(label, &block)
18
+ new.tap { |timed_trace| timed_trace.span(label, &block) }
19
+ end
20
+
21
+ def initialize
22
+ @spans = {}
23
+ end
24
+
25
+ # @param [String] label
26
+ # @return [Boolean]
27
+ def span(label)
28
+ start_span(label)
29
+ yield
30
+ stop_span(label)
31
+ end
32
+
33
+ # @param [String] label
34
+ # @return [Boolean]
35
+ def start_span(label)
36
+ return false if @spans.key?(label)
37
+
38
+ @spans[label] = Airbrake::Benchmark.new
39
+ true
40
+ end
41
+
42
+ # @param [String] label
43
+ # @return [Boolean]
44
+ def stop_span(label)
45
+ return false unless @spans.key?(label)
46
+
47
+ @spans[label].stop
48
+ true
49
+ end
50
+
51
+ # @return [Hash<String=>Float>]
52
+ def spans
53
+ @spans.each_with_object({}) do |(label, benchmark), new_spans|
54
+ new_spans[label] = benchmark.duration
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,115 @@
1
+ module Airbrake
2
+ # This class is responsible for truncation of too big objects. Mainly, you
3
+ # should use it for simple objects such as strings, hashes, & arrays.
4
+ #
5
+ # @api private
6
+ # @since v1.0.0
7
+ class Truncator
8
+ # @return [Hash] the options for +String#encode+
9
+ ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
10
+
11
+ # @return [String] the temporary encoding to be used when fixing invalid
12
+ # strings with +ENCODING_OPTIONS+
13
+ TEMP_ENCODING = 'utf-16'.freeze
14
+
15
+ # @return [String] what to append when something is a circular reference
16
+ CIRCULAR = '[Circular]'.freeze
17
+
18
+ # @return [String] what to append when something is truncated
19
+ TRUNCATED = '[Truncated]'.freeze
20
+
21
+ # @return [Array<Class>] The types that can contain references to itself
22
+ CIRCULAR_TYPES = [Array, Hash, Set].freeze
23
+
24
+ # @param [Integer] max_size maximum size of hashes, arrays and strings
25
+ def initialize(max_size)
26
+ @max_size = max_size
27
+ end
28
+
29
+ # Performs deep truncation of arrays, hashes, sets & strings. Uses a
30
+ # placeholder for recursive objects (`[Circular]`).
31
+ #
32
+ # @param [Object] object The object to truncate
33
+ # @param [Set] seen The cache that helps to detect recursion
34
+ # @return [Object] truncated object
35
+ def truncate(object, seen = Set.new)
36
+ if seen.include?(object.object_id)
37
+ return CIRCULAR if CIRCULAR_TYPES.any? { |t| object.is_a?(t) }
38
+ return object
39
+ end
40
+ truncate_object(object, seen << object.object_id)
41
+ end
42
+
43
+ # Reduces maximum allowed size of hashes, arrays, sets & strings by half.
44
+ # @return [Integer] current +max_size+ value
45
+ def reduce_max_size
46
+ @max_size /= 2
47
+ end
48
+
49
+ private
50
+
51
+ def truncate_object(object, seen)
52
+ case object
53
+ when Hash then truncate_hash(object, seen)
54
+ when Array then truncate_array(object, seen)
55
+ when Set then truncate_set(object, seen)
56
+ when String then truncate_string(object)
57
+ when Numeric, TrueClass, FalseClass, Symbol, NilClass then object
58
+ else
59
+ truncate_string(stringify_object(object))
60
+ end
61
+ end
62
+
63
+ def truncate_string(str)
64
+ fixed_str = replace_invalid_characters(str)
65
+ return fixed_str if fixed_str.length <= @max_size
66
+ (fixed_str.slice(0, @max_size) + TRUNCATED).freeze
67
+ end
68
+
69
+ def stringify_object(object)
70
+ object.to_json
71
+ rescue *Notice::JSON_EXCEPTIONS
72
+ object.to_s
73
+ end
74
+
75
+ def truncate_hash(hash, seen)
76
+ truncated_hash = {}
77
+ hash.each_with_index do |(key, val), idx|
78
+ break if idx + 1 > @max_size
79
+ truncated_hash[key] = truncate(val, seen)
80
+ end
81
+
82
+ truncated_hash.freeze
83
+ end
84
+
85
+ def truncate_array(array, seen)
86
+ array.slice(0, @max_size).map! { |elem| truncate(elem, seen) }.freeze
87
+ end
88
+
89
+ def truncate_set(set, seen)
90
+ truncated_set = Set.new
91
+
92
+ set.each do |elem|
93
+ truncated_set << truncate(elem, seen)
94
+ break if truncated_set.size >= @max_size
95
+ end
96
+
97
+ truncated_set.freeze
98
+ end
99
+
100
+ # Replaces invalid characters in a string with arbitrary encoding.
101
+ #
102
+ # @param [String] str The string to replace characters
103
+ # @return [String] a UTF-8 encoded string
104
+ # @see https://github.com/flori/json/commit/3e158410e81f94dbbc3da6b7b35f4f64983aa4e3
105
+ def replace_invalid_characters(str)
106
+ encoding = str.encoding
107
+ utf8_string = (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII)
108
+ return str if utf8_string && str.valid_encoding?
109
+
110
+ temp_str = str.dup
111
+ temp_str.encode!(TEMP_ENCODING, ENCODING_OPTIONS) if utf8_string
112
+ temp_str.encode!('utf-8', ENCODING_OPTIONS)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,6 @@
1
+ # We use Semantic Versioning v2.0.0
2
+ # More information: http://semver.org/
3
+ module Airbrake
4
+ # @return [String] the library version
5
+ AIRBRAKE_RUBY_VERSION = '4.6.0'.freeze
6
+ end
@@ -0,0 +1,324 @@
1
+ RSpec.describe Airbrake do
2
+ it "gets initialized with a performance notifier" do
3
+ expect(described_class.performance_notifier).not_to be_nil
4
+ end
5
+
6
+ it "gets initialized with a notice notifier" do
7
+ expect(described_class.notice_notifier).not_to be_nil
8
+ end
9
+
10
+ it "gets initialized with a deploy notifier" do
11
+ expect(described_class.deploy_notifier).not_to be_nil
12
+ end
13
+
14
+ describe ".configure" do
15
+ before do
16
+ Airbrake::Config.instance = Airbrake::Config.new
17
+ described_class.reset
18
+ end
19
+
20
+ after { described_class.reset }
21
+
22
+ it "yields the config" do
23
+ expect do |b|
24
+ begin
25
+ described_class.configure(&b)
26
+ rescue Airbrake::Error
27
+ nil
28
+ end
29
+ end.to yield_with_args(Airbrake::Config)
30
+ end
31
+
32
+ it "sets logger to Airbrake::Loggable" do
33
+ logger = Logger.new(File::NULL)
34
+ described_class.configure do |c|
35
+ c.project_id = 1
36
+ c.project_key = '123'
37
+ c.logger = logger
38
+ end
39
+
40
+ expect(Airbrake::Loggable.instance).to eql(logger)
41
+ end
42
+
43
+ it "makes Airbrake configured" do
44
+ expect(described_class).not_to be_configured
45
+
46
+ described_class.configure do |c|
47
+ c.project_id = 1
48
+ c.project_key = '2'
49
+ end
50
+
51
+ expect(described_class).to be_configured
52
+ end
53
+
54
+ context "when a notifier was configured" do
55
+ before do
56
+ expect(described_class).to receive(:configured?).and_return(true)
57
+ end
58
+
59
+ it "closes previously configured notice notifier" do
60
+ expect(described_class).to receive(:close)
61
+ described_class.configure {}
62
+ end
63
+ end
64
+
65
+ context "when a notifier wasn't configured" do
66
+ before do
67
+ expect(described_class).to receive(:configured?).and_return(false)
68
+ end
69
+
70
+ it "doesn't close previously configured notice notifier" do
71
+ expect(described_class).not_to receive(:close)
72
+ described_class.configure {}
73
+ end
74
+ end
75
+
76
+ context "when called multiple times" do
77
+ it "doesn't overwrite performance notifier" do
78
+ described_class.configure {}
79
+ performance_notifier = described_class.performance_notifier
80
+
81
+ described_class.configure {}
82
+ expect(described_class.performance_notifier).to eql(performance_notifier)
83
+ end
84
+
85
+ it "doesn't overwrite notice notifier" do
86
+ described_class.configure {}
87
+ notice_notifier = described_class.notice_notifier
88
+
89
+ described_class.configure {}
90
+ expect(described_class.notice_notifier).to eql(notice_notifier)
91
+ end
92
+
93
+ it "doesn't overwrite deploy notifier" do
94
+ described_class.configure {}
95
+ deploy_notifier = described_class.deploy_notifier
96
+
97
+ described_class.configure {}
98
+ expect(described_class.deploy_notifier).to eql(deploy_notifier)
99
+ end
100
+ end
101
+
102
+ context "when blacklist_keys gets configured" do
103
+ before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
104
+
105
+ it "adds blacklist filter" do
106
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
107
+ .with(an_instance_of(Airbrake::Filters::KeysBlacklist))
108
+ described_class.configure { |c| c.blacklist_keys = %w[password] }
109
+ end
110
+
111
+ it "initializes blacklist with specified parameters" do
112
+ expect(Airbrake::Filters::KeysBlacklist).to receive(:new).with(%w[password])
113
+ described_class.configure { |c| c.blacklist_keys = %w[password] }
114
+ end
115
+ end
116
+
117
+ context "when whitelist_keys gets configured" do
118
+ before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
119
+
120
+ it "adds whitelist filter" do
121
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
122
+ .with(an_instance_of(Airbrake::Filters::KeysWhitelist))
123
+ described_class.configure { |c| c.whitelist_keys = %w[banana] }
124
+ end
125
+
126
+ it "initializes whitelist with specified parameters" do
127
+ expect(Airbrake::Filters::KeysWhitelist).to receive(:new).with(%w[banana])
128
+ described_class.configure { |c| c.whitelist_keys = %w[banana] }
129
+ end
130
+ end
131
+
132
+ context "when root_directory gets configured" do
133
+ before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
134
+
135
+ it "adds root directory filter" do
136
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
137
+ .with(an_instance_of(Airbrake::Filters::RootDirectoryFilter))
138
+ described_class.configure { |c| c.root_directory = '/my/path' }
139
+ end
140
+
141
+ it "initializes root directory filter with specified path" do
142
+ expect(Airbrake::Filters::RootDirectoryFilter)
143
+ .to receive(:new).with('/my/path')
144
+ described_class.configure { |c| c.root_directory = '/my/path' }
145
+ end
146
+
147
+ it "adds git revision filter" do
148
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
149
+ .with(an_instance_of(Airbrake::Filters::GitRevisionFilter))
150
+ described_class.configure { |c| c.root_directory = '/my/path' }
151
+ end
152
+
153
+ it "initializes git revision filter with correct root directory" do
154
+ expect(Airbrake::Filters::GitRevisionFilter)
155
+ .to receive(:new).with('/my/path')
156
+ described_class.configure { |c| c.root_directory = '/my/path' }
157
+ end
158
+
159
+ it "adds git repository filter" do
160
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
161
+ .with(an_instance_of(Airbrake::Filters::GitRepositoryFilter))
162
+ described_class.configure { |c| c.root_directory = '/my/path' }
163
+ end
164
+
165
+ it "initializes git repository filter with correct root directory" do
166
+ expect(Airbrake::Filters::GitRepositoryFilter)
167
+ .to receive(:new).with('/my/path')
168
+ described_class.configure { |c| c.root_directory = '/my/path' }
169
+ end
170
+
171
+ it "adds git last checkout filter" do
172
+ expect(Airbrake.notice_notifier).to receive(:add_filter)
173
+ .with(an_instance_of(Airbrake::Filters::GitLastCheckoutFilter))
174
+ described_class.configure { |c| c.root_directory = '/my/path' }
175
+ end
176
+
177
+ it "initializes git last checkout filter with correct root directory" do
178
+ expect(Airbrake::Filters::GitLastCheckoutFilter)
179
+ .to receive(:new).with('/my/path')
180
+ described_class.configure { |c| c.root_directory = '/my/path' }
181
+ end
182
+ end
183
+ end
184
+
185
+ describe "#reset" do
186
+ context "when Airbrake was previously configured" do
187
+ before do
188
+ expect(described_class).to receive(:configured?).and_return(true)
189
+ end
190
+
191
+ it "closes notice notifier" do
192
+ expect(described_class).to receive(:close)
193
+ subject.reset
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "#notify_request" do
199
+ context "when :stash key is not provided" do
200
+ it "doesn't add anything to the stash of the request" do
201
+ expect(described_class.performance_notifier).to receive(:notify) do |request|
202
+ expect(request.stash).to be_empty
203
+ end
204
+
205
+ described_class.notify_request(
206
+ method: 'GET',
207
+ route: '/',
208
+ status_code: 200,
209
+ start_time: Time.now
210
+ )
211
+ end
212
+ end
213
+
214
+ context "when :stash key is provided" do
215
+ it "adds the value as the stash of the request" do
216
+ expect(described_class.performance_notifier).to receive(:notify) do |request|
217
+ expect(request.stash).to eq(request_id: 1)
218
+ end
219
+
220
+ described_class.notify_request(
221
+ {
222
+ method: 'GET',
223
+ route: '/',
224
+ status_code: 200,
225
+ start_time: Time.now
226
+ },
227
+ request_id: 1
228
+ )
229
+ end
230
+ end
231
+ end
232
+
233
+ describe "#notify_query" do
234
+ context "when :stash key is not provided" do
235
+ it "doesn't add anything to the stash of the query" do
236
+ expect(described_class.performance_notifier).to receive(:notify) do |query|
237
+ expect(query.stash).to be_empty
238
+ end
239
+
240
+ described_class.notify_query(
241
+ method: 'GET',
242
+ route: '/',
243
+ query: '',
244
+ start_time: Time.now
245
+ )
246
+ end
247
+ end
248
+
249
+ context "when :stash key is provided" do
250
+ it "adds the value as the stash of the query" do
251
+ expect(described_class.performance_notifier).to receive(:notify) do |query|
252
+ expect(query.stash).to eq(request_id: 1)
253
+ end
254
+
255
+ described_class.notify_query(
256
+ {
257
+ method: 'GET',
258
+ route: '/',
259
+ query: '',
260
+ start_time: Time.now
261
+ },
262
+ request_id: 1
263
+ )
264
+ end
265
+ end
266
+ end
267
+
268
+ describe "#notify_performance_breakdown" do
269
+ context "when :stash key is not provided" do
270
+ it "doesn't add anything to the stash of the performance breakdown" do
271
+ expect(described_class.performance_notifier).to receive(:notify) do |query|
272
+ expect(query.stash).to be_empty
273
+ end
274
+
275
+ described_class.notify_query(
276
+ method: 'GET',
277
+ route: '/',
278
+ query: '',
279
+ start_time: Time.now
280
+ )
281
+ end
282
+ end
283
+
284
+ context "when :stash key is provided" do
285
+ it "adds the value as the stash of the performance breakdown" do
286
+ expect(
287
+ described_class.performance_notifier
288
+ ).to receive(:notify) do |performance_breakdown|
289
+ expect(performance_breakdown.stash).to eq(request_id: 1)
290
+ end
291
+
292
+ described_class.notify_performance_breakdown(
293
+ {
294
+ method: 'GET',
295
+ route: '/',
296
+ response_type: :html,
297
+ groups: {},
298
+ start_time: Time.now
299
+ },
300
+ request_id: 1
301
+ )
302
+ end
303
+ end
304
+ end
305
+
306
+ describe ".performance_notifier" do
307
+ it "returns a performance notifier" do
308
+ expect(described_class.performance_notifier)
309
+ .to be_an(Airbrake::PerformanceNotifier)
310
+ end
311
+ end
312
+
313
+ describe ".notice_notifier" do
314
+ it "returns a notice notifier" do
315
+ expect(described_class.notice_notifier).to be_an(Airbrake::NoticeNotifier)
316
+ end
317
+ end
318
+
319
+ describe ".deploy_notifier" do
320
+ it "returns a deploy notifier" do
321
+ expect(described_class.deploy_notifier).to be_an(Airbrake::DeployNotifier)
322
+ end
323
+ end
324
+ end