exception_notification 4.2.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +4 -3
  3. data/CHANGELOG.rdoc +57 -1
  4. data/CONTRIBUTING.md +21 -2
  5. data/Gemfile +3 -1
  6. data/README.md +106 -789
  7. data/Rakefile +4 -2
  8. data/docs/notifiers/campfire.md +50 -0
  9. data/docs/notifiers/custom.md +42 -0
  10. data/docs/notifiers/datadog.md +51 -0
  11. data/docs/notifiers/email.md +195 -0
  12. data/docs/notifiers/google_chat.md +31 -0
  13. data/docs/notifiers/hipchat.md +66 -0
  14. data/docs/notifiers/irc.md +97 -0
  15. data/docs/notifiers/mattermost.md +115 -0
  16. data/docs/notifiers/slack.md +161 -0
  17. data/docs/notifiers/sns.md +37 -0
  18. data/docs/notifiers/teams.md +54 -0
  19. data/docs/notifiers/webhook.md +60 -0
  20. data/examples/sample_app.rb +56 -0
  21. data/examples/sinatra/Gemfile +8 -6
  22. data/examples/sinatra/config.ru +3 -1
  23. data/examples/sinatra/sinatra_app.rb +19 -11
  24. data/exception_notification.gemspec +30 -23
  25. data/gemfiles/rails4_0.gemfile +1 -2
  26. data/gemfiles/rails4_1.gemfile +1 -2
  27. data/gemfiles/rails4_2.gemfile +1 -2
  28. data/gemfiles/rails5_0.gemfile +1 -2
  29. data/gemfiles/rails5_1.gemfile +7 -0
  30. data/gemfiles/rails5_2.gemfile +7 -0
  31. data/gemfiles/rails6_0.gemfile +7 -0
  32. data/lib/exception_notification.rb +3 -0
  33. data/lib/exception_notification/rack.rb +34 -27
  34. data/lib/exception_notification/rails.rb +3 -0
  35. data/lib/exception_notification/resque.rb +10 -10
  36. data/lib/exception_notification/sidekiq.rb +10 -12
  37. data/lib/exception_notification/version.rb +5 -0
  38. data/lib/exception_notifier.rb +79 -11
  39. data/lib/exception_notifier/base_notifier.rb +10 -5
  40. data/lib/exception_notifier/campfire_notifier.rb +14 -9
  41. data/lib/exception_notifier/datadog_notifier.rb +156 -0
  42. data/lib/exception_notifier/email_notifier.rb +78 -87
  43. data/lib/exception_notifier/google_chat_notifier.rb +44 -0
  44. data/lib/exception_notifier/hipchat_notifier.rb +16 -10
  45. data/lib/exception_notifier/irc_notifier.rb +38 -31
  46. data/lib/exception_notifier/mattermost_notifier.rb +54 -131
  47. data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
  48. data/lib/exception_notifier/modules/error_grouping.rb +87 -0
  49. data/lib/exception_notifier/modules/formatter.rb +121 -0
  50. data/lib/exception_notifier/notifier.rb +9 -6
  51. data/lib/exception_notifier/slack_notifier.rb +75 -32
  52. data/lib/exception_notifier/sns_notifier.rb +86 -0
  53. data/lib/exception_notifier/teams_notifier.rb +200 -0
  54. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
  55. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
  56. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
  57. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
  58. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -4
  59. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
  60. data/lib/exception_notifier/webhook_notifier.rb +19 -16
  61. data/lib/generators/exception_notification/install_generator.rb +11 -5
  62. data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +14 -12
  63. data/test/exception_notification/rack_test.rb +90 -4
  64. data/test/exception_notification/resque_test.rb +54 -0
  65. data/test/exception_notifier/campfire_notifier_test.rb +66 -39
  66. data/test/exception_notifier/datadog_notifier_test.rb +153 -0
  67. data/test/exception_notifier/email_notifier_test.rb +301 -145
  68. data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
  69. data/test/exception_notifier/hipchat_notifier_test.rb +112 -65
  70. data/test/exception_notifier/irc_notifier_test.rb +48 -30
  71. data/test/exception_notifier/mattermost_notifier_test.rb +218 -55
  72. data/test/exception_notifier/modules/error_grouping_test.rb +167 -0
  73. data/test/exception_notifier/modules/formatter_test.rb +152 -0
  74. data/test/exception_notifier/sidekiq_test.rb +9 -6
  75. data/test/exception_notifier/slack_notifier_test.rb +109 -59
  76. data/test/exception_notifier/sns_notifier_test.rb +123 -0
  77. data/test/exception_notifier/teams_notifier_test.rb +92 -0
  78. data/test/exception_notifier/webhook_notifier_test.rb +68 -38
  79. data/test/exception_notifier_test.rb +220 -37
  80. data/test/support/exception_notifier_helper.rb +14 -0
  81. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  82. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  83. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  84. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  85. data/test/test_helper.rb +14 -13
  86. metadata +154 -162
  87. data/test/dummy/.gitignore +0 -4
  88. data/test/dummy/Rakefile +0 -7
  89. data/test/dummy/app/controllers/application_controller.rb +0 -3
  90. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  91. data/test/dummy/app/helpers/application_helper.rb +0 -2
  92. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  93. data/test/dummy/app/models/post.rb +0 -2
  94. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  95. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  96. data/test/dummy/app/views/posts/new.html.erb +0 -0
  97. data/test/dummy/app/views/posts/show.html.erb +0 -0
  98. data/test/dummy/config.ru +0 -4
  99. data/test/dummy/config/application.rb +0 -42
  100. data/test/dummy/config/boot.rb +0 -6
  101. data/test/dummy/config/database.yml +0 -22
  102. data/test/dummy/config/environment.rb +0 -17
  103. data/test/dummy/config/environments/development.rb +0 -25
  104. data/test/dummy/config/environments/production.rb +0 -50
  105. data/test/dummy/config/environments/test.rb +0 -38
  106. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  107. data/test/dummy/config/initializers/inflections.rb +0 -10
  108. data/test/dummy/config/initializers/mime_types.rb +0 -5
  109. data/test/dummy/config/initializers/secret_token.rb +0 -8
  110. data/test/dummy/config/initializers/session_store.rb +0 -8
  111. data/test/dummy/config/locales/en.yml +0 -5
  112. data/test/dummy/config/routes.rb +0 -3
  113. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  114. data/test/dummy/db/schema.rb +0 -24
  115. data/test/dummy/db/seeds.rb +0 -7
  116. data/test/dummy/lib/tasks/.gitkeep +0 -0
  117. data/test/dummy/public/404.html +0 -26
  118. data/test/dummy/public/422.html +0 -26
  119. data/test/dummy/public/500.html +0 -26
  120. data/test/dummy/public/favicon.ico +0 -0
  121. data/test/dummy/public/images/rails.png +0 -0
  122. data/test/dummy/public/index.html +0 -239
  123. data/test/dummy/public/javascripts/application.js +0 -2
  124. data/test/dummy/public/javascripts/controls.js +0 -965
  125. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  126. data/test/dummy/public/javascripts/effects.js +0 -1123
  127. data/test/dummy/public/javascripts/prototype.js +0 -6001
  128. data/test/dummy/public/javascripts/rails.js +0 -191
  129. data/test/dummy/public/robots.txt +0 -5
  130. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  131. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  132. data/test/dummy/script/rails +0 -6
  133. data/test/dummy/test/functional/posts_controller_test.rb +0 -218
  134. data/test/dummy/test/test_helper.rb +0 -7
@@ -1,18 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sidekiq'
2
4
 
3
5
  # Note: this class is only needed for Sidekiq version < 3.
4
6
  module ExceptionNotification
5
7
  class Sidekiq
6
-
7
- def call(worker, msg, queue)
8
- begin
9
- yield
10
- rescue Exception => exception
11
- ExceptionNotifier.notify_exception(exception, :data => { :sidekiq => msg })
12
- raise exception
13
- end
8
+ def call(_worker, msg, _queue)
9
+ yield
10
+ rescue Exception => e
11
+ ExceptionNotifier.notify_exception(e, data: { sidekiq: msg })
12
+ raise e
14
13
  end
15
-
16
14
  end
17
15
  end
18
16
 
@@ -24,8 +22,8 @@ if ::Sidekiq::VERSION < '3'
24
22
  end
25
23
  else
26
24
  ::Sidekiq.configure_server do |config|
27
- config.error_handlers << Proc.new { |ex, context|
28
- ExceptionNotifier.notify_exception(ex, :data => { :sidekiq => context })
29
- }
25
+ config.error_handlers << proc do |ex, context|
26
+ ExceptionNotifier.notify_exception(ex, data: { sidekiq: context })
27
+ end
30
28
  end
31
29
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExceptionNotification
4
+ VERSION = '4.4.1'
5
+ end
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
  require 'active_support/core_ext/string/inflections'
3
5
  require 'active_support/core_ext/module/attribute_accessors'
4
6
  require 'exception_notifier/base_notifier'
7
+ require 'exception_notifier/modules/error_grouping'
5
8
 
6
9
  module ExceptionNotifier
10
+ include ErrorGrouping
7
11
 
8
12
  autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner'
13
+ autoload :Formatter, 'exception_notifier/modules/formatter'
9
14
 
10
15
  autoload :Notifier, 'exception_notifier/notifier'
11
16
  autoload :EmailNotifier, 'exception_notifier/email_notifier'
@@ -15,6 +20,10 @@ module ExceptionNotifier
15
20
  autoload :IrcNotifier, 'exception_notifier/irc_notifier'
16
21
  autoload :SlackNotifier, 'exception_notifier/slack_notifier'
17
22
  autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier'
23
+ autoload :TeamsNotifier, 'exception_notifier/teams_notifier'
24
+ autoload :SnsNotifier, 'exception_notifier/sns_notifier'
25
+ autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier'
26
+ autoload :DatadogNotifier, 'exception_notifier/datadog_notifier'
18
27
 
19
28
  class UndefinedNotifierError < StandardError; end
20
29
 
@@ -24,7 +33,10 @@ module ExceptionNotifier
24
33
 
25
34
  # Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
26
35
  mattr_accessor :ignored_exceptions
27
- @@ignored_exceptions = %w{ActiveRecord::RecordNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError}
36
+ @@ignored_exceptions = %w[
37
+ ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound
38
+ ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError
39
+ ]
28
40
 
29
41
  mattr_accessor :testing_mode
30
42
  @@testing_mode = false
@@ -33,6 +45,9 @@ module ExceptionNotifier
33
45
  # Store conditions that decide when exceptions must be ignored or not.
34
46
  @@ignores = []
35
47
 
48
+ # Store by-notifier conditions that decide when exceptions must be ignored or not.
49
+ @@by_notifier_ignores = {}
50
+
36
51
  # Store notifiers that send notifications when exceptions are raised.
37
52
  @@notifiers = {}
38
53
 
@@ -40,14 +55,25 @@ module ExceptionNotifier
40
55
  self.testing_mode = true
41
56
  end
42
57
 
43
- def notify_exception(exception, options={})
58
+ def notify_exception(exception, options = {}, &block)
44
59
  return false if ignored_exception?(options[:ignore_exceptions], exception)
45
60
  return false if ignored?(exception, options)
61
+
62
+ if error_grouping
63
+ errors_count = group_error!(exception, options)
64
+ return false unless send_notification?(exception, errors_count)
65
+ end
66
+
67
+ notification_fired = false
46
68
  selected_notifiers = options.delete(:notifiers) || notifiers
47
69
  [*selected_notifiers].each do |notifier|
48
- fire_notification(notifier, exception, options.dup)
70
+ unless notifier_ignored?(exception, options, notifier: notifier)
71
+ fire_notification(notifier, exception, options.dup, &block)
72
+ notification_fired = true
73
+ end
49
74
  end
50
- true
75
+
76
+ notification_fired
51
77
  end
52
78
 
53
79
  def register_exception_notifier(name, notifier_or_options)
@@ -82,31 +108,65 @@ module ExceptionNotifier
82
108
  @@ignores << block
83
109
  end
84
110
 
111
+ def ignore_notifier_if(notifier, &block)
112
+ @@by_notifier_ignores[notifier] = block
113
+ end
114
+
115
+ def ignore_crawlers(crawlers)
116
+ ignore_if do |_exception, opts|
117
+ opts.key?(:env) && from_crawler(opts[:env], crawlers)
118
+ end
119
+ end
120
+
85
121
  def clear_ignore_conditions!
86
122
  @@ignores.clear
123
+ @@by_notifier_ignores.clear
87
124
  end
88
125
 
89
126
  private
127
+
90
128
  def ignored?(exception, options)
91
- @@ignores.any?{ |condition| condition.call(exception, options) }
129
+ @@ignores.any? { |condition| condition.call(exception, options) }
92
130
  rescue Exception => e
93
131
  raise e if @@testing_mode
94
132
 
95
- logger.warn "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
133
+ logger.warn(
134
+ "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
135
+ )
136
+ false
137
+ end
138
+
139
+ def notifier_ignored?(exception, options, notifier:)
140
+ return false unless @@by_notifier_ignores.key?(notifier)
141
+
142
+ condition = @@by_notifier_ignores[notifier]
143
+ condition.call(exception, options)
144
+ rescue Exception => e
145
+ raise e if @@testing_mode
146
+
147
+ logger.warn(<<~"MESSAGE")
148
+ An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message}
149
+ #{e.backtrace.join("\n")}
150
+ MESSAGE
96
151
  false
97
152
  end
98
153
 
99
154
  def ignored_exception?(ignore_array, exception)
100
- (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s).include?(exception.class.name)
155
+ all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
156
+ exception_ancestors = exception.singleton_class.ancestors.map(&:to_s)
157
+ !(all_ignored_exceptions & exception_ancestors).empty?
101
158
  end
102
159
 
103
- def fire_notification(notifier_name, exception, options)
160
+ def fire_notification(notifier_name, exception, options, &block)
104
161
  notifier = registered_exception_notifier(notifier_name)
105
- notifier.call(exception, options)
162
+ notifier.call(exception, options, &block)
106
163
  rescue Exception => e
107
164
  raise e if @@testing_mode
108
165
 
109
- logger.warn "An error occurred when sending a notification using '#{notifier_name}' notifier. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
166
+ logger.warn(
167
+ "An error occurred when sending a notification using '#{notifier_name}' notifier." \
168
+ "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
169
+ )
110
170
  false
111
171
  end
112
172
 
@@ -116,7 +176,15 @@ module ExceptionNotifier
116
176
  notifier = notifier_class.new(options)
117
177
  register_exception_notifier(name, notifier)
118
178
  rescue NameError => e
119
- raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
179
+ raise UndefinedNotifierError,
180
+ "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
181
+ end
182
+
183
+ def from_crawler(env, ignored_crawlers)
184
+ agent = env['HTTP_USER_AGENT']
185
+ Array(ignored_crawlers).any? do |crawler|
186
+ agent =~ Regexp.new(crawler)
187
+ end
120
188
  end
121
189
  end
122
190
  end
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class BaseNotifier
3
5
  attr_accessor :base_options
4
6
 
5
- def initialize(options={})
7
+ def initialize(options = {})
6
8
  @base_options = options
7
9
  end
8
10
 
9
- def send_notice(exception, options, message, message_opts=nil)
11
+ def send_notice(exception, options, message, message_opts = nil)
10
12
  _pre_callback(exception, options, message, message_opts)
11
13
  result = yield(message, message_opts)
12
14
  _post_callback(exception, options, message, message_opts)
@@ -14,12 +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)
22
- end
25
+ return unless @base_options[:post_callback].respond_to?(:call)
23
26
 
27
+ @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
28
+ end
24
29
  end
25
30
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class CampfireNotifier < BaseNotifier
3
-
4
5
  attr_accessor :subdomain
5
6
  attr_accessor :token
6
7
  attr_accessor :room
@@ -12,18 +13,22 @@ module ExceptionNotifier
12
13
  room_name = options.delete(:room_name)
13
14
  @campfire = Tinder::Campfire.new subdomain, options
14
15
  @room = @campfire.find_room_by_name room_name
15
- rescue
16
+ rescue StandardError
16
17
  @campfire = @room = nil
17
18
  end
18
19
  end
19
20
 
20
- def call(exception, options={})
21
- if active?
22
- message = "A new exception occurred: '#{exception.message}'"
23
- message += " on '#{exception.backtrace.first}'" if exception.backtrace
24
- send_notice(exception, options, message) do |msg, _|
25
- @room.paste msg
26
- end
21
+ def call(exception, options = {})
22
+ return unless active?
23
+
24
+ message = if options[:accumulated_errors_count].to_i > 1
25
+ "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'"
26
+ else
27
+ "A new exception occurred: '#{exception.message}'"
28
+ end
29
+ message += " on '#{exception.backtrace.first}'" if exception.backtrace
30
+ send_notice(exception, options, message) do |msg, _|
31
+ @room.paste msg
27
32
  end
28
33
  end
29
34
 
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_dispatch'
4
+
5
+ module ExceptionNotifier
6
+ class DatadogNotifier < BaseNotifier
7
+ attr_reader :client,
8
+ :default_options
9
+
10
+ def initialize(options)
11
+ super
12
+ @client = options.fetch(:client)
13
+ @default_options = options
14
+ end
15
+
16
+ def call(exception, options = {})
17
+ client.emit_event(
18
+ datadog_event(exception, options)
19
+ )
20
+ end
21
+
22
+ def datadog_event(exception, options = {})
23
+ DatadogExceptionEvent.new(
24
+ exception,
25
+ options.reverse_merge(default_options)
26
+ ).event
27
+ end
28
+
29
+ class DatadogExceptionEvent
30
+ include ExceptionNotifier::BacktraceCleaner
31
+
32
+ MAX_TITLE_LENGTH = 120
33
+ MAX_VALUE_LENGTH = 300
34
+ MAX_BACKTRACE_SIZE = 3
35
+ ALERT_TYPE = 'error'
36
+
37
+ attr_reader :exception,
38
+ :options
39
+
40
+ def initialize(exception, options)
41
+ @exception = exception
42
+ @options = options
43
+ end
44
+
45
+ def request
46
+ @request ||= ActionDispatch::Request.new(options[:env]) if options[:env]
47
+ end
48
+
49
+ def controller
50
+ @controller ||= options[:env] && options[:env]['action_controller.instance']
51
+ end
52
+
53
+ def backtrace
54
+ @backtrace ||= exception.backtrace ? clean_backtrace(exception) : []
55
+ end
56
+
57
+ def tags
58
+ options[:tags] || []
59
+ end
60
+
61
+ def title_prefix
62
+ options[:title_prefix] || ''
63
+ end
64
+
65
+ def event
66
+ title = formatted_title
67
+ body = formatted_body
68
+
69
+ Dogapi::Event.new(
70
+ body,
71
+ msg_title: title,
72
+ alert_type: ALERT_TYPE,
73
+ tags: tags,
74
+ aggregation_key: [title]
75
+ )
76
+ end
77
+
78
+ def formatted_title
79
+ title =
80
+ "#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"
81
+
82
+ truncate(title, MAX_TITLE_LENGTH)
83
+ end
84
+
85
+ def formatted_body
86
+ text = []
87
+
88
+ text << '%%%'
89
+ text << formatted_request if request
90
+ text << formatted_session if request
91
+ text << formatted_backtrace
92
+ text << '%%%'
93
+
94
+ text.join("\n")
95
+ end
96
+
97
+ def formatted_key_value(key, value)
98
+ "**#{key}:** #{value}"
99
+ end
100
+
101
+ def formatted_request
102
+ text = []
103
+ text << '### **Request**'
104
+ text << formatted_key_value('URL', request.url)
105
+ text << formatted_key_value('HTTP Method', request.request_method)
106
+ text << formatted_key_value('IP Address', request.remote_ip)
107
+ text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
108
+ text << formatted_key_value('Timestamp', Time.current)
109
+ text << formatted_key_value('Server', Socket.gethostname)
110
+ text << formatted_key_value('Rails root', Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
111
+ text << formatted_key_value('Process', $PROCESS_ID)
112
+ text << '___'
113
+ text.join("\n")
114
+ end
115
+
116
+ def formatted_session
117
+ text = []
118
+ text << '### **Session**'
119
+ text << formatted_key_value('Data', request.session.to_hash)
120
+ text << '___'
121
+ text.join("\n")
122
+ end
123
+
124
+ def formatted_backtrace
125
+ size = [backtrace.size, MAX_BACKTRACE_SIZE].min
126
+
127
+ text = []
128
+ text << '### **Backtrace**'
129
+ text << '````'
130
+ size.times { |i| text << backtrace[i] }
131
+ text << '````'
132
+ text << '___'
133
+ text.join("\n")
134
+ end
135
+
136
+ def truncate(string, max)
137
+ string.length > max ? "#{string[0...max]}..." : string
138
+ end
139
+
140
+ def inspect_object(object)
141
+ case object
142
+ when Hash, Array
143
+ truncate(object.inspect, MAX_VALUE_LENGTH)
144
+ else
145
+ object.to_s
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def controller_subtitle
152
+ "#{controller.controller_name} #{controller.action_name}" if controller
153
+ end
154
+ end
155
+ end
156
+ end
@@ -1,4 +1,5 @@
1
- require "active_support/core_ext/hash/reverse_merge"
1
+ # frozen_string_literal: true
2
+
2
3
  require 'active_support/core_ext/time'
3
4
  require 'action_mailer'
4
5
  require 'action_dispatch'
@@ -6,47 +7,61 @@ require 'pp'
6
7
 
7
8
  module ExceptionNotifier
8
9
  class EmailNotifier < BaseNotifier
9
- attr_accessor(:sender_address, :exception_recipients,
10
- :pre_callback, :post_callback,
11
- :email_prefix, :email_format, :sections, :background_sections,
12
- :verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
13
- :email_headers, :mailer_parent, :template_path, :deliver_with)
10
+ DEFAULT_OPTIONS = {
11
+ sender_address: %("Exception Notifier" <exception.notifier@example.com>),
12
+ exception_recipients: [],
13
+ email_prefix: '[ERROR] ',
14
+ email_format: :text,
15
+ sections: %w[request session environment backtrace],
16
+ background_sections: %w[backtrace data],
17
+ verbose_subject: true,
18
+ normalize_subject: false,
19
+ include_controller_and_action_names_in_subject: true,
20
+ delivery_method: nil,
21
+ mailer_settings: nil,
22
+ email_headers: {},
23
+ mailer_parent: 'ActionMailer::Base',
24
+ template_path: 'exception_notifier',
25
+ deliver_with: nil
26
+ }.freeze
14
27
 
15
28
  module Mailer
16
29
  class MissingController
17
- def method_missing(*args, &block)
18
- end
30
+ def method_missing(*args, &block); end
19
31
  end
20
32
 
21
33
  def self.extended(base)
22
34
  base.class_eval do
23
- self.send(:include, ExceptionNotifier::BacktraceCleaner)
35
+ send(:include, ExceptionNotifier::BacktraceCleaner)
24
36
 
25
37
  # Append application view path to the ExceptionNotifier lookup context.
26
- self.append_view_path "#{File.dirname(__FILE__)}/views"
38
+ append_view_path "#{File.dirname(__FILE__)}/views"
27
39
 
28
- def exception_notification(env, exception, options={}, default_options={})
40
+ def exception_notification(env, exception, options = {}, default_options = {})
29
41
  load_custom_views
30
42
 
31
43
  @env = env
32
44
  @exception = exception
33
- @options = options.reverse_merge(env['exception_notifier.options'] || {}).reverse_merge(default_options)
45
+
46
+ env_options = env['exception_notifier.options'] || {}
47
+ @options = default_options.merge(env_options).merge(options)
48
+
34
49
  @kontroller = env['action_controller.instance'] || MissingController.new
35
50
  @request = ActionDispatch::Request.new(env)
36
51
  @backtrace = exception.backtrace ? clean_backtrace(exception) : []
37
52
  @timestamp = Time.current
38
53
  @sections = @options[:sections]
39
54
  @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
40
- @sections = @sections + %w(data) unless @data.empty?
55
+ @sections += %w[data] unless @data.empty?
41
56
 
42
57
  compose_email
43
58
  end
44
59
 
45
- def background_exception_notification(exception, options={}, default_options={})
60
+ def background_exception_notification(exception, options = {}, default_options = {})
46
61
  load_custom_views
47
62
 
48
63
  @exception = exception
49
- @options = options.reverse_merge(default_options)
64
+ @options = default_options.merge(options).symbolize_keys
50
65
  @backtrace = exception.backtrace || []
51
66
  @timestamp = Time.current
52
67
  @sections = @options[:background_sections]
@@ -59,12 +74,17 @@ module ExceptionNotifier
59
74
  private
60
75
 
61
76
  def compose_subject
62
- subject = "#{@options[:email_prefix]}"
63
- subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if @kontroller
77
+ subject = @options[:email_prefix].to_s.dup
78
+ subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
79
+ subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if include_controller?
64
80
  subject << " (#{@exception.class})"
65
81
  subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
66
82
  subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
67
- subject.length > 120 ? subject[0...120] + "..." : subject
83
+ subject.length > 120 ? subject[0...120] + '...' : subject
84
+ end
85
+
86
+ def include_controller?
87
+ @kontroller && @options[:include_controller_and_action_names_in_subject]
68
88
  end
69
89
 
70
90
  def set_data_variables
@@ -75,19 +95,23 @@ module ExceptionNotifier
75
95
 
76
96
  helper_method :inspect_object
77
97
 
98
+ def truncate(string, max)
99
+ string.length > max ? "#{string[0...max]}..." : string
100
+ end
101
+
78
102
  def inspect_object(object)
79
103
  case object
80
- when Hash, Array
81
- object.inspect
82
- else
83
- object.to_s
104
+ when Hash, Array
105
+ truncate(object.inspect, 300)
106
+ else
107
+ object.to_s
84
108
  end
85
109
  end
86
110
 
87
111
  helper_method :safe_encode
88
112
 
89
113
  def safe_encode(value)
90
- value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_")
114
+ value.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
91
115
  end
92
116
 
93
117
  def html_mail?
@@ -101,11 +125,11 @@ module ExceptionNotifier
101
125
  exception_recipients = maybe_call(@options[:exception_recipients])
102
126
 
103
127
  headers = {
104
- :delivery_method => @options[:delivery_method],
105
- :to => exception_recipients,
106
- :from => @options[:sender_address],
107
- :subject => subject,
108
- :template_name => name
128
+ delivery_method: @options[:delivery_method],
129
+ to: exception_recipients,
130
+ from: self.class.default[:from] || @options[:sender_address],
131
+ subject: subject,
132
+ template_name: name
109
133
  }.merge(@options[:email_headers])
110
134
 
111
135
  mail = mail(headers) do |format|
@@ -119,9 +143,9 @@ module ExceptionNotifier
119
143
  end
120
144
 
121
145
  def load_custom_views
122
- if defined?(Rails) && Rails.respond_to?(:root)
123
- self.prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views"
124
- end
146
+ return unless defined?(Rails) && Rails.respond_to?(:root)
147
+
148
+ prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views"
125
149
  end
126
150
 
127
151
  def maybe_call(maybe_proc)
@@ -133,81 +157,48 @@ module ExceptionNotifier
133
157
 
134
158
  def initialize(options)
135
159
  super
160
+
136
161
  delivery_method = (options[:delivery_method] || :smtp)
137
162
  mailer_settings_key = "#{delivery_method}_settings".to_sym
138
163
  options[:mailer_settings] = options.delete(mailer_settings_key)
139
164
 
140
- options.reverse_merge(EmailNotifier.default_options).select{|k,v|[
141
- :sender_address, :exception_recipients,
142
- :pre_callback, :post_callback,
143
- :email_prefix, :email_format, :sections, :background_sections,
144
- :verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
145
- :email_headers, :mailer_parent, :template_path, :deliver_with].include?(k)}.each{|k,v| send("#{k}=", v)}
146
- end
147
-
148
- def options
149
- @options ||= {}.tap do |opts|
150
- self.instance_variables.each { |var| opts[var[1..-1].to_sym] = self.instance_variable_get(var) }
151
- end
152
- end
153
-
154
- def mailer
155
- @mailer ||= Class.new(mailer_parent.constantize).tap do |mailer|
156
- mailer.extend(EmailNotifier::Mailer)
157
- mailer.mailer_name = template_path
158
- end
165
+ @base_options = DEFAULT_OPTIONS.merge(options)
159
166
  end
160
167
 
161
- def call(exception, options={})
168
+ def call(exception, options = {})
162
169
  message = create_email(exception, options)
163
170
 
164
- # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
165
- if deliver_with == :default
166
- if message.respond_to?(:deliver_now)
167
- message.deliver_now
168
- else
169
- message.deliver
170
- end
171
- else
172
- message.send(deliver_with)
173
- end
171
+ message.send(base_options[:deliver_with] || default_deliver_with(message))
174
172
  end
175
173
 
176
- def create_email(exception, options={})
174
+ def create_email(exception, options = {})
177
175
  env = options[:env]
178
- default_options = self.options
179
- if env.nil?
180
- send_notice(exception, options, nil, default_options) do |_, default_opts|
176
+
177
+ send_notice(exception, options, nil, base_options) do |_, default_opts|
178
+ if env.nil?
181
179
  mailer.background_exception_notification(exception, options, default_opts)
182
- end
183
- else
184
- send_notice(exception, options, nil, default_options) do |_, default_opts|
180
+ else
185
181
  mailer.exception_notification(env, exception, options, default_opts)
186
182
  end
187
183
  end
188
184
  end
189
185
 
190
- def self.default_options
191
- {
192
- :sender_address => %("Exception Notifier" <exception.notifier@example.com>),
193
- :exception_recipients => [],
194
- :email_prefix => "[ERROR] ",
195
- :email_format => :text,
196
- :sections => %w(request session environment backtrace),
197
- :background_sections => %w(backtrace data),
198
- :verbose_subject => true,
199
- :normalize_subject => false,
200
- :delivery_method => nil,
201
- :mailer_settings => nil,
202
- :email_headers => {},
203
- :mailer_parent => 'ActionMailer::Base',
204
- :template_path => 'exception_notifier',
205
- :deliver_with => :default
206
- }
207
- end
208
-
209
186
  def self.normalize_digits(string)
210
187
  string.gsub(/[0-9]+/, 'N')
211
188
  end
189
+
190
+ private
191
+
192
+ def mailer
193
+ @mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer|
194
+ mailer.extend(EmailNotifier::Mailer)
195
+ mailer.mailer_name = base_options[:template_path]
196
+ end
197
+ end
198
+
199
+ def default_deliver_with(message)
200
+ # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
201
+ message.respond_to?(:deliver_now) ? :deliver_now : :deliver
202
+ end
212
203
  end
213
204
  end