exceptify 1.0.0

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +16 -0
  3. data/CODE_OF_CONDUCT.md +22 -0
  4. data/CONTRIBUTING.md +33 -0
  5. data/MIT-LICENSE +23 -0
  6. data/README.md +534 -0
  7. data/RELEASING.md +51 -0
  8. data/Rakefile +25 -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/slack.md +154 -0
  13. data/docs/notifiers/sns.md +37 -0
  14. data/docs/notifiers/teams.md +54 -0
  15. data/docs/notifiers/webhook.md +60 -0
  16. data/exceptify.gemspec +48 -0
  17. data/lib/exceptify/base_notifier.rb +30 -0
  18. data/lib/exceptify/configuration.rb +184 -0
  19. data/lib/exceptify/datadog_notifier.rb +160 -0
  20. data/lib/exceptify/dispatcher.rb +49 -0
  21. data/lib/exceptify/email_notifier.rb +208 -0
  22. data/lib/exceptify/modules/backtrace_cleaner.rb +13 -0
  23. data/lib/exceptify/modules/error_grouping.rb +170 -0
  24. data/lib/exceptify/modules/formatter.rb +119 -0
  25. data/lib/exceptify/notification.rb +71 -0
  26. data/lib/exceptify/notifier.rb +19 -0
  27. data/lib/exceptify/notifier_registry.rb +55 -0
  28. data/lib/exceptify/rack.rb +88 -0
  29. data/lib/exceptify/rails/runner_tie.rb +57 -0
  30. data/lib/exceptify/rails.rb +29 -0
  31. data/lib/exceptify/rake.rb +59 -0
  32. data/lib/exceptify/request_context.rb +35 -0
  33. data/lib/exceptify/resque.rb +25 -0
  34. data/lib/exceptify/sidekiq.rb +15 -0
  35. data/lib/exceptify/slack_notifier.rb +141 -0
  36. data/lib/exceptify/sns_notifier.rb +98 -0
  37. data/lib/exceptify/solid_queue.rb +68 -0
  38. data/lib/exceptify/teams_notifier.rb +209 -0
  39. data/lib/exceptify/version.rb +5 -0
  40. data/lib/exceptify/views/exceptify/_backtrace.html.erb +3 -0
  41. data/lib/exceptify/views/exceptify/_backtrace.text.erb +1 -0
  42. data/lib/exceptify/views/exceptify/_data.html.erb +6 -0
  43. data/lib/exceptify/views/exceptify/_data.text.erb +1 -0
  44. data/lib/exceptify/views/exceptify/_environment.html.erb +10 -0
  45. data/lib/exceptify/views/exceptify/_environment.text.erb +5 -0
  46. data/lib/exceptify/views/exceptify/_request.html.erb +36 -0
  47. data/lib/exceptify/views/exceptify/_request.text.erb +10 -0
  48. data/lib/exceptify/views/exceptify/_session.html.erb +10 -0
  49. data/lib/exceptify/views/exceptify/_session.text.erb +2 -0
  50. data/lib/exceptify/views/exceptify/_title.html.erb +3 -0
  51. data/lib/exceptify/views/exceptify/_title.text.erb +3 -0
  52. data/lib/exceptify/views/exceptify/background_exceptify.html.erb +53 -0
  53. data/lib/exceptify/views/exceptify/background_exceptify.text.erb +14 -0
  54. data/lib/exceptify/views/exceptify/exceptify.html.erb +52 -0
  55. data/lib/exceptify/views/exceptify/exceptify.text.erb +24 -0
  56. data/lib/exceptify/webhook_notifier.rb +63 -0
  57. data/lib/exceptify.rb +177 -0
  58. data/lib/generators/exceptify/install_generator.rb +24 -0
  59. data/lib/generators/exceptify/templates/exceptify.rb.erb +44 -0
  60. metadata +364 -0
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Exception</title>
6
+ </head>
7
+ <body>
8
+
9
+ <%
10
+ sections_content = @sections.map do |section|
11
+ begin
12
+ summary = render(section).strip
13
+ unless summary.blank?
14
+ title = render("title", :title => section).strip
15
+ [title, summary]
16
+ end
17
+
18
+ rescue Exception => e
19
+ title = render("title", :title => section).strip
20
+ summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n")
21
+
22
+ [title, summary]
23
+ end
24
+ end
25
+ %>
26
+
27
+ <table width="100%" border="0" cellpadding="0" cellspacing="0">
28
+ <tr><td align="center" valign="top">
29
+ <table width="650" border="0" cellpadding="0" cellspacing="20">
30
+ <tr>
31
+ <td style="padding: 10px; border: 1px solid #eed3d7; background-color: #f2dede">
32
+ <h3 style="color: #b94a48">
33
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= @timestamp %> :
34
+ </h3>
35
+ <p style="color: #b94a48"><%= @exception.message %></p>
36
+ <pre style="font-size: 12px; padding: 5px; background-color:#f5f5f5">
37
+ <%= @backtrace.first %>
38
+ </pre>
39
+ </td>
40
+ </tr>
41
+ <% sections_content.each do |title, summary| %>
42
+ <tr>
43
+ <td style="border-bottom: 1px solid #eeeeee;"><%= raw title %></td>
44
+ </tr>
45
+ <tr>
46
+ <td><%= raw summary %></td>
47
+ </tr>
48
+ <% end %>
49
+ </table>
50
+ </td></tr>
51
+ </table>
52
+ </body>
53
+ </html>
@@ -0,0 +1,14 @@
1
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= raw @timestamp %> :
2
+
3
+ <%= @exception.message %>
4
+ <%= @backtrace.first %>
5
+
6
+ <% sections = @sections.map do |section|
7
+ summary = render(section).strip
8
+ unless summary.blank?
9
+ title = render("title", :title => section).strip
10
+ "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
11
+ end
12
+ end.join
13
+ %>
14
+ <%= raw sections %>
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Exception</title>
6
+ </head>
7
+ <body>
8
+
9
+ <%
10
+ sections_content = @sections.map do |section|
11
+ begin
12
+ summary = render(section).strip
13
+ unless summary.blank?
14
+ title = render("title", title: section).strip
15
+ [title, summary]
16
+ end
17
+ rescue Exception => e
18
+ title = render("title", title: section).strip
19
+ summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n")
20
+ [title, summary]
21
+ end
22
+ end
23
+ %>
24
+
25
+ <table width="100%" border="0" cellpadding="0" cellspacing="0">
26
+ <tr><td align="center" valign="top">
27
+ <table width="650" border="0" cellpadding="0" cellspacing="20">
28
+ <tr>
29
+ <td style="padding: 10px; border: 1px solid #eed3d7; background-color: #f2dede">
30
+ <h3 style="color: #b94a48">
31
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in <%= @kontroller.controller_name %>#<%= @kontroller.action_name %>:
32
+ </h3>
33
+ <p style="color: #b94a48"><%= @exception.message %></p>
34
+ <pre style="font-size: 12px; padding: 5px; background-color:#f5f5f5">
35
+ <%= @backtrace.first %>
36
+ </pre>
37
+ </td>
38
+ </tr>
39
+ <% sections_content.each do |title, summary| %>
40
+ <tr>
41
+ <td style="border-bottom: 1px solid #eeeeee;"><%= raw title %></td>
42
+ </tr>
43
+ <tr>
44
+ <td><%= raw summary %></td>
45
+ </tr>
46
+ <% end %>
47
+ </table>
48
+ </td></tr>
49
+ </table>
50
+
51
+ </body>
52
+ </html>
@@ -0,0 +1,24 @@
1
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in <%= @kontroller.controller_name %>#<%= @kontroller.action_name %>:
2
+
3
+ <%= raw @exception.message %>
4
+ <%= raw @backtrace.first %>
5
+
6
+ <%
7
+ sections = @sections.map do |section|
8
+ begin
9
+ summary = render(section).strip
10
+ unless summary.blank?
11
+ title = render("title", title: section).strip
12
+ "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
13
+ end
14
+
15
+ rescue Exception => e
16
+ title = render("title", title: section).strip
17
+ summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n")
18
+
19
+ [title, summary.gsub(/^/, " "), nil].join("\n\n")
20
+ end
21
+ end.join
22
+ %>
23
+
24
+ <%= raw sections %>
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+ require "active_support/core_ext/time"
5
+
6
+ module Exceptify
7
+ class WebhookNotifier < BaseNotifier
8
+ def initialize(options)
9
+ options = options.dup
10
+ @http_client = options.delete(:http_client) || HTTParty
11
+ super()
12
+ self.base_options = options
13
+ @default_options = options
14
+ end
15
+
16
+ def call(exception, options = {})
17
+ notification = Notification.new(exception, options)
18
+ env = notification.env
19
+
20
+ options = options.reverse_merge(@default_options)
21
+ url = options.delete(:url)
22
+ raise ArgumentError, "You must provide 'url' option" if blank?(url)
23
+
24
+ http_method = options.delete(:http_method) || :post
25
+
26
+ options[:body] ||= {}
27
+ options[:body][:server] = notification.hostname
28
+ options[:body][:process] = Process.pid
29
+ options[:body][:rails_root] = Rails.root if defined?(Rails) && Rails.respond_to?(:root)
30
+ options[:body][:exception] = {
31
+ error_class: exception.class.to_s,
32
+ message: exception.message.inspect,
33
+ backtrace: notification.backtrace
34
+ }
35
+ options[:body][:data] = notification.data
36
+
37
+ unless env.nil?
38
+ request = notification.request_context.request
39
+
40
+ request_items = {
41
+ url: request.original_url,
42
+ http_method: request.method,
43
+ ip_address: request.remote_ip,
44
+ parameters: request.filtered_parameters,
45
+ timestamp: notification.timestamp
46
+ }
47
+
48
+ options[:body][:request] = request_items
49
+ options[:body][:session] = request.session
50
+ options[:body][:environment] = request.filtered_env
51
+ end
52
+ send_notice(exception, options, nil, @default_options) do |_, _|
53
+ @http_client.send(http_method, url, options)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def blank?(value)
60
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
61
+ end
62
+ end
63
+ end
data/lib/exceptify.rb ADDED
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "exceptify/version"
4
+ require "exceptify/base_notifier"
5
+ require "exceptify/configuration"
6
+ require "exceptify/dispatcher"
7
+ require "exceptify/notification"
8
+ require "exceptify/request_context"
9
+
10
+ module Exceptify
11
+ autoload :BacktraceCleaner, "exceptify/modules/backtrace_cleaner"
12
+ autoload :Formatter, "exceptify/modules/formatter"
13
+
14
+ autoload :Rack, "exceptify/rack"
15
+ autoload :Notifier, "exceptify/notifier"
16
+ autoload :EmailNotifier, "exceptify/email_notifier"
17
+ autoload :WebhookNotifier, "exceptify/webhook_notifier"
18
+ autoload :SlackNotifier, "exceptify/slack_notifier"
19
+ autoload :TeamsNotifier, "exceptify/teams_notifier"
20
+ autoload :SnsNotifier, "exceptify/sns_notifier"
21
+ autoload :DatadogNotifier, "exceptify/datadog_notifier"
22
+
23
+ class UndefinedNotifierError < StandardError; end
24
+
25
+ class << self
26
+ attr_writer :configuration
27
+
28
+ def configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def configure
33
+ yield configuration
34
+ end
35
+
36
+ def reset!
37
+ self.configuration = Configuration.new
38
+ end
39
+
40
+ def reset_notifiers!
41
+ configuration.reset!
42
+ end
43
+
44
+ def testing_mode!
45
+ configuration.testing_mode!
46
+ end
47
+
48
+ def notify_exception(exception, options = {}, &block)
49
+ Dispatcher.new(configuration).notify_exception(exception, options, &block)
50
+ end
51
+
52
+ def register_notifier(name, notifier_or_options)
53
+ configuration.register_notifier(name, notifier_or_options)
54
+ end
55
+ alias_method :add_notifier, :register_notifier
56
+
57
+ def unregister_notifier(name)
58
+ configuration.unregister_notifier(name)
59
+ end
60
+
61
+ def registered_notifier(name)
62
+ configuration.registered_notifier(name)
63
+ end
64
+
65
+ def notifiers
66
+ configuration.notifiers
67
+ end
68
+
69
+ def ignore_if(&block)
70
+ configuration.ignore_if(&block)
71
+ end
72
+
73
+ def ignore_notifier_if(notifier, &block)
74
+ configuration.ignore_notifier_if(notifier, &block)
75
+ end
76
+
77
+ def ignore_crawlers(crawlers)
78
+ configuration.ignore_crawlers(crawlers)
79
+ end
80
+
81
+ def clear_ignore_conditions!
82
+ configuration.clear_ignore_conditions!
83
+ end
84
+
85
+ def ignored?(exception, options)
86
+ configuration.ignored?(exception, options)
87
+ end
88
+
89
+ def notifier_ignored?(exception, options, notifier:)
90
+ configuration.notifier_ignored?(exception, options, notifier: notifier)
91
+ end
92
+
93
+ def ignored_exception?(ignore_array, exception)
94
+ configuration.ignored_exception?(ignore_array, exception)
95
+ end
96
+
97
+ def error_count(error_key)
98
+ configuration.error_count(error_key)
99
+ end
100
+
101
+ def save_error_count(error_key, count)
102
+ configuration.save_error_count(error_key, count)
103
+ end
104
+
105
+ def group_error!(exception, options)
106
+ configuration.group_error!(exception, options)
107
+ end
108
+
109
+ def send_notification?(exception, count)
110
+ configuration.send_notification?(exception, count)
111
+ end
112
+
113
+ def logger
114
+ configuration.logger
115
+ end
116
+
117
+ def logger=(logger)
118
+ configuration.logger = logger
119
+ end
120
+
121
+ def ignored_exceptions
122
+ configuration.ignored_exceptions
123
+ end
124
+
125
+ def ignored_exceptions=(ignored_exceptions)
126
+ configuration.ignored_exceptions = ignored_exceptions
127
+ end
128
+
129
+ def testing_mode
130
+ configuration.testing_mode
131
+ end
132
+
133
+ def testing_mode=(testing_mode)
134
+ configuration.testing_mode = testing_mode
135
+ end
136
+
137
+ def error_grouping
138
+ configuration.error_grouping
139
+ end
140
+
141
+ def error_grouping=(error_grouping)
142
+ configuration.error_grouping = error_grouping
143
+ end
144
+
145
+ def error_grouping_period
146
+ configuration.error_grouping_period
147
+ end
148
+
149
+ def error_grouping_period=(error_grouping_period)
150
+ configuration.error_grouping_period = error_grouping_period
151
+ end
152
+
153
+ def notification_trigger
154
+ configuration.notification_trigger
155
+ end
156
+
157
+ def notification_trigger=(notification_trigger)
158
+ configuration.notification_trigger = notification_trigger
159
+ end
160
+
161
+ def error_grouping_cache
162
+ configuration.error_grouping_cache
163
+ end
164
+
165
+ def error_grouping_cache=(error_grouping_cache)
166
+ configuration.error_grouping_cache = error_grouping_cache
167
+ end
168
+
169
+ def fallback_cache_store
170
+ configuration.fallback_cache_store
171
+ end
172
+
173
+ def fallback_cache_store=(fallback_cache_store)
174
+ configuration.fallback_cache_store = fallback_cache_store
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptify
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ desc "Creates an Exceptify initializer."
7
+
8
+ source_root File.expand_path("templates", __dir__)
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."
15
+ class_option :solid_queue,
16
+ type: :boolean,
17
+ desc: "Add support for sending notifications when errors occur in Solid Queue jobs."
18
+
19
+ def copy_initializer
20
+ template "exceptify.rb.erb", "config/initializers/exceptify.rb"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ # Move this require to your `config/application.rb` if you want to be notified from runner commands too.
2
+ require "exceptify/rails"
3
+ require "exceptify/rake"
4
+ <% if options.sidekiq? %>
5
+ require "exceptify/sidekiq"<% end %><% if options.solid_queue? %>
6
+ require "exceptify/solid_queue"<% end %><% if options.resque? %>
7
+ require "resque/failure/multiple"
8
+ require "resque/failure/redis"
9
+ require "exceptify/resque"
10
+
11
+ Resque::Failure::Multiple.classes = [ Resque::Failure::Redis, Exceptify::Resque ]
12
+ Resque::Failure.backend = Resque::Failure::Multiple<% end %>
13
+
14
+ Exceptify.configure do |config|
15
+ # Ignore additional exception types. The default list of exception classes is:
16
+ # ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound
17
+ # ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError
18
+ # ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError
19
+ # config.ignored_exceptions += %w[ActionView::TemplateError CustomError]
20
+
21
+ # Adds a condition to decide when an exception must be ignored or not.
22
+ # The ignore_if method can be invoked multiple times to add extra conditions.
23
+ # config.ignore_if do |exception, options|
24
+ # Rails.env.local?
25
+ # end
26
+
27
+ # Ignore exceptions generated by crawlers
28
+ # config.ignore_crawlers %w[Googlebot bingbot]
29
+
30
+ # Notifiers =================================================================
31
+
32
+ # Email notifier sends notifications by email.
33
+ config.add_notifier :email, {
34
+ email_prefix: "[ERROR] ",
35
+ sender_address: %("Notifier" <notifier@example.com>),
36
+ exception_recipients: %w[exceptions@example.com]
37
+ }
38
+
39
+ # Webhook notifier sends notifications over HTTP protocol. Requires "httparty" gem.
40
+ # config.add_notifier :webhook, {
41
+ # url: "http://example.com:5555/hubot/path",
42
+ # http_method: :post
43
+ # }
44
+ end