exception_notification 4.4.0 → 4.4.1

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 (61) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +3 -1
  3. data/CHANGELOG.rdoc +10 -0
  4. data/Gemfile +2 -0
  5. data/README.md +34 -8
  6. data/Rakefile +2 -0
  7. data/examples/sample_app.rb +2 -0
  8. data/examples/sinatra/Gemfile +2 -0
  9. data/examples/sinatra/config.ru +2 -0
  10. data/examples/sinatra/sinatra_app.rb +6 -2
  11. data/exception_notification.gemspec +9 -8
  12. data/gemfiles/rails4_0.gemfile +3 -3
  13. data/gemfiles/rails4_1.gemfile +3 -3
  14. data/gemfiles/rails4_2.gemfile +3 -3
  15. data/gemfiles/rails5_0.gemfile +3 -3
  16. data/gemfiles/rails5_1.gemfile +3 -3
  17. data/gemfiles/rails5_2.gemfile +3 -3
  18. data/lib/exception_notification.rb +2 -0
  19. data/lib/exception_notification/rack.rb +24 -13
  20. data/lib/exception_notification/rails.rb +2 -0
  21. data/lib/exception_notification/resque.rb +2 -0
  22. data/lib/exception_notification/sidekiq.rb +5 -3
  23. data/lib/exception_notification/version.rb +3 -1
  24. data/lib/exception_notifier.rb +46 -7
  25. data/lib/exception_notifier/base_notifier.rb +8 -2
  26. data/lib/exception_notifier/campfire_notifier.rb +2 -0
  27. data/lib/exception_notifier/datadog_notifier.rb +12 -9
  28. data/lib/exception_notifier/email_notifier.rb +11 -3
  29. data/lib/exception_notifier/google_chat_notifier.rb +2 -0
  30. data/lib/exception_notifier/hipchat_notifier.rb +2 -0
  31. data/lib/exception_notifier/irc_notifier.rb +4 -3
  32. data/lib/exception_notifier/mattermost_notifier.rb +10 -0
  33. data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -0
  34. data/lib/exception_notifier/modules/error_grouping.rb +19 -9
  35. data/lib/exception_notifier/modules/formatter.rb +3 -0
  36. data/lib/exception_notifier/notifier.rb +5 -1
  37. data/lib/exception_notifier/slack_notifier.rb +2 -0
  38. data/lib/exception_notifier/sns_notifier.rb +4 -3
  39. data/lib/exception_notifier/teams_notifier.rb +10 -3
  40. data/lib/exception_notifier/webhook_notifier.rb +3 -3
  41. data/lib/generators/exception_notification/install_generator.rb +8 -2
  42. data/test/exception_notification/rack_test.rb +48 -2
  43. data/test/exception_notification/resque_test.rb +2 -0
  44. data/test/exception_notifier/campfire_notifier_test.rb +8 -1
  45. data/test/exception_notifier/datadog_notifier_test.rb +2 -0
  46. data/test/exception_notifier/email_notifier_test.rb +29 -3
  47. data/test/exception_notifier/google_chat_notifier_test.rb +15 -11
  48. data/test/exception_notifier/hipchat_notifier_test.rb +8 -2
  49. data/test/exception_notifier/irc_notifier_test.rb +2 -0
  50. data/test/exception_notifier/mattermost_notifier_test.rb +73 -24
  51. data/test/exception_notifier/modules/error_grouping_test.rb +2 -0
  52. data/test/exception_notifier/modules/formatter_test.rb +2 -0
  53. data/test/exception_notifier/sidekiq_test.rb +3 -11
  54. data/test/exception_notifier/slack_notifier_test.rb +12 -10
  55. data/test/exception_notifier/sns_notifier_test.rb +9 -7
  56. data/test/exception_notifier/teams_notifier_test.rb +2 -0
  57. data/test/exception_notifier/webhook_notifier_test.rb +6 -4
  58. data/test/exception_notifier_test.rb +112 -6
  59. data/test/support/exception_notifier_helper.rb +14 -0
  60. data/test/test_helper.rb +5 -1
  61. metadata +22 -27
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class BaseNotifier
3
5
  attr_accessor :base_options
@@ -14,11 +16,15 @@ module ExceptionNotifier
14
16
  end
15
17
 
16
18
  def _pre_callback(exception, options, message, message_opts)
17
- @base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:pre_callback].respond_to?(:call)
19
+ return unless @base_options[:pre_callback].respond_to?(:call)
20
+
21
+ @base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts)
18
22
  end
19
23
 
20
24
  def _post_callback(exception, options, message, message_opts)
21
- @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:post_callback].respond_to?(:call)
25
+ return unless @base_options[:post_callback].respond_to?(:call)
26
+
27
+ @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
22
28
  end
23
29
  end
24
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class CampfireNotifier < BaseNotifier
3
5
  attr_accessor :subdomain
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_dispatch'
2
4
 
3
5
  module ExceptionNotifier
@@ -30,7 +32,7 @@ module ExceptionNotifier
30
32
  MAX_TITLE_LENGTH = 120
31
33
  MAX_VALUE_LENGTH = 300
32
34
  MAX_BACKTRACE_SIZE = 3
33
- ALERT_TYPE = 'error'.freeze
35
+ ALERT_TYPE = 'error'
34
36
 
35
37
  attr_reader :exception,
36
38
  :options
@@ -74,11 +76,8 @@ module ExceptionNotifier
74
76
  end
75
77
 
76
78
  def formatted_title
77
- title = ''
78
- title << title_prefix
79
- title << "#{controller.controller_name} #{controller.action_name}" if controller
80
- title << " (#{exception.class})"
81
- title << " #{exception.message.inspect}"
79
+ title =
80
+ "#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"
82
81
 
83
82
  truncate(title, MAX_TITLE_LENGTH)
84
83
  end
@@ -108,9 +107,7 @@ module ExceptionNotifier
108
107
  text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
109
108
  text << formatted_key_value('Timestamp', Time.current)
110
109
  text << formatted_key_value('Server', Socket.gethostname)
111
- if defined?(Rails) && Rails.respond_to?(:root)
112
- text << formatted_key_value('Rails root', Rails.root)
113
- end
110
+ text << formatted_key_value('Rails root', Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
114
111
  text << formatted_key_value('Process', $PROCESS_ID)
115
112
  text << '___'
116
113
  text.join("\n")
@@ -148,6 +145,12 @@ module ExceptionNotifier
148
145
  object.to_s
149
146
  end
150
147
  end
148
+
149
+ private
150
+
151
+ def controller_subtitle
152
+ "#{controller.controller_name} #{controller.action_name}" if controller
153
+ end
151
154
  end
152
155
  end
153
156
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/time'
2
4
  require 'action_mailer'
3
5
  require 'action_dispatch'
@@ -74,13 +76,17 @@ module ExceptionNotifier
74
76
  def compose_subject
75
77
  subject = @options[:email_prefix].to_s.dup
76
78
  subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
77
- subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if @kontroller && @options[:include_controller_and_action_names_in_subject]
79
+ subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if include_controller?
78
80
  subject << " (#{@exception.class})"
79
81
  subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
80
82
  subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
81
83
  subject.length > 120 ? subject[0...120] + '...' : subject
82
84
  end
83
85
 
86
+ def include_controller?
87
+ @kontroller && @options[:include_controller_and_action_names_in_subject]
88
+ end
89
+
84
90
  def set_data_variables
85
91
  @data.each do |name, value|
86
92
  instance_variable_set("@#{name}", value)
@@ -121,7 +127,7 @@ module ExceptionNotifier
121
127
  headers = {
122
128
  delivery_method: @options[:delivery_method],
123
129
  to: exception_recipients,
124
- from: @options[:sender_address],
130
+ from: self.class.default[:from] || @options[:sender_address],
125
131
  subject: subject,
126
132
  template_name: name
127
133
  }.merge(@options[:email_headers])
@@ -137,7 +143,9 @@ module ExceptionNotifier
137
143
  end
138
144
 
139
145
  def load_custom_views
140
- prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views" if defined?(Rails) && Rails.respond_to?(:root)
146
+ return unless defined?(Rails) && Rails.respond_to?(:root)
147
+
148
+ prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views"
141
149
  end
142
150
 
143
151
  def maybe_call(maybe_proc)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'httparty'
2
4
 
3
5
  module ExceptionNotifier
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class HipchatNotifier < BaseNotifier
3
5
  attr_accessor :from
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class IrcNotifier < BaseNotifier
3
5
  def initialize(options)
@@ -9,9 +11,8 @@ module ExceptionNotifier
9
11
  def call(exception, options = {})
10
12
  errors_count = options[:accumulated_errors_count].to_i
11
13
 
12
- message = "'#{exception.message}'"
13
- message.prepend("(#{errors_count} times)") if errors_count > 1
14
-
14
+ occurrences = "(#{errors_count} times)" if errors_count > 1
15
+ message = "#{occurrences}'#{exception.message}'"
15
16
  message += " on '#{exception.backtrace.first}'" if exception.backtrace
16
17
 
17
18
  return unless active?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'httparty'
2
4
 
3
5
  module ExceptionNotifier
@@ -10,6 +12,8 @@ module ExceptionNotifier
10
12
 
11
13
  @gitlab_url = options[:git_url]
12
14
 
15
+ @env = options[:env] || {}
16
+
13
17
  payload = {
14
18
  text: message_text.compact.join("\n"),
15
19
  username: options[:username] || 'Exception Notifier'
@@ -52,6 +56,12 @@ module ExceptionNotifier
52
56
  text << backtrace
53
57
  end
54
58
 
59
+ if (exception_data = @env['exception_notifier.exception_data'])
60
+ text << '### Data'
61
+ data_string = exception_data.map { |k, v| "* #{k} : #{v}" }.join("\n")
62
+ text << "```\n#{data_string}\n```"
63
+ end
64
+
55
65
  text << message_issue_link if @gitlab_url
56
66
 
57
67
  text
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  module BacktraceCleaner
3
5
  def clean_backtrace(exception)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/numeric/time'
2
4
  require 'active_support/concern'
3
5
 
@@ -25,20 +27,21 @@ module ExceptionNotifier
25
27
  end
26
28
 
27
29
  def error_count(error_key)
28
- count = begin
29
- error_grouping_cache.read(error_key)
30
- rescue StandardError => e
31
- ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to read, reason: #{e.message}. Falling back to memory cache store.")
32
- fallback_cache_store.read(error_key)
33
- end
30
+ count =
31
+ begin
32
+ error_grouping_cache.read(error_key)
33
+ rescue StandardError => e
34
+ log_cache_error(error_grouping_cache, e, :read)
35
+ fallback_cache_store.read(error_key)
36
+ end
34
37
 
35
- count.to_i if count
38
+ count&.to_i
36
39
  end
37
40
 
38
41
  def save_error_count(error_key, count)
39
42
  error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
40
43
  rescue StandardError => e
41
- ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to write, reason: #{e.message}. Falling back to memory cache store.")
44
+ log_cache_error(error_grouping_cache, e, :write)
42
45
  fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
43
46
  end
44
47
 
@@ -50,7 +53,8 @@ module ExceptionNotifier
50
53
  accumulated_errors_count = count + 1
51
54
  save_error_count(message_based_key, accumulated_errors_count)
52
55
  else
53
- backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
56
+ backtrace_based_key =
57
+ "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
54
58
 
55
59
  if (count = error_grouping_cache.read(backtrace_based_key))
56
60
  accumulated_errors_count = count + 1
@@ -72,6 +76,12 @@ module ExceptionNotifier
72
76
  factor.to_i == factor
73
77
  end
74
78
  end
79
+
80
+ private
81
+
82
+ def log_cache_error(cache, exception, action)
83
+ "#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
84
+ end
75
85
  end
76
86
  end
77
87
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/time'
2
4
  require 'action_dispatch'
3
5
 
@@ -108,6 +110,7 @@ module ExceptionNotifier
108
110
 
109
111
  def rails_app_name
110
112
  return unless defined?(::Rails) && ::Rails.respond_to?(:application)
113
+
111
114
  Rails.application.class.parent_name.underscore
112
115
  end
113
116
 
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/deprecation'
2
4
 
3
5
  module ExceptionNotifier
4
6
  class Notifier
5
7
  def self.exception_notification(env, exception, options = {})
6
- ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).'
8
+ ActiveSupport::Deprecation.warn(
9
+ 'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).'
10
+ )
7
11
  ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env))
8
12
  end
9
13
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class SlackNotifier < BaseNotifier
3
5
  include ExceptionNotifier::BacktraceCleaner
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class SnsNotifier < BaseNotifier
3
5
  def initialize(options)
@@ -33,9 +35,8 @@ module ExceptionNotifier
33
35
  attr_reader :notifier, :options
34
36
 
35
37
  def build_subject(exception, options)
36
- subject = "#{options[:sns_prefix]} - "
37
- subject << accumulated_exception_name(exception, options)
38
- subject << ' occurred'
38
+ subject =
39
+ "#{options[:sns_prefix]} - #{accumulated_exception_name(exception, options)} occurred"
39
40
  subject.length > 120 ? subject[0...120] + '...' : subject
40
41
  end
41
42
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_dispatch'
2
4
  require 'active_support/core_ext/time'
3
5
  require 'json'
@@ -64,8 +66,6 @@ module ExceptionNotifier
64
66
  private
65
67
 
66
68
  def message_text
67
- errors_count = @options[:accumulated_errors_count].to_i
68
-
69
69
  text = {
70
70
  '@type' => 'MessageCard',
71
71
  '@context' => 'http://schema.org/extensions',
@@ -73,7 +73,7 @@ module ExceptionNotifier
73
73
  'title' => "⚠️ Exception Occurred in #{env_name} ⚠️",
74
74
  'sections' => [
75
75
  {
76
- 'activityTitle' => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + (@controller ? " in *#{controller_and_method}*." : '.'),
76
+ 'activityTitle' => activity_title,
77
77
  'activitySubtitle' => @exception.message.to_s
78
78
  }
79
79
  ],
@@ -100,6 +100,13 @@ module ExceptionNotifier
100
100
  details
101
101
  end
102
102
 
103
+ def activity_title
104
+ errors_count = @options[:accumulated_errors_count].to_i
105
+
106
+ "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" +
107
+ (@controller ? " in *#{controller_and_method}*." : '.')
108
+ end
109
+
103
110
  def message_request
104
111
  {
105
112
  'name' => 'Request',
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_dispatch'
2
4
  require 'active_support/core_ext/time'
3
5
 
@@ -18,9 +20,7 @@ module ExceptionNotifier
18
20
  options[:body] ||= {}
19
21
  options[:body][:server] = Socket.gethostname
20
22
  options[:body][:process] = $PROCESS_ID
21
- if defined?(Rails) && Rails.respond_to?(:root)
22
- options[:body][:rails_root] = Rails.root
23
- end
23
+ options[:body][:rails_root] = Rails.root if defined?(Rails) && Rails.respond_to?(:root)
24
24
  options[:body][:exception] = {
25
25
  error_class: exception.class.to_s,
26
26
  message: exception.message.inspect,
@@ -1,11 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotification
2
4
  module Generators
3
5
  class InstallGenerator < Rails::Generators::Base
4
6
  desc 'Creates a ExceptionNotification initializer.'
5
7
 
6
8
  source_root File.expand_path('templates', __dir__)
7
- class_option :resque, type: :boolean, desc: 'Add support for sending notifications when errors occur in Resque jobs.'
8
- class_option :sidekiq, type: :boolean, desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.'
9
+ class_option :resque,
10
+ type: :boolean,
11
+ desc: 'Add support for sending notifications when errors occur in Resque jobs.'
12
+ class_option :sidekiq,
13
+ type: :boolean,
14
+ desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.'
9
15
 
10
16
  def copy_initializer
11
17
  template 'exception_notification.rb.erb', 'config/initializers/exception_notification.rb'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class RackTest < ActiveSupport::TestCase
@@ -10,8 +12,7 @@ class RackTest < ActiveSupport::TestCase
10
12
  end
11
13
 
12
14
  teardown do
13
- ExceptionNotifier.error_grouping = false
14
- ExceptionNotifier.notification_trigger = nil
15
+ ExceptionNotifier.reset_notifiers!
15
16
  end
16
17
 
17
18
  test 'should ignore "X-Cascade" header by default' do
@@ -57,4 +58,49 @@ class RackTest < ActiveSupport::TestCase
57
58
  refute env['exception_notifier.delivered']
58
59
  end
59
60
  end
61
+
62
+ test 'should ignore exceptions if ignore_if condition is met' do
63
+ exception_app = Object.new
64
+ exception_app.stubs(:call).raises(RuntimeError)
65
+
66
+ env = {}
67
+
68
+ begin
69
+ ExceptionNotification::Rack.new(
70
+ exception_app,
71
+ ignore_if: ->(_env, exception) { exception.is_a? RuntimeError }
72
+ ).call(env)
73
+
74
+ flunk
75
+ rescue StandardError
76
+ refute env['exception_notifier.delivered']
77
+ end
78
+ end
79
+
80
+ test 'should ignore exceptions with notifiers that satisfies ignore_notifier_if condition' do
81
+ exception_app = Object.new
82
+ exception_app.stubs(:call).raises(RuntimeError)
83
+
84
+ notifier1_called = notifier2_called = false
85
+ notifier1 = ->(_exception, _options) { notifier1_called = true }
86
+ notifier2 = ->(_exception, _options) { notifier2_called = true }
87
+
88
+ env = {}
89
+
90
+ begin
91
+ ExceptionNotification::Rack.new(
92
+ exception_app,
93
+ ignore_notifier_if: {
94
+ notifier1: ->(_env, exception) { exception.is_a? RuntimeError }
95
+ },
96
+ notifier1: notifier1,
97
+ notifier2: notifier2
98
+ ).call(env)
99
+
100
+ flunk
101
+ rescue StandardError
102
+ refute notifier1_called
103
+ assert notifier2_called
104
+ end
105
+ end
60
106
  end