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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/pipeline.yml +36 -0
  4. data/.gitignore +3 -0
  5. data/.rspec +3 -0
  6. data/.ruby-version +1 -1
  7. data/.tool-versions +1 -0
  8. data/Appraisals +13 -0
  9. data/CHANGELOG.md +150 -0
  10. data/Gemfile +10 -16
  11. data/Gemfile.lock +65 -128
  12. data/README.md +51 -19
  13. data/Rakefile +8 -11
  14. data/exception_handling.gemspec +11 -13
  15. data/gemfiles/rails_5.gemfile +16 -0
  16. data/gemfiles/rails_6.gemfile +16 -0
  17. data/gemfiles/rails_7.gemfile +16 -0
  18. data/lib/exception_handling/escalate_callback.rb +19 -0
  19. data/lib/exception_handling/exception_info.rb +15 -11
  20. data/lib/exception_handling/log_stub_error.rb +2 -1
  21. data/lib/exception_handling/logging_methods.rb +21 -0
  22. data/lib/exception_handling/testing.rb +9 -12
  23. data/lib/exception_handling/version.rb +1 -1
  24. data/lib/exception_handling.rb +83 -173
  25. data/{test → spec}/helpers/exception_helpers.rb +2 -2
  26. data/spec/rake_test_warning_false.rb +20 -0
  27. data/{test/test_helper.rb → spec/spec_helper.rb} +63 -66
  28. data/spec/unit/exception_handling/escalate_callback_spec.rb +81 -0
  29. data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
  30. data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
  31. data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +170 -114
  32. data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
  33. data/spec/unit/exception_handling/logging_methods_spec.rb +38 -0
  34. data/spec/unit/exception_handling_spec.rb +1063 -0
  35. metadata +62 -91
  36. data/lib/exception_handling/honeybadger_callbacks.rb +0 -59
  37. data/lib/exception_handling/mailer.rb +0 -70
  38. data/lib/exception_handling/methods.rb +0 -101
  39. data/lib/exception_handling/sensu.rb +0 -28
  40. data/semaphore_ci/setup.sh +0 -3
  41. data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
  42. data/test/unit/exception_handling/exception_description_test.rb +0 -82
  43. data/test/unit/exception_handling/honeybadger_callbacks_test.rb +0 -122
  44. data/test/unit/exception_handling/mailer_test.rb +0 -98
  45. data/test/unit/exception_handling/methods_test.rb +0 -84
  46. data/test/unit/exception_handling/sensu_test.rb +0 -52
  47. data/test/unit/exception_handling_test.rb +0 -1109
  48. data/views/exception_handling/mailer/escalate_custom.html.erb +0 -17
  49. data/views/exception_handling/mailer/escalation_notification.html.erb +0 -17
  50. data/views/exception_handling/mailer/log_parser_exception_notification.html.erb +0 -82
  51. /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