exception_notification 3.0.1 → 4.4.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 (153) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +7 -0
  3. data/CHANGELOG.rdoc +129 -1
  4. data/CODE_OF_CONDUCT.md +22 -0
  5. data/CONTRIBUTING.md +29 -1
  6. data/Gemfile +1 -1
  7. data/MIT-LICENSE +23 -0
  8. data/README.md +168 -222
  9. data/Rakefile +5 -11
  10. data/docs/notifiers/campfire.md +50 -0
  11. data/docs/notifiers/custom.md +42 -0
  12. data/docs/notifiers/datadog.md +51 -0
  13. data/docs/notifiers/email.md +195 -0
  14. data/docs/notifiers/google_chat.md +31 -0
  15. data/docs/notifiers/hipchat.md +66 -0
  16. data/docs/notifiers/irc.md +97 -0
  17. data/docs/notifiers/mattermost.md +115 -0
  18. data/docs/notifiers/slack.md +161 -0
  19. data/docs/notifiers/sns.md +37 -0
  20. data/docs/notifiers/teams.md +54 -0
  21. data/docs/notifiers/webhook.md +60 -0
  22. data/examples/sample_app.rb +54 -0
  23. data/examples/sinatra/Gemfile +8 -0
  24. data/examples/sinatra/Gemfile.lock +95 -0
  25. data/examples/sinatra/Procfile +2 -0
  26. data/examples/sinatra/README.md +11 -0
  27. data/examples/sinatra/config.ru +3 -0
  28. data/examples/sinatra/sinatra_app.rb +36 -0
  29. data/exception_notification.gemspec +32 -11
  30. data/gemfiles/rails4_0.gemfile +7 -0
  31. data/gemfiles/rails4_1.gemfile +7 -0
  32. data/gemfiles/rails4_2.gemfile +7 -0
  33. data/gemfiles/rails5_0.gemfile +7 -0
  34. data/gemfiles/rails5_1.gemfile +7 -0
  35. data/gemfiles/rails5_2.gemfile +7 -0
  36. data/gemfiles/rails6_0.gemfile +7 -0
  37. data/lib/exception_notification.rb +11 -0
  38. data/lib/exception_notification/rack.rb +55 -0
  39. data/lib/exception_notification/rails.rb +9 -0
  40. data/lib/exception_notification/resque.rb +22 -0
  41. data/lib/exception_notification/sidekiq.rb +27 -0
  42. data/lib/exception_notification/version.rb +3 -0
  43. data/lib/exception_notifier.rb +137 -61
  44. data/lib/exception_notifier/base_notifier.rb +24 -0
  45. data/lib/exception_notifier/campfire_notifier.rb +16 -11
  46. data/lib/exception_notifier/datadog_notifier.rb +153 -0
  47. data/lib/exception_notifier/email_notifier.rb +196 -0
  48. data/lib/exception_notifier/google_chat_notifier.rb +42 -0
  49. data/lib/exception_notifier/hipchat_notifier.rb +49 -0
  50. data/lib/exception_notifier/irc_notifier.rb +57 -0
  51. data/lib/exception_notifier/mattermost_notifier.rb +72 -0
  52. data/lib/exception_notifier/modules/backtrace_cleaner.rb +11 -0
  53. data/lib/exception_notifier/modules/error_grouping.rb +77 -0
  54. data/lib/exception_notifier/modules/formatter.rb +118 -0
  55. data/lib/exception_notifier/notifier.rb +9 -179
  56. data/lib/exception_notifier/slack_notifier.rb +111 -0
  57. data/lib/exception_notifier/sns_notifier.rb +85 -0
  58. data/lib/exception_notifier/teams_notifier.rb +193 -0
  59. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -1
  60. data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -1
  61. data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +8 -6
  62. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -4
  63. data/lib/exception_notifier/views/exception_notifier/_request.html.erb +36 -5
  64. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +10 -5
  65. data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -2
  66. data/lib/exception_notifier/views/exception_notifier/_session.text.erb +2 -2
  67. data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -3
  68. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +38 -11
  69. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +10 -11
  70. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +38 -22
  71. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -3
  72. data/lib/exception_notifier/webhook_notifier.rb +51 -0
  73. data/lib/generators/exception_notification/install_generator.rb +15 -0
  74. data/lib/generators/exception_notification/templates/exception_notification.rb.erb +55 -0
  75. data/test/exception_notification/rack_test.rb +60 -0
  76. data/test/exception_notification/resque_test.rb +52 -0
  77. data/test/exception_notifier/campfire_notifier_test.rb +120 -0
  78. data/test/exception_notifier/datadog_notifier_test.rb +151 -0
  79. data/test/exception_notifier/email_notifier_test.rb +351 -0
  80. data/test/exception_notifier/google_chat_notifier_test.rb +181 -0
  81. data/test/exception_notifier/hipchat_notifier_test.rb +218 -0
  82. data/test/exception_notifier/irc_notifier_test.rb +137 -0
  83. data/test/exception_notifier/mattermost_notifier_test.rb +202 -0
  84. data/test/exception_notifier/modules/error_grouping_test.rb +165 -0
  85. data/test/exception_notifier/modules/formatter_test.rb +150 -0
  86. data/test/exception_notifier/sidekiq_test.rb +38 -0
  87. data/test/exception_notifier/slack_notifier_test.rb +227 -0
  88. data/test/exception_notifier/sns_notifier_test.rb +121 -0
  89. data/test/exception_notifier/teams_notifier_test.rb +90 -0
  90. data/test/exception_notifier/webhook_notifier_test.rb +96 -0
  91. data/test/exception_notifier_test.rb +182 -0
  92. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  93. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  94. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  95. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  96. data/test/test_helper.rb +12 -8
  97. metadata +333 -164
  98. data/.gemtest +0 -0
  99. data/Gemfile.lock +0 -122
  100. data/test/background_exception_notification_test.rb +0 -82
  101. data/test/campfire_test.rb +0 -53
  102. data/test/dummy/.gitignore +0 -4
  103. data/test/dummy/Gemfile +0 -33
  104. data/test/dummy/Gemfile.lock +0 -118
  105. data/test/dummy/Rakefile +0 -7
  106. data/test/dummy/app/controllers/application_controller.rb +0 -3
  107. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  110. data/test/dummy/app/models/post.rb +0 -2
  111. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  112. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  113. data/test/dummy/app/views/posts/new.html.erb +0 -0
  114. data/test/dummy/app/views/posts/show.html.erb +0 -0
  115. data/test/dummy/config.ru +0 -4
  116. data/test/dummy/config/application.rb +0 -42
  117. data/test/dummy/config/boot.rb +0 -6
  118. data/test/dummy/config/database.yml +0 -22
  119. data/test/dummy/config/environment.rb +0 -13
  120. data/test/dummy/config/environments/development.rb +0 -24
  121. data/test/dummy/config/environments/production.rb +0 -49
  122. data/test/dummy/config/environments/test.rb +0 -35
  123. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  124. data/test/dummy/config/initializers/inflections.rb +0 -10
  125. data/test/dummy/config/initializers/mime_types.rb +0 -5
  126. data/test/dummy/config/initializers/secret_token.rb +0 -7
  127. data/test/dummy/config/initializers/session_store.rb +0 -8
  128. data/test/dummy/config/locales/en.yml +0 -5
  129. data/test/dummy/config/routes.rb +0 -3
  130. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  131. data/test/dummy/db/schema.rb +0 -24
  132. data/test/dummy/db/seeds.rb +0 -7
  133. data/test/dummy/lib/tasks/.gitkeep +0 -0
  134. data/test/dummy/public/404.html +0 -26
  135. data/test/dummy/public/422.html +0 -26
  136. data/test/dummy/public/500.html +0 -26
  137. data/test/dummy/public/favicon.ico +0 -0
  138. data/test/dummy/public/images/rails.png +0 -0
  139. data/test/dummy/public/index.html +0 -239
  140. data/test/dummy/public/javascripts/application.js +0 -2
  141. data/test/dummy/public/javascripts/controls.js +0 -965
  142. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  143. data/test/dummy/public/javascripts/effects.js +0 -1123
  144. data/test/dummy/public/javascripts/prototype.js +0 -6001
  145. data/test/dummy/public/javascripts/rails.js +0 -191
  146. data/test/dummy/public/robots.txt +0 -5
  147. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  148. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  149. data/test/dummy/script/rails +0 -6
  150. data/test/dummy/test/fixtures/posts.yml +0 -11
  151. data/test/dummy/test/functional/posts_controller_test.rb +0 -239
  152. data/test/dummy/test/test_helper.rb +0 -13
  153. data/test/exception_notification_test.rb +0 -73
@@ -0,0 +1,196 @@
1
+ require 'active_support/core_ext/time'
2
+ require 'action_mailer'
3
+ require 'action_dispatch'
4
+ require 'pp'
5
+
6
+ module ExceptionNotifier
7
+ class EmailNotifier < BaseNotifier
8
+ DEFAULT_OPTIONS = {
9
+ sender_address: %("Exception Notifier" <exception.notifier@example.com>),
10
+ exception_recipients: [],
11
+ email_prefix: '[ERROR] ',
12
+ email_format: :text,
13
+ sections: %w[request session environment backtrace],
14
+ background_sections: %w[backtrace data],
15
+ verbose_subject: true,
16
+ normalize_subject: false,
17
+ include_controller_and_action_names_in_subject: true,
18
+ delivery_method: nil,
19
+ mailer_settings: nil,
20
+ email_headers: {},
21
+ mailer_parent: 'ActionMailer::Base',
22
+ template_path: 'exception_notifier',
23
+ deliver_with: nil
24
+ }.freeze
25
+
26
+ module Mailer
27
+ class MissingController
28
+ def method_missing(*args, &block); end
29
+ end
30
+
31
+ def self.extended(base)
32
+ base.class_eval do
33
+ send(:include, ExceptionNotifier::BacktraceCleaner)
34
+
35
+ # Append application view path to the ExceptionNotifier lookup context.
36
+ append_view_path "#{File.dirname(__FILE__)}/views"
37
+
38
+ def exception_notification(env, exception, options = {}, default_options = {})
39
+ load_custom_views
40
+
41
+ @env = env
42
+ @exception = exception
43
+
44
+ env_options = env['exception_notifier.options'] || {}
45
+ @options = default_options.merge(env_options).merge(options)
46
+
47
+ @kontroller = env['action_controller.instance'] || MissingController.new
48
+ @request = ActionDispatch::Request.new(env)
49
+ @backtrace = exception.backtrace ? clean_backtrace(exception) : []
50
+ @timestamp = Time.current
51
+ @sections = @options[:sections]
52
+ @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
53
+ @sections += %w[data] unless @data.empty?
54
+
55
+ compose_email
56
+ end
57
+
58
+ def background_exception_notification(exception, options = {}, default_options = {})
59
+ load_custom_views
60
+
61
+ @exception = exception
62
+ @options = default_options.merge(options).symbolize_keys
63
+ @backtrace = exception.backtrace || []
64
+ @timestamp = Time.current
65
+ @sections = @options[:background_sections]
66
+ @data = options[:data] || {}
67
+ @env = @kontroller = nil
68
+
69
+ compose_email
70
+ end
71
+
72
+ private
73
+
74
+ def compose_subject
75
+ subject = @options[:email_prefix].to_s.dup
76
+ 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]
78
+ subject << " (#{@exception.class})"
79
+ subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
80
+ subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
81
+ subject.length > 120 ? subject[0...120] + '...' : subject
82
+ end
83
+
84
+ def set_data_variables
85
+ @data.each do |name, value|
86
+ instance_variable_set("@#{name}", value)
87
+ end
88
+ end
89
+
90
+ helper_method :inspect_object
91
+
92
+ def truncate(string, max)
93
+ string.length > max ? "#{string[0...max]}..." : string
94
+ end
95
+
96
+ def inspect_object(object)
97
+ case object
98
+ when Hash, Array
99
+ truncate(object.inspect, 300)
100
+ else
101
+ object.to_s
102
+ end
103
+ end
104
+
105
+ helper_method :safe_encode
106
+
107
+ def safe_encode(value)
108
+ value.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
109
+ end
110
+
111
+ def html_mail?
112
+ @options[:email_format] == :html
113
+ end
114
+
115
+ def compose_email
116
+ set_data_variables
117
+ subject = compose_subject
118
+ name = @env.nil? ? 'background_exception_notification' : 'exception_notification'
119
+ exception_recipients = maybe_call(@options[:exception_recipients])
120
+
121
+ headers = {
122
+ delivery_method: @options[:delivery_method],
123
+ to: exception_recipients,
124
+ from: @options[:sender_address],
125
+ subject: subject,
126
+ template_name: name
127
+ }.merge(@options[:email_headers])
128
+
129
+ mail = mail(headers) do |format|
130
+ format.text
131
+ format.html if html_mail?
132
+ end
133
+
134
+ mail.delivery_method.settings.merge!(@options[:mailer_settings]) if @options[:mailer_settings]
135
+
136
+ mail
137
+ end
138
+
139
+ def load_custom_views
140
+ prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views" if defined?(Rails) && Rails.respond_to?(:root)
141
+ end
142
+
143
+ def maybe_call(maybe_proc)
144
+ maybe_proc.respond_to?(:call) ? maybe_proc.call : maybe_proc
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def initialize(options)
151
+ super
152
+
153
+ delivery_method = (options[:delivery_method] || :smtp)
154
+ mailer_settings_key = "#{delivery_method}_settings".to_sym
155
+ options[:mailer_settings] = options.delete(mailer_settings_key)
156
+
157
+ @base_options = DEFAULT_OPTIONS.merge(options)
158
+ end
159
+
160
+ def call(exception, options = {})
161
+ message = create_email(exception, options)
162
+
163
+ message.send(base_options[:deliver_with] || default_deliver_with(message))
164
+ end
165
+
166
+ def create_email(exception, options = {})
167
+ env = options[:env]
168
+
169
+ send_notice(exception, options, nil, base_options) do |_, default_opts|
170
+ if env.nil?
171
+ mailer.background_exception_notification(exception, options, default_opts)
172
+ else
173
+ mailer.exception_notification(env, exception, options, default_opts)
174
+ end
175
+ end
176
+ end
177
+
178
+ def self.normalize_digits(string)
179
+ string.gsub(/[0-9]+/, 'N')
180
+ end
181
+
182
+ private
183
+
184
+ def mailer
185
+ @mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer|
186
+ mailer.extend(EmailNotifier::Mailer)
187
+ mailer.mailer_name = base_options[:template_path]
188
+ end
189
+ end
190
+
191
+ def default_deliver_with(message)
192
+ # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
193
+ message.respond_to?(:deliver_now) ? :deliver_now : :deliver
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,42 @@
1
+ require 'httparty'
2
+
3
+ module ExceptionNotifier
4
+ class GoogleChatNotifier < BaseNotifier
5
+ def call(exception, opts = {})
6
+ options = base_options.merge(opts)
7
+ formatter = Formatter.new(exception, options)
8
+
9
+ HTTParty.post(
10
+ options[:webhook_url],
11
+ body: { text: body(exception, formatter) }.to_json,
12
+ headers: { 'Content-Type' => 'application/json' }
13
+ )
14
+ end
15
+
16
+ private
17
+
18
+ def body(exception, formatter)
19
+ text = [
20
+ "\nApplication: *#{formatter.app_name}*",
21
+ formatter.subtitle,
22
+ '',
23
+ formatter.title,
24
+ "*#{exception.message.tr('`', "'")}*"
25
+ ]
26
+
27
+ if (request = formatter.request_message.presence)
28
+ text << ''
29
+ text << '*Request:*'
30
+ text << request
31
+ end
32
+
33
+ if (backtrace = formatter.backtrace_message.presence)
34
+ text << ''
35
+ text << '*Backtrace:*'
36
+ text << backtrace
37
+ end
38
+
39
+ text.compact.join("\n")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ module ExceptionNotifier
2
+ class HipchatNotifier < BaseNotifier
3
+ attr_accessor :from
4
+ attr_accessor :room
5
+ attr_accessor :message_options
6
+
7
+ def initialize(options)
8
+ super
9
+ begin
10
+ api_token = options.delete(:api_token)
11
+ room_name = options.delete(:room_name)
12
+ opts = {
13
+ api_version: options.delete(:api_version) || 'v1'
14
+ }
15
+ opts[:server_url] = options.delete(:server_url) if options[:server_url]
16
+ @from = options.delete(:from) || 'Exception'
17
+ @room = HipChat::Client.new(api_token, opts)[room_name]
18
+ @message_template = options.delete(:message_template) || lambda { |exception, errors_count|
19
+ msg = if errors_count > 1
20
+ "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'"
21
+ else
22
+ "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'"
23
+ end
24
+ msg += " on '#{exception.backtrace.first}'" if exception.backtrace
25
+ msg
26
+ }
27
+ @message_options = options
28
+ @message_options[:color] ||= 'red'
29
+ rescue StandardError
30
+ @room = nil
31
+ end
32
+ end
33
+
34
+ def call(exception, options = {})
35
+ return unless active?
36
+
37
+ message = @message_template.call(exception, options[:accumulated_errors_count].to_i)
38
+ send_notice(exception, options, message, @message_options) do |msg, message_opts|
39
+ @room.send(@from, msg, message_opts)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def active?
46
+ !@room.nil?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ module ExceptionNotifier
2
+ class IrcNotifier < BaseNotifier
3
+ def initialize(options)
4
+ super
5
+ @config = OpenStruct.new
6
+ parse_options(options)
7
+ end
8
+
9
+ def call(exception, options = {})
10
+ errors_count = options[:accumulated_errors_count].to_i
11
+
12
+ message = "'#{exception.message}'"
13
+ message.prepend("(#{errors_count} times)") if errors_count > 1
14
+
15
+ message += " on '#{exception.backtrace.first}'" if exception.backtrace
16
+
17
+ return unless active?
18
+
19
+ send_notice(exception, options, message) do |msg, _|
20
+ send_message([*@config.prefix, *msg].join(' '))
21
+ end
22
+ end
23
+
24
+ def send_message(message)
25
+ CarrierPigeon.send @config.irc.merge(message: message)
26
+ end
27
+
28
+ private
29
+
30
+ def parse_options(options)
31
+ nick = options.fetch(:nick, 'ExceptionNotifierBot')
32
+ password = options[:password] ? ":#{options[:password]}" : nil
33
+ domain = options.fetch(:domain, nil)
34
+ port = options[:port] ? ":#{options[:port]}" : nil
35
+ channel = options.fetch(:channel, '#log')
36
+ notice = options.fetch(:notice, false)
37
+ ssl = options.fetch(:ssl, false)
38
+ join = options.fetch(:join, false)
39
+ uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}"
40
+ prefix = options.fetch(:prefix, nil)
41
+ recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil
42
+
43
+ @config.prefix = [*prefix, *recipients].join(' ')
44
+ @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join }
45
+ end
46
+
47
+ def active?
48
+ valid_uri? @config.irc[:uri]
49
+ end
50
+
51
+ def valid_uri?(uri)
52
+ URI.parse(uri)
53
+ rescue URI::InvalidURIError
54
+ false
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,72 @@
1
+ require 'httparty'
2
+
3
+ module ExceptionNotifier
4
+ class MattermostNotifier < BaseNotifier
5
+ def call(exception, opts = {})
6
+ options = opts.merge(base_options)
7
+ @exception = exception
8
+
9
+ @formatter = Formatter.new(exception, options)
10
+
11
+ @gitlab_url = options[:git_url]
12
+
13
+ payload = {
14
+ text: message_text.compact.join("\n"),
15
+ username: options[:username] || 'Exception Notifier'
16
+ }
17
+
18
+ payload[:icon_url] = options[:avatar] if options[:avatar]
19
+ payload[:channel] = options[:channel] if options[:channel]
20
+
21
+ httparty_options = options.except(
22
+ :avatar, :channel, :username, :git_url, :webhook_url,
23
+ :env, :accumulated_errors_count, :app_name
24
+ )
25
+
26
+ httparty_options[:body] = payload.to_json
27
+ httparty_options[:headers] ||= {}
28
+ httparty_options[:headers]['Content-Type'] = 'application/json'
29
+
30
+ HTTParty.post(options[:webhook_url], httparty_options)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :formatter
36
+
37
+ def message_text
38
+ text = [
39
+ '@channel',
40
+ "### #{formatter.title}",
41
+ formatter.subtitle,
42
+ "*#{@exception.message}*"
43
+ ]
44
+
45
+ if (request = formatter.request_message.presence)
46
+ text << '### Request'
47
+ text << request
48
+ end
49
+
50
+ if (backtrace = formatter.backtrace_message.presence)
51
+ text << '### Backtrace'
52
+ text << backtrace
53
+ end
54
+
55
+ text << message_issue_link if @gitlab_url
56
+
57
+ text
58
+ end
59
+
60
+ def message_issue_link
61
+ link = [@gitlab_url, formatter.app_name, 'issues', 'new'].join('/')
62
+ params = {
63
+ 'issue[title]' => ['[BUG] Error 500 :',
64
+ formatter.controller_and_action || '',
65
+ "(#{@exception.class})",
66
+ @exception.message].compact.join(' ')
67
+ }.to_query
68
+
69
+ "[Create an issue](#{link}/?#{params})"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,11 @@
1
+ module ExceptionNotifier
2
+ module BacktraceCleaner
3
+ def clean_backtrace(exception)
4
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
5
+ Rails.backtrace_cleaner.send(:filter, exception.backtrace)
6
+ else
7
+ exception.backtrace
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,77 @@
1
+ require 'active_support/core_ext/numeric/time'
2
+ require 'active_support/concern'
3
+
4
+ module ExceptionNotifier
5
+ module ErrorGrouping
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ mattr_accessor :error_grouping
10
+ self.error_grouping = false
11
+
12
+ mattr_accessor :error_grouping_period
13
+ self.error_grouping_period = 5.minutes
14
+
15
+ mattr_accessor :notification_trigger
16
+
17
+ mattr_accessor :error_grouping_cache
18
+ end
19
+
20
+ module ClassMethods
21
+ # Fallback to the memory store while the specified cache store doesn't work
22
+ #
23
+ def fallback_cache_store
24
+ @fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new
25
+ end
26
+
27
+ 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
34
+
35
+ count.to_i if count
36
+ end
37
+
38
+ def save_error_count(error_key, count)
39
+ error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
40
+ rescue StandardError => e
41
+ ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to write, reason: #{e.message}. Falling back to memory cache store.")
42
+ fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
43
+ end
44
+
45
+ def group_error!(exception, options)
46
+ message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
47
+ accumulated_errors_count = 1
48
+
49
+ if (count = error_count(message_based_key))
50
+ accumulated_errors_count = count + 1
51
+ save_error_count(message_based_key, accumulated_errors_count)
52
+ else
53
+ backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
54
+
55
+ if (count = error_grouping_cache.read(backtrace_based_key))
56
+ accumulated_errors_count = count + 1
57
+ save_error_count(backtrace_based_key, accumulated_errors_count)
58
+ else
59
+ save_error_count(backtrace_based_key, accumulated_errors_count)
60
+ save_error_count(message_based_key, accumulated_errors_count)
61
+ end
62
+ end
63
+
64
+ options[:accumulated_errors_count] = accumulated_errors_count
65
+ end
66
+
67
+ def send_notification?(exception, count)
68
+ if notification_trigger.respond_to?(:call)
69
+ notification_trigger.call(exception, count)
70
+ else
71
+ factor = Math.log2(count)
72
+ factor.to_i == factor
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end