exception_notification 4.4.0 → 4.4.1

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