exception_handling 3.0.pre.1 → 3.0.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.
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 +60 -89
  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