exception_notification 4.2.1 → 4.4.3

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 (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 +105 -780
  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 +30 -23
  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 +71 -40
  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 +17 -14
  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} +13 -11
  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 +59 -38
  66. data/test/exception_notifier/datadog_notifier_test.rb +153 -0
  67. data/test/exception_notifier/email_notifier_test.rb +279 -145
  68. data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
  69. data/test/exception_notifier/hipchat_notifier_test.rb +105 -64
  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 -17
  75. data/test/exception_notifier/slack_notifier_test.rb +84 -62
  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 +52 -48
  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 -35
  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.3'
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 Mongoid::Errors::DocumentNotFound 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: @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