exception_handling 3.0.pre.1 → 3.0.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.
- checksums.yaml +5 -5
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/pipeline.yml +36 -0
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -1
- data/.tool-versions +1 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +150 -0
- data/Gemfile +10 -16
- data/Gemfile.lock +65 -128
- data/README.md +51 -19
- data/Rakefile +8 -11
- data/exception_handling.gemspec +11 -13
- data/gemfiles/rails_5.gemfile +16 -0
- data/gemfiles/rails_6.gemfile +16 -0
- data/gemfiles/rails_7.gemfile +16 -0
- data/lib/exception_handling/escalate_callback.rb +19 -0
- data/lib/exception_handling/exception_info.rb +15 -11
- data/lib/exception_handling/log_stub_error.rb +2 -1
- data/lib/exception_handling/logging_methods.rb +21 -0
- data/lib/exception_handling/testing.rb +9 -12
- data/lib/exception_handling/version.rb +1 -1
- data/lib/exception_handling.rb +83 -173
- data/{test → spec}/helpers/exception_helpers.rb +2 -2
- data/spec/rake_test_warning_false.rb +20 -0
- data/{test/test_helper.rb → spec/spec_helper.rb} +63 -66
- data/spec/unit/exception_handling/escalate_callback_spec.rb +81 -0
- data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
- data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
- data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +170 -114
- data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
- data/spec/unit/exception_handling/logging_methods_spec.rb +38 -0
- data/spec/unit/exception_handling_spec.rb +1063 -0
- metadata +62 -91
- data/lib/exception_handling/honeybadger_callbacks.rb +0 -59
- data/lib/exception_handling/mailer.rb +0 -70
- data/lib/exception_handling/methods.rb +0 -101
- data/lib/exception_handling/sensu.rb +0 -28
- data/semaphore_ci/setup.sh +0 -3
- data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
- data/test/unit/exception_handling/exception_description_test.rb +0 -82
- data/test/unit/exception_handling/honeybadger_callbacks_test.rb +0 -122
- data/test/unit/exception_handling/mailer_test.rb +0 -98
- data/test/unit/exception_handling/methods_test.rb +0 -84
- data/test/unit/exception_handling/sensu_test.rb +0 -52
- data/test/unit/exception_handling_test.rb +0 -1109
- data/views/exception_handling/mailer/escalate_custom.html.erb +0 -17
- data/views/exception_handling/mailer/escalation_notification.html.erb +0 -17
- data/views/exception_handling/mailer/log_parser_exception_notification.html.erb +0 -82
- /data/{test → spec}/helpers/controller_helpers.rb +0 -0
@@ -0,0 +1,1063 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../spec_helper', __dir__)
|
4
|
+
require_test_helper 'controller_helpers'
|
5
|
+
require_test_helper 'exception_helpers'
|
6
|
+
|
7
|
+
describe ExceptionHandling do
|
8
|
+
include ControllerHelpers
|
9
|
+
include ExceptionHelpers
|
10
|
+
|
11
|
+
before do
|
12
|
+
@fail_count = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def dont_stub_log_error
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def append_organization_info_config(data)
|
20
|
+
data[:user_details] = {}
|
21
|
+
data[:user_details][:username] = "CaryP"
|
22
|
+
data[:user_details][:organization] = "Invoca Engineering Dept."
|
23
|
+
rescue StandardError
|
24
|
+
# don't let these out!
|
25
|
+
end
|
26
|
+
|
27
|
+
def custom_data_callback_returns_nil_message_exception(_data)
|
28
|
+
raise_exception_with_nil_message
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
|
32
|
+
@fail_count += 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
|
36
|
+
@callback_data = data
|
37
|
+
@treat_like_warning = treat_like_warning
|
38
|
+
@fail_count += 1
|
39
|
+
@honeybadger_status = honeybadger_status
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_error_callback_with_failure(_data, _ex)
|
43
|
+
raise "this should be rescued"
|
44
|
+
end
|
45
|
+
|
46
|
+
def log_error_callback_returns_nil_message_exception(_data, _ex)
|
47
|
+
raise_exception_with_nil_message
|
48
|
+
end
|
49
|
+
|
50
|
+
module EventMachineStub
|
51
|
+
class << self
|
52
|
+
attr_accessor :block
|
53
|
+
|
54
|
+
def schedule(&block)
|
55
|
+
@block = block
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class DNSResolvStub
|
61
|
+
class << self
|
62
|
+
attr_accessor :callback_block
|
63
|
+
attr_accessor :errback_block
|
64
|
+
|
65
|
+
def resolve(_hostname)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def callback(&block)
|
70
|
+
@callback_block = block
|
71
|
+
end
|
72
|
+
|
73
|
+
def errback(&block)
|
74
|
+
@errback_block = block
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class SmtpClientStub
|
80
|
+
class << self
|
81
|
+
attr_reader :block
|
82
|
+
attr_reader :last_method
|
83
|
+
|
84
|
+
def errback(&block)
|
85
|
+
@block = block
|
86
|
+
end
|
87
|
+
|
88
|
+
def send_hash
|
89
|
+
@send_hash ||= {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def send(hash)
|
93
|
+
@last_method = :send
|
94
|
+
send_hash.clear
|
95
|
+
send_hash.merge!(hash)
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def asend(hash)
|
100
|
+
send(hash)
|
101
|
+
@last_method = :asend
|
102
|
+
self
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class SmtpClientErrbackStub < SmtpClientStub
|
108
|
+
end
|
109
|
+
|
110
|
+
before(:each) do
|
111
|
+
# Reset this for every test since they are applied to the class
|
112
|
+
ExceptionHandling.honeybadger_auto_tagger = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
context "with warn and honeybadger notify stubbed" do
|
116
|
+
before do
|
117
|
+
allow(ExceptionHandling).to receive(:warn).with(any_args)
|
118
|
+
allow(Honeybadger).to receive(:notify).with(any_args)
|
119
|
+
end
|
120
|
+
|
121
|
+
context "with logger stashed" do
|
122
|
+
before { @original_logger = ExceptionHandling.logger }
|
123
|
+
after { ExceptionHandling.logger = @original_logger }
|
124
|
+
|
125
|
+
it "stores logger as-is if it has ContextualLogger::Mixin" do
|
126
|
+
logger = Logger.new('/dev/null')
|
127
|
+
logger.extend(ContextualLogger::LoggerMixin)
|
128
|
+
ancestors = logger.singleton_class.ancestors.*.name
|
129
|
+
|
130
|
+
ExceptionHandling.logger = logger
|
131
|
+
expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to eq(ancestors)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "allows logger = nil" do
|
135
|
+
ExceptionHandling.logger = nil
|
136
|
+
expect { ExceptionHandling.logger }.to raise_error(ArgumentError, "You must assign a value to ExceptionHandling.logger")
|
137
|
+
end
|
138
|
+
|
139
|
+
context "#log_error" do
|
140
|
+
it "takes in additional logging context hash and pass it to the logger" do
|
141
|
+
ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
|
142
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Error/)
|
143
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
144
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
145
|
+
end
|
146
|
+
|
147
|
+
it "passes :honeybadger_tags in log context to honeybadger" do
|
148
|
+
expect(Honeybadger).to receive(:notify).with(hash_including({ tags: " , awesome , totallytubular, " }))
|
149
|
+
ExceptionHandling.log_error('This is an Error', 'This is the prefix context', honeybadger_tags: ' , awesome , totallytubular, ')
|
150
|
+
|
151
|
+
expect(Honeybadger).to receive(:notify).with(hash_including({ tags: " cool neat" }))
|
152
|
+
ExceptionHandling.log_error('This is an Error', 'This is the prefix context', honeybadger_tags: [" ", " cool ", "neat"])
|
153
|
+
end
|
154
|
+
|
155
|
+
it "logs with Severity::FATAL" do
|
156
|
+
ExceptionHandling.log_error('This is a Warning', service_name: 'exception_handling')
|
157
|
+
expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context "#log_warning" do
|
163
|
+
it "have empty array as a backtrace" do
|
164
|
+
expect(ExceptionHandling).to receive(:log_error) do |error|
|
165
|
+
expect(error.class).to eq(ExceptionHandling::Warning)
|
166
|
+
expect(error.backtrace).to eq([])
|
167
|
+
end
|
168
|
+
ExceptionHandling.log_warning('Now with empty array as a backtrace!')
|
169
|
+
end
|
170
|
+
|
171
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
172
|
+
ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
|
173
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Warning/)
|
174
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
175
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
176
|
+
end
|
177
|
+
|
178
|
+
it "log with Severity::WARN" do
|
179
|
+
ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
|
180
|
+
expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "#log_info" do
|
185
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
186
|
+
ExceptionHandling.log_info('This is an Info', service_name: 'exception_handling')
|
187
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Info/)
|
188
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
189
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
190
|
+
end
|
191
|
+
|
192
|
+
it "log with Severity::INFO" do
|
193
|
+
ExceptionHandling.log_info('This is a Warning', service_name: 'exception_handling')
|
194
|
+
expect('INFO').to eq(logged_excluding_reload_filter.last[:severity])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "#log_debug" do
|
199
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
200
|
+
ExceptionHandling.log_debug('This is a Debug', service_name: 'exception_handling')
|
201
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Debug/)
|
202
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
203
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
204
|
+
end
|
205
|
+
|
206
|
+
it "log with Severity::DEBUG" do
|
207
|
+
ExceptionHandling.log_debug('This is a Warning', service_name: 'exception_handling')
|
208
|
+
expect('DEBUG').to eq(logged_excluding_reload_filter.last[:severity])
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context "#write_exception_to_log" do
|
213
|
+
it "log warnings with Severity::WARN" do
|
214
|
+
warning = ExceptionHandling::Warning.new('This is a Warning')
|
215
|
+
ExceptionHandling.write_exception_to_log(warning, '', Time.now.to_i, service_name: 'exception_handling')
|
216
|
+
expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
|
217
|
+
end
|
218
|
+
|
219
|
+
it "log everything else with Severity::FATAL" do
|
220
|
+
error = RuntimeError.new('This is a runtime error')
|
221
|
+
ExceptionHandling.write_exception_to_log(error, '', Time.now.to_i, service_name: 'exception_handling')
|
222
|
+
expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context "configuration with custom_data_hook or post_log_error_hook" do
|
227
|
+
after do
|
228
|
+
ExceptionHandling.custom_data_hook = nil
|
229
|
+
ExceptionHandling.post_log_error_hook = nil
|
230
|
+
end
|
231
|
+
|
232
|
+
it "support a custom_data_hook" do
|
233
|
+
capture_notifications
|
234
|
+
|
235
|
+
ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
|
236
|
+
ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
|
237
|
+
|
238
|
+
expect(sent_notifications.last.enhanced_data['user_details'].to_s).to match(/Invoca Engineering Dept./)
|
239
|
+
end
|
240
|
+
|
241
|
+
it "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
|
242
|
+
@honeybadger_status = nil
|
243
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
244
|
+
|
245
|
+
notify_args = []
|
246
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
|
247
|
+
ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
|
248
|
+
expect(@fail_count).to eq(1)
|
249
|
+
expect(@treat_like_warning).to eq(false)
|
250
|
+
expect(@honeybadger_status).to eq(:success)
|
251
|
+
|
252
|
+
expect(@callback_data["notes"]).to eq("this is used by a test")
|
253
|
+
expect(notify_args.size).to eq(1), notify_args.inspect
|
254
|
+
expect(notify_args.last[:context].to_s).to match(/this is used by a test/)
|
255
|
+
end
|
256
|
+
|
257
|
+
it "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
|
258
|
+
@honeybadger_status = nil
|
259
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
260
|
+
ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
|
261
|
+
expect(@fail_count).to eq(1)
|
262
|
+
expect(@treat_like_warning).to eq(true)
|
263
|
+
expect(@honeybadger_status).to eq(:skipped)
|
264
|
+
end
|
265
|
+
|
266
|
+
it "include logging context in the exception data" do
|
267
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
268
|
+
ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
|
269
|
+
|
270
|
+
expected_log_context = {
|
271
|
+
"log_context_test" => "contextual_logging"
|
272
|
+
}
|
273
|
+
expect(@callback_data[:log_context]).to eq(expected_log_context)
|
274
|
+
end
|
275
|
+
|
276
|
+
it "support rescue exceptions from a log_error hook" do
|
277
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
|
278
|
+
log_info_messages = []
|
279
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
280
|
+
log_info_messages << message
|
281
|
+
end
|
282
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
|
283
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
|
284
|
+
end
|
285
|
+
|
286
|
+
it "handle nil message exceptions resulting from the log_error hook" do
|
287
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
|
288
|
+
log_info_messages = []
|
289
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
290
|
+
log_info_messages << message
|
291
|
+
end
|
292
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
|
293
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
|
294
|
+
end
|
295
|
+
|
296
|
+
it "handle nil message exceptions resulting from the custom data hook" do
|
297
|
+
ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
|
298
|
+
log_info_messages = []
|
299
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
300
|
+
log_info_messages << message
|
301
|
+
end
|
302
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.not_to raise_error
|
303
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }).to be_truthy
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context "Exception Handling" do
|
308
|
+
context "default_metric_name" do
|
309
|
+
context "when metric_name is present in exception_data" do
|
310
|
+
it "include metric_name in resulting metric name" do
|
311
|
+
exception = StandardError.new('this is an exception')
|
312
|
+
metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
|
313
|
+
expect(metric).to eq('special_metric')
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
context "when metric_name is not present in exception_data" do
|
318
|
+
it "return exception_handling.warning when using log warning" do
|
319
|
+
warning = ExceptionHandling::Warning.new('this is a warning')
|
320
|
+
metric = ExceptionHandling.default_metric_name({}, warning, false)
|
321
|
+
expect(metric).to eq('warning')
|
322
|
+
end
|
323
|
+
|
324
|
+
it "return exception_handling.exception when using log error" do
|
325
|
+
exception = StandardError.new('this is an exception')
|
326
|
+
metric = ExceptionHandling.default_metric_name({}, exception, false)
|
327
|
+
expect(metric).to eq('exception')
|
328
|
+
end
|
329
|
+
|
330
|
+
context "when using log error with treat_like_warning" do
|
331
|
+
it "return exception_handling.unforwarded_exception when exception not present" do
|
332
|
+
metric = ExceptionHandling.default_metric_name({}, nil, true)
|
333
|
+
expect(metric).to eq('unforwarded_exception')
|
334
|
+
end
|
335
|
+
|
336
|
+
it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
|
337
|
+
module SomeModule
|
338
|
+
class SomeException < StandardError
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
exception = SomeModule::SomeException.new('this is an exception')
|
343
|
+
metric = ExceptionHandling.default_metric_name({}, exception, true)
|
344
|
+
expect(metric).to eq('unforwarded_exception_SomeException')
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context "ExceptionHandling.ensure_safe" do
|
351
|
+
it "log an exception with call stack if an exception is raised." do
|
352
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
353
|
+
ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
|
354
|
+
end
|
355
|
+
|
356
|
+
it "should not log an exception if an exception is not raised." do
|
357
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
358
|
+
ExceptionHandling.ensure_safe { ; }
|
359
|
+
end
|
360
|
+
|
361
|
+
it "return its value if used during an assignment" do
|
362
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
363
|
+
b = ExceptionHandling.ensure_safe { 5 }
|
364
|
+
expect(b).to eq(5)
|
365
|
+
end
|
366
|
+
|
367
|
+
it "return nil if an exception is raised during an assignment" do
|
368
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
369
|
+
b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
|
370
|
+
expect(b).to be_nil
|
371
|
+
end
|
372
|
+
|
373
|
+
it "allow a message to be appended to the error when logged." do
|
374
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
375
|
+
b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
|
376
|
+
expect(b).to be_nil
|
377
|
+
end
|
378
|
+
|
379
|
+
it "only rescue StandardError and descendents" do
|
380
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise Exception } }.to raise_exception(Exception)
|
381
|
+
|
382
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nStandardError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
383
|
+
|
384
|
+
b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
|
385
|
+
expect(b).to be_nil
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
context "ExceptionHandling.ensure_completely_safe" do
|
390
|
+
it "log an exception if an exception is raised." do
|
391
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
392
|
+
ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
|
393
|
+
end
|
394
|
+
|
395
|
+
it "should not log an exception if an exception is not raised." do
|
396
|
+
expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
|
397
|
+
ExceptionHandling.ensure_completely_safe { ; }
|
398
|
+
end
|
399
|
+
|
400
|
+
it "return its value if used during an assignment" do
|
401
|
+
expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
|
402
|
+
b = ExceptionHandling.ensure_completely_safe { 5 }
|
403
|
+
expect(b).to eq(5)
|
404
|
+
end
|
405
|
+
|
406
|
+
it "return nil if an exception is raised during an assignment" do
|
407
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args) { nil }
|
408
|
+
b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
|
409
|
+
expect(b).to be_nil
|
410
|
+
end
|
411
|
+
|
412
|
+
it "allow a message to be appended to the error when logged." do
|
413
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
414
|
+
b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
|
415
|
+
expect(b).to be_nil
|
416
|
+
end
|
417
|
+
|
418
|
+
it "rescue any instance or child of Exception" do
|
419
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
420
|
+
ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
|
421
|
+
end
|
422
|
+
|
423
|
+
it "not rescue the special exceptions that Ruby uses" do
|
424
|
+
[SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
|
425
|
+
expect do
|
426
|
+
ExceptionHandling.ensure_completely_safe do
|
427
|
+
raise exception
|
428
|
+
end
|
429
|
+
end.to raise_exception(exception)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
context "exception timestamp" do
|
435
|
+
before do
|
436
|
+
Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
|
437
|
+
end
|
438
|
+
|
439
|
+
it "include the timestamp when the exception is logged" do
|
440
|
+
capture_notifications
|
441
|
+
|
442
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:517033020\) context\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
|
443
|
+
b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
|
444
|
+
expect(b).to be_nil
|
445
|
+
|
446
|
+
expect(ExceptionHandling.last_exception_timestamp).to eq(517_033_020)
|
447
|
+
|
448
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
449
|
+
|
450
|
+
expect(sent_notifications.last.enhanced_data['timestamp']).to eq(517_033_020)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
it "log the error if the exception message is nil" do
|
455
|
+
capture_notifications
|
456
|
+
|
457
|
+
ExceptionHandling.log_error(exception_with_nil_message)
|
458
|
+
|
459
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
460
|
+
expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
|
461
|
+
end
|
462
|
+
|
463
|
+
it "log the error if the exception message is nil and the exception context is a hash" do
|
464
|
+
capture_notifications
|
465
|
+
|
466
|
+
ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
|
467
|
+
|
468
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
469
|
+
expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
|
470
|
+
end
|
471
|
+
|
472
|
+
context "Honeybadger integration" do
|
473
|
+
context "with Honeybadger not defined" do
|
474
|
+
before do
|
475
|
+
allow(ExceptionHandling).to receive(:honeybadger_defined?) { false }
|
476
|
+
end
|
477
|
+
|
478
|
+
it "not invoke send_exception_to_honeybadger when log_error is executed" do
|
479
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
480
|
+
ExceptionHandling.log_error(exception_1)
|
481
|
+
end
|
482
|
+
|
483
|
+
it "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
|
484
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
485
|
+
ExceptionHandling.ensure_safe { raise exception_1 }
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
context "with Honeybadger defined" do
|
490
|
+
it "not send_exception_to_honeybadger when log_warning is executed" do
|
491
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
492
|
+
ExceptionHandling.log_warning("This should not go to honeybadger")
|
493
|
+
end
|
494
|
+
|
495
|
+
it "not send_exception_to_honeybadger when log_error is called with a Warning" do
|
496
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
497
|
+
ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
|
498
|
+
end
|
499
|
+
|
500
|
+
it "invoke send_exception_to_honeybadger when log_error is executed" do
|
501
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
502
|
+
ExceptionHandling.log_error(exception_1)
|
503
|
+
end
|
504
|
+
|
505
|
+
it "invoke send_exception_to_honeybadger when log_error_rack is executed" do
|
506
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
507
|
+
ExceptionHandling.log_error_rack(exception_1, {}, nil)
|
508
|
+
end
|
509
|
+
|
510
|
+
it "invoke send_exception_to_honeybadger when ensure_safe is executed" do
|
511
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
512
|
+
ExceptionHandling.ensure_safe { raise exception_1 }
|
513
|
+
end
|
514
|
+
|
515
|
+
it "specify error message as an empty string when notifying honeybadger if exception message is nil" do
|
516
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |args|
|
517
|
+
expect(args[:error_message]).to eq("")
|
518
|
+
end
|
519
|
+
ExceptionHandling.log_error(exception_with_nil_message)
|
520
|
+
end
|
521
|
+
|
522
|
+
context "with stubbed values" do
|
523
|
+
before do
|
524
|
+
Time.now_override = Time.now
|
525
|
+
@env = { server: "fe98" }
|
526
|
+
@parameters = { advertiser_id: 435, controller: "some_controller" }
|
527
|
+
@session = { username: "jsmith" }
|
528
|
+
@request_uri = "host/path"
|
529
|
+
@controller = create_dummy_controller(@env, @parameters, @session, @request_uri)
|
530
|
+
allow(ExceptionHandling).to receive(:server_name) { "invoca_fe98" }
|
531
|
+
|
532
|
+
@exception = StandardError.new("Some Exception")
|
533
|
+
@exception.set_backtrace([
|
534
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
535
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
536
|
+
])
|
537
|
+
@exception_context = { "SERVER_NAME" => "exceptional.com" }
|
538
|
+
end
|
539
|
+
|
540
|
+
it "send error details and relevant context data to Honeybadger with log_context" do
|
541
|
+
honeybadger_data = nil
|
542
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |data|
|
543
|
+
honeybadger_data = data
|
544
|
+
end
|
545
|
+
ExceptionHandling.logger.global_context = { service_name: "rails", region: "AWS-us-east-1", honeybadger_tags: ['Data-Services', 'web'] }
|
546
|
+
log_context = { log_source: "gem/listen", service_name: "bin/console" }
|
547
|
+
ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
|
548
|
+
data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
|
549
|
+
data[:user_details] = { username: "jsmith" }
|
550
|
+
data[:event_response] = "Event successfully received"
|
551
|
+
data[:other_section] = "This should not be included in the response"
|
552
|
+
end
|
553
|
+
|
554
|
+
expected_data = {
|
555
|
+
error_class: :"Test Exception",
|
556
|
+
error_message: "Some Exception",
|
557
|
+
controller: "some_controller",
|
558
|
+
exception: @exception,
|
559
|
+
tags: "Data-Services web",
|
560
|
+
context: {
|
561
|
+
timestamp: Time.now.to_i,
|
562
|
+
error_class: "StandardError",
|
563
|
+
server: "invoca_fe98",
|
564
|
+
exception_context: { "SERVER_NAME" => "exceptional.com" },
|
565
|
+
scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
|
566
|
+
notes: "this is used by a test",
|
567
|
+
user_details: { "username" => "jsmith" },
|
568
|
+
request: {
|
569
|
+
"params" => { "advertiser_id" => 435, "controller" => "some_controller" },
|
570
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
571
|
+
"url" => "host/path"
|
572
|
+
},
|
573
|
+
session: {
|
574
|
+
"key" => nil,
|
575
|
+
"data" => { "username" => "jsmith" }
|
576
|
+
},
|
577
|
+
environment: {
|
578
|
+
"SERVER_NAME" => "exceptional.com"
|
579
|
+
},
|
580
|
+
backtrace: [
|
581
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
582
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
583
|
+
],
|
584
|
+
event_response: "Event successfully received",
|
585
|
+
log_context: { "service_name" => "bin/console", "region" => "AWS-us-east-1", "log_source" => "gem/listen", "honeybadger_tags" => ['Data-Services', 'web'] }
|
586
|
+
}
|
587
|
+
}
|
588
|
+
expect(honeybadger_data).to eq(expected_data)
|
589
|
+
end
|
590
|
+
|
591
|
+
it "send error details and relevant context data to Honeybadger with empty log_context" do
|
592
|
+
honeybadger_data = nil
|
593
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |data|
|
594
|
+
honeybadger_data = data
|
595
|
+
end
|
596
|
+
ExceptionHandling.logger.global_context = {}
|
597
|
+
log_context = {}
|
598
|
+
ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
|
599
|
+
data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
|
600
|
+
data[:user_details] = { username: "jsmith" }
|
601
|
+
data[:event_response] = "Event successfully received"
|
602
|
+
data[:other_section] = "This should not be included in the response"
|
603
|
+
end
|
604
|
+
|
605
|
+
expected_data = {
|
606
|
+
error_class: :"Test Exception",
|
607
|
+
error_message: "Some Exception",
|
608
|
+
controller: "some_controller",
|
609
|
+
exception: @exception,
|
610
|
+
tags: "",
|
611
|
+
context: {
|
612
|
+
timestamp: Time.now.to_i,
|
613
|
+
error_class: "StandardError",
|
614
|
+
server: "invoca_fe98",
|
615
|
+
exception_context: { "SERVER_NAME" => "exceptional.com" },
|
616
|
+
scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
|
617
|
+
notes: "this is used by a test",
|
618
|
+
user_details: { "username" => "jsmith" },
|
619
|
+
request: {
|
620
|
+
"params" => { "advertiser_id" => 435, "controller" => "some_controller" },
|
621
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
622
|
+
"url" => "host/path"
|
623
|
+
},
|
624
|
+
session: {
|
625
|
+
"key" => nil,
|
626
|
+
"data" => { "username" => "jsmith" }
|
627
|
+
},
|
628
|
+
environment: {
|
629
|
+
"SERVER_NAME" => "exceptional.com"
|
630
|
+
},
|
631
|
+
backtrace: [
|
632
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
633
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
634
|
+
],
|
635
|
+
event_response: "Event successfully received"
|
636
|
+
}
|
637
|
+
}
|
638
|
+
expect(honeybadger_data).to eq(expected_data)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
context "with post_log_error_hook set" do
|
643
|
+
after do
|
644
|
+
ExceptionHandling.post_log_error_hook = nil
|
645
|
+
end
|
646
|
+
|
647
|
+
it "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
|
648
|
+
@honeybadger_status = nil
|
649
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
650
|
+
filter_list = {
|
651
|
+
NoHoneybadger: {
|
652
|
+
error: "suppress Honeybadger notification",
|
653
|
+
send_to_honeybadger: false
|
654
|
+
}
|
655
|
+
}
|
656
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
657
|
+
expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
|
658
|
+
|
659
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger_unless_filtered).with(any_args).exactly(1).and_call_original
|
660
|
+
expect(Honeybadger).to_not receive(:notify)
|
661
|
+
ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
|
662
|
+
expect(@honeybadger_status).to eq(:skipped)
|
663
|
+
end
|
664
|
+
|
665
|
+
it "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
|
666
|
+
@honeybadger_status = nil
|
667
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
668
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { raise "Honeybadger Notification Failure" }
|
669
|
+
ExceptionHandling.log_error(exception_1)
|
670
|
+
expect(@honeybadger_status).to eq(:failure)
|
671
|
+
end
|
672
|
+
|
673
|
+
it "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
|
674
|
+
@honeybadger_status = nil
|
675
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
676
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { false }
|
677
|
+
ExceptionHandling.log_error(exception_1)
|
678
|
+
expect(@honeybadger_status).to eq(:failure)
|
679
|
+
end
|
680
|
+
|
681
|
+
it "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
|
682
|
+
@honeybadger_status = nil
|
683
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
684
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { '06220c5a-b471-41e5-baeb-de247da45a56' }
|
685
|
+
ExceptionHandling.log_error(exception_1)
|
686
|
+
expect(@honeybadger_status).to eq(:success)
|
687
|
+
end
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
class EventResponse
|
693
|
+
def to_s
|
694
|
+
"message from to_s!"
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
it "allow sections to have data with just a to_s method" do
|
699
|
+
capture_notifications
|
700
|
+
|
701
|
+
ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
|
702
|
+
data.merge!(event_response: EventResponse.new)
|
703
|
+
end
|
704
|
+
|
705
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
706
|
+
expect(sent_notifications.last.enhanced_data['event_response'].to_s).to match(/message from to_s!/)
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
it "return the error ID (timestamp)" do
|
711
|
+
result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
|
712
|
+
expect(result).to eq(ExceptionHandling.last_exception_timestamp)
|
713
|
+
end
|
714
|
+
|
715
|
+
it "rescue exceptions that happen in log_error" do
|
716
|
+
allow(ExceptionHandling).to receive(:make_exception) { raise ArgumentError, "Bad argument" }
|
717
|
+
expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
|
718
|
+
satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
|
719
|
+
any_args)
|
720
|
+
ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
|
721
|
+
end
|
722
|
+
|
723
|
+
it "rescue exceptions that happen when log_error yields" do
|
724
|
+
expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
|
725
|
+
satisfy { |context| context['Context message'] },
|
726
|
+
anything,
|
727
|
+
any_args)
|
728
|
+
ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
|
729
|
+
end
|
730
|
+
|
731
|
+
context "Exception Filtering" do
|
732
|
+
before do
|
733
|
+
filter_list = { exception1: { 'error' => "my error message" },
|
734
|
+
exception2: { 'error' => "some other message", :session => "misc data" } }
|
735
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
736
|
+
|
737
|
+
# bump modified time up to get the above filter loaded
|
738
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
739
|
+
end
|
740
|
+
|
741
|
+
it "handle case where filter list is not found" do
|
742
|
+
allow(YAML).to receive(:load_file) { raise Errno::ENOENT, "File not found" }
|
743
|
+
|
744
|
+
capture_notifications
|
745
|
+
|
746
|
+
ExceptionHandling.log_error("My error message is in list")
|
747
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
748
|
+
end
|
749
|
+
|
750
|
+
it "log exception and suppress email when exception is on filter list" do
|
751
|
+
capture_notifications
|
752
|
+
|
753
|
+
ExceptionHandling.log_error("Error message is not in list")
|
754
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
755
|
+
|
756
|
+
sent_notifications.clear
|
757
|
+
ExceptionHandling.log_error("My error message is in list")
|
758
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
759
|
+
end
|
760
|
+
|
761
|
+
it "allow filtering exception on any text in exception data" do
|
762
|
+
filters = { exception1: { session: "data: my extra session data" } }
|
763
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filters) }
|
764
|
+
|
765
|
+
capture_notifications
|
766
|
+
|
767
|
+
ExceptionHandling.log_error("No match here") do |data|
|
768
|
+
data[:session] = {
|
769
|
+
key: "@session_id",
|
770
|
+
data: "my extra session data"
|
771
|
+
}
|
772
|
+
end
|
773
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
774
|
+
|
775
|
+
ExceptionHandling.log_error("No match here") do |data|
|
776
|
+
data[:session] = {
|
777
|
+
key: "@session_id",
|
778
|
+
data: "my extra session <no match!> data"
|
779
|
+
}
|
780
|
+
end
|
781
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
782
|
+
end
|
783
|
+
|
784
|
+
it "reload filter list on the next exception if file was modified" do
|
785
|
+
capture_notifications
|
786
|
+
|
787
|
+
ExceptionHandling.log_error("Error message is not in list")
|
788
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
789
|
+
|
790
|
+
filter_list = { exception1: { 'error' => "Error message is not in list" } }
|
791
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
792
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
793
|
+
|
794
|
+
sent_notifications.clear
|
795
|
+
ExceptionHandling.log_error("Error message is not in list")
|
796
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
797
|
+
end
|
798
|
+
|
799
|
+
it "not consider filter if both error message and body do not match" do
|
800
|
+
capture_notifications
|
801
|
+
|
802
|
+
# error message matches, but not full text
|
803
|
+
ExceptionHandling.log_error("some other message")
|
804
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
805
|
+
|
806
|
+
# now both match
|
807
|
+
sent_notifications.clear
|
808
|
+
ExceptionHandling.log_error("some other message") do |data|
|
809
|
+
data[:session] = { some_random_key: "misc data" }
|
810
|
+
end
|
811
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
812
|
+
end
|
813
|
+
|
814
|
+
it "skip environment keys not on whitelist" do
|
815
|
+
capture_notifications
|
816
|
+
|
817
|
+
ExceptionHandling.log_error("some message") do |data|
|
818
|
+
data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
|
819
|
+
end
|
820
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
821
|
+
|
822
|
+
mail = sent_notifications.last
|
823
|
+
environment = mail.enhanced_data['environment']
|
824
|
+
|
825
|
+
expect(environment["RAILS_SECRETS_YML_CONTENTS"]).to be_nil, environment.inspect # this is not on whitelist).to be_nil
|
826
|
+
expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this is
|
827
|
+
end
|
828
|
+
|
829
|
+
it "omit environment defaults" do
|
830
|
+
capture_notifications
|
831
|
+
|
832
|
+
allow(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args) { |exception_info| sent_notifications << exception_info }
|
833
|
+
|
834
|
+
ExceptionHandling.log_error("some message") do |data|
|
835
|
+
data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
|
836
|
+
end
|
837
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
838
|
+
mail = sent_notifications.last
|
839
|
+
environment = mail.enhanced_data['environment']
|
840
|
+
|
841
|
+
expect(environment["SERVER_PORT"]).to be_nil, environment.inspect # this was default).to be_nil
|
842
|
+
expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this was not
|
843
|
+
end
|
844
|
+
|
845
|
+
it "reject the filter file if any contain all empty regexes" do
|
846
|
+
filter_list = { exception1: { 'error' => "", :session => "" },
|
847
|
+
exception2: { 'error' => "is not in list", :session => "" } }
|
848
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
849
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
850
|
+
|
851
|
+
capture_notifications
|
852
|
+
|
853
|
+
ExceptionHandling.log_error("Error message is not in list")
|
854
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
855
|
+
end
|
856
|
+
|
857
|
+
it "reload filter file if filename changes" do
|
858
|
+
catalog = ExceptionHandling.exception_catalog
|
859
|
+
ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
|
860
|
+
expect(ExceptionHandling.exception_catalog).to_not eq(catalog)
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
context "Exception mapping" do
|
865
|
+
before do
|
866
|
+
@data = {
|
867
|
+
environment: {
|
868
|
+
'HTTP_HOST' => "localhost",
|
869
|
+
'HTTP_REFERER' => "http://localhost/action/controller/instance",
|
870
|
+
},
|
871
|
+
session: {
|
872
|
+
data: {
|
873
|
+
affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
|
874
|
+
edit_mode: true,
|
875
|
+
advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
|
876
|
+
username_id: defined?(Username) ? Username.first.id : '1',
|
877
|
+
user_id: defined?(User) ? User.first.id : '1',
|
878
|
+
flash: {},
|
879
|
+
impersonated_organization_pk: 'Advertiser_1'
|
880
|
+
}
|
881
|
+
},
|
882
|
+
request: {},
|
883
|
+
backtrace: ["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
|
884
|
+
api_key: "none",
|
885
|
+
error_class: "StandardError",
|
886
|
+
error: 'Some error message'
|
887
|
+
}
|
888
|
+
end
|
889
|
+
|
890
|
+
it "clean backtraces" do
|
891
|
+
begin
|
892
|
+
raise "test exception"
|
893
|
+
rescue => ex
|
894
|
+
backtrace = ex.backtrace
|
895
|
+
end
|
896
|
+
result = ExceptionHandling.send(:clean_backtrace, ex).to_s
|
897
|
+
expect(backtrace).to_not eq(result)
|
898
|
+
end
|
899
|
+
|
900
|
+
it "return entire backtrace if cleaned is emtpy" do
|
901
|
+
begin
|
902
|
+
backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
|
903
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
|
904
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
|
905
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
|
906
|
+
"/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
|
907
|
+
"/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
|
908
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
|
909
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
|
910
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
|
911
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
|
912
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
|
913
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
|
914
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
|
915
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
|
916
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
|
917
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
|
918
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
|
919
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
|
920
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
|
921
|
+
"(eval):12:in `run_suite'",
|
922
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
|
923
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
|
924
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
|
925
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
|
926
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
|
927
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
|
928
|
+
"-e:1"]
|
929
|
+
|
930
|
+
module ::Rails
|
931
|
+
class BacktraceCleaner
|
932
|
+
def clean(_backtrace)
|
933
|
+
[]
|
934
|
+
end
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
rails = double(Rails)
|
939
|
+
expect(rails).to receive(:backtrace_cleaner) { Rails::BacktraceCleaner.new }
|
940
|
+
rails.backtrace_cleaner
|
941
|
+
|
942
|
+
ex = Exception.new
|
943
|
+
ex.set_backtrace(backtrace)
|
944
|
+
result = ExceptionHandling.send(:clean_backtrace, ex)
|
945
|
+
expect(result).to eq(backtrace)
|
946
|
+
ensure
|
947
|
+
Object.send(:remove_const, :Rails)
|
948
|
+
end
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
context "log_perodically" do
|
953
|
+
before do
|
954
|
+
Time.now_override = Time.now # Freeze time
|
955
|
+
ExceptionHandling.logger.clear
|
956
|
+
end
|
957
|
+
|
958
|
+
after do
|
959
|
+
Time.now_override = nil
|
960
|
+
end
|
961
|
+
|
962
|
+
it "take in additional logging context and pass them to the logger" do
|
963
|
+
ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
|
964
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
965
|
+
expect(logged_excluding_reload_filter.last[:context]).to eq({ service_name: 'exception_handling' })
|
966
|
+
end
|
967
|
+
|
968
|
+
it "log immediately when we are expected to log" do
|
969
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
970
|
+
expect(logged_excluding_reload_filter.size).to eq(1)
|
971
|
+
|
972
|
+
Time.now_override = Time.now + 5.minutes
|
973
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
|
974
|
+
expect(logged_excluding_reload_filter.size).to eq(1)
|
975
|
+
|
976
|
+
ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
|
977
|
+
expect(logged_excluding_reload_filter.size).to eq(2)
|
978
|
+
|
979
|
+
Time.now_override = Time.now + 26.minutes
|
980
|
+
|
981
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
982
|
+
expect(logged_excluding_reload_filter.size).to eq(3)
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
context "#honeybadger_auto_tagger=" do
|
987
|
+
context "with proc that runs successfully" do
|
988
|
+
before do
|
989
|
+
ExceptionHandling.honeybadger_auto_tagger =
|
990
|
+
->(exception) do
|
991
|
+
if exception.message =~ /donuts/
|
992
|
+
['donut-error', 'high-urgency']
|
993
|
+
else
|
994
|
+
['low-urgency']
|
995
|
+
end
|
996
|
+
end
|
997
|
+
end
|
998
|
+
|
999
|
+
context "without manually passed tags" do
|
1000
|
+
let(:exception) { StandardError.new("We are out of chocolate milk") }
|
1001
|
+
|
1002
|
+
it "adds tags from autotagger" do
|
1003
|
+
expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "low-urgency" }))
|
1004
|
+
ExceptionHandling.log_error(exception, nil)
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
context "with manually passed tags" do
|
1009
|
+
let(:exception) { StandardError.new("The donuts are burning") }
|
1010
|
+
|
1011
|
+
it "merges tags from autotagger with manually passed tags" do
|
1012
|
+
expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "donut-error high-urgency upset-customers" }))
|
1013
|
+
ExceptionHandling.log_error(exception, nil, honeybadger_tags: ["upset-customers"])
|
1014
|
+
end
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
context "with proc that raises an exception" do
|
1020
|
+
let(:exception) { StandardError.new("Something else occurred") }
|
1021
|
+
|
1022
|
+
before do
|
1023
|
+
ExceptionHandling.honeybadger_auto_tagger = ->(_exception) { raise StandardError, "boom" }
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
it "logs a message and returns [] for the tags" do
|
1027
|
+
expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "some-other-tag" }))
|
1028
|
+
expect(ExceptionHandling).to receive(:log_info).with(/Unable to execute honeybadger_auto_tags callback. boom/)
|
1029
|
+
ExceptionHandling.log_error(exception, nil, honeybadger_tags: ["some-other-tag"])
|
1030
|
+
end
|
1031
|
+
end
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
|
1035
|
+
private
|
1036
|
+
|
1037
|
+
def logged_excluding_reload_filter
|
1038
|
+
ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def incrementing_mtime
|
1042
|
+
@mtime ||= Time.now
|
1043
|
+
@mtime += 1.day
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
def exception_1
|
1047
|
+
@exception_1 ||=
|
1048
|
+
begin
|
1049
|
+
raise StandardError, "Exception 1"
|
1050
|
+
rescue => ex
|
1051
|
+
ex
|
1052
|
+
end
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
def exception_2
|
1056
|
+
@exception_2 ||=
|
1057
|
+
begin
|
1058
|
+
raise StandardError, "Exception 2"
|
1059
|
+
rescue => ex
|
1060
|
+
ex
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
end
|