exception_notification 4.1.4 → 4.2.0.rc1

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.
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/hash/reverse_merge"
2
+ require 'active_support/core_ext/time'
2
3
  require 'action_mailer'
3
4
  require 'action_dispatch'
4
5
  require 'pp'
@@ -33,6 +34,7 @@ module ExceptionNotifier
33
34
  @kontroller = env['action_controller.instance'] || MissingController.new
34
35
  @request = ActionDispatch::Request.new(env)
35
36
  @backtrace = exception.backtrace ? clean_backtrace(exception) : []
37
+ @timestamp = Time.current
36
38
  @sections = @options[:sections]
37
39
  @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
38
40
  @sections = @sections + %w(data) unless @data.empty?
@@ -46,8 +48,10 @@ module ExceptionNotifier
46
48
  @exception = exception
47
49
  @options = options.reverse_merge(default_options)
48
50
  @backtrace = exception.backtrace || []
51
+ @timestamp = Time.current
49
52
  @sections = @options[:background_sections]
50
53
  @data = options[:data] || {}
54
+ @env = @kontroller = nil
51
55
 
52
56
  compose_email
53
57
  end
@@ -94,10 +98,11 @@ module ExceptionNotifier
94
98
  set_data_variables
95
99
  subject = compose_subject
96
100
  name = @env.nil? ? 'background_exception_notification' : 'exception_notification'
101
+ exception_recipients = maybe_call(@options[:exception_recipients])
97
102
 
98
103
  headers = {
99
104
  :delivery_method => @options[:delivery_method],
100
- :to => @options[:exception_recipients],
105
+ :to => exception_recipients,
101
106
  :from => @options[:sender_address],
102
107
  :subject => subject,
103
108
  :template_name => name
@@ -118,6 +123,10 @@ module ExceptionNotifier
118
123
  self.prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views"
119
124
  end
120
125
  end
126
+
127
+ def maybe_call(maybe_proc)
128
+ maybe_proc.respond_to?(:call) ? maybe_proc.call : maybe_proc
129
+ end
121
130
  end
122
131
  end
123
132
  end
@@ -150,7 +159,18 @@ module ExceptionNotifier
150
159
  end
151
160
 
152
161
  def call(exception, options={})
153
- create_email(exception, options).send(deliver_with)
162
+ message = create_email(exception, options)
163
+
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
154
174
  end
155
175
 
156
176
  def create_email(exception, options={})
@@ -182,7 +202,7 @@ module ExceptionNotifier
182
202
  :email_headers => {},
183
203
  :mailer_parent => 'ActionMailer::Base',
184
204
  :template_path => 'exception_notifier',
185
- :deliver_with => :deliver_now
205
+ :deliver_with => :default
186
206
  }
187
207
  end
188
208
 
@@ -0,0 +1,159 @@
1
+ require 'action_dispatch'
2
+ require 'active_support/core_ext/time'
3
+
4
+ module ExceptionNotifier
5
+ class MattermostNotifier
6
+ include ExceptionNotifier::BacktraceCleaner
7
+
8
+ attr_accessor :httparty
9
+
10
+ def initialize(options = {})
11
+ super()
12
+ @default_options = options
13
+ @httparty = HTTParty
14
+ end
15
+
16
+ def call(exception, options = {})
17
+ @options = options.merge(@default_options)
18
+ @exception = exception
19
+ @backtrace = exception.backtrace ? clean_backtrace(exception) : nil
20
+
21
+ @env = @options.delete(:env)
22
+
23
+ @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore
24
+ @gitlab_url = @options.delete(:git_url)
25
+ @username = @options.delete(:username) || "Exception Notifier"
26
+ @avatar = @options.delete(:avatar)
27
+
28
+ @channel = @options.delete(:channel)
29
+ @webhook_url = @options.delete(:webhook_url)
30
+ raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url
31
+
32
+ unless @env.nil?
33
+ @controller = @env['action_controller.instance'] || MissingController.new
34
+
35
+ request = ActionDispatch::Request.new(@env)
36
+
37
+ @request_items = { url: request.original_url,
38
+ http_method: request.method,
39
+ ip_address: request.remote_ip,
40
+ parameters: request.filtered_parameters,
41
+ timestamp: Time.current }
42
+
43
+ if request.session["warden.user.user.key"]
44
+ current_user = User.find(request.session["warden.user.user.key"][0][0])
45
+ @request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } })
46
+ end
47
+ else
48
+ @controller = @request_items = nil
49
+ end
50
+
51
+ payload = message_text.merge(user_info).merge(channel_info)
52
+
53
+ @options[:body] = payload.to_json
54
+ @options[:headers] ||= {}
55
+ @options[:headers].merge!({ 'Content-Type' => 'application/json' })
56
+
57
+ @httparty.post(@webhook_url, @options)
58
+ end
59
+
60
+ private
61
+
62
+ def channel_info
63
+ if @channel
64
+ { channel: @channel }
65
+ else
66
+ {}
67
+ end
68
+ end
69
+
70
+ def user_info
71
+ infos = {}
72
+
73
+ infos.merge!({ username: @username }) if @username
74
+ infos.merge!({ icon_url: @avatar }) if @avatar
75
+
76
+ infos
77
+ end
78
+
79
+ def message_text
80
+ text = []
81
+
82
+ text += ["@channel"]
83
+ text += message_header
84
+ text += message_request if @request_items
85
+ text += message_backtrace if @backtrace
86
+ text += message_issue_link if @gitlab_url
87
+
88
+ { text: text.join("\n") }
89
+ end
90
+
91
+ def message_header
92
+ text = []
93
+
94
+ text << "### :warning: Error 500 in #{Rails.env} :warning:"
95
+ text << "An *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end
96
+ text << "*#{@exception.message}*"
97
+
98
+ text
99
+ end
100
+
101
+ def message_request
102
+ text = []
103
+
104
+ text << "### Request"
105
+ text << "```"
106
+ text << hash_presentation(@request_items)
107
+ text << "```"
108
+
109
+ text
110
+ end
111
+
112
+ def message_backtrace(size = 3)
113
+ text = []
114
+
115
+ size = @backtrace.size < size ? @backtrace.size : size
116
+ text << "### Backtrace"
117
+ text << "```"
118
+ size.times { |i| text << "* " + @backtrace[i] }
119
+ text << "```"
120
+
121
+ text
122
+ end
123
+
124
+ def message_issue_link
125
+ text = []
126
+
127
+ link = [@gitlab_url, @application_name, "issues", "new"].join("/")
128
+ params = {
129
+ "issue[title]" => ["[BUG] Error 500 :",
130
+ controller_and_method,
131
+ "(#{@exception.class})",
132
+ @exception.message].compact.join(" ")
133
+ }.to_query
134
+
135
+ text << "[Create an issue](#{link}/?#{params})"
136
+
137
+ text
138
+ end
139
+
140
+ def controller_and_method
141
+ if @controller
142
+ "#{@controller.controller_name}##{@controller.action_name}"
143
+ else
144
+ ""
145
+ end
146
+ end
147
+
148
+ def hash_presentation(hash)
149
+ text = []
150
+
151
+ hash.each do |key, value|
152
+ text << "* #{key} : #{value}"
153
+ end
154
+
155
+ text.join("\n")
156
+ end
157
+
158
+ end
159
+ end
@@ -26,8 +26,10 @@ module ExceptionNotifier
26
26
  clean_message = exception.message.gsub("`", "'")
27
27
  fields = [ { title: 'Exception', value: clean_message} ]
28
28
 
29
+ fields.push({ title: 'Hostname', value: Socket.gethostname })
30
+
29
31
  if exception.backtrace
30
- formatted_backtrace = "```#{exception.backtrace.first(5).join("\n")}```"
32
+ formatted_backtrace = "```#{exception.backtrace.join("\n")}```"
31
33
  fields.push({ title: 'Backtrace', value: formatted_backtrace })
32
34
  end
33
35
 
@@ -40,7 +42,7 @@ module ExceptionNotifier
40
42
  attchs = [color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields)]
41
43
 
42
44
  if valid?
43
- send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts|
45
+ send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts|
44
46
  @notifier.ping '', message_opts
45
47
  end
46
48
  end
@@ -17,7 +17,7 @@
17
17
  </li>
18
18
  <li>
19
19
  <strong>Timestamp:</strong>
20
- <span><%= Time.current %></span>
20
+ <span><%= @timestamp %></span>
21
21
  </li>
22
22
  <li>
23
23
  <strong>Server:</strong>
@@ -2,7 +2,7 @@
2
2
  * HTTP Method: <%= raw @request.request_method %>
3
3
  * IP address : <%= raw @request.remote_ip %>
4
4
  * Parameters : <%= raw safe_encode @request.filtered_parameters.inspect %>
5
- * Timestamp : <%= raw Time.current %>
5
+ * Timestamp : <%= raw @timestamp %>
6
6
  * Server : <%= raw Socket.gethostname %>
7
7
  <% if defined?(Rails) && Rails.respond_to?(:root) %>
8
8
  * Rails root : <%= raw Rails.root %>
@@ -30,7 +30,7 @@
30
30
  <tr>
31
31
  <td style="padding: 10px; border: 1px solid #eed3d7; background-color: #f2dede">
32
32
  <h3 style="color: #b94a48">
33
- <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= Time.current %> :
33
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= @timestamp %> :
34
34
  </h3>
35
35
  <p style="color: #b94a48"><%= @exception.message %></p>
36
36
  <pre style="font-size: 12px; padding: 5px; background-color:#f5f5f5">
@@ -1,4 +1,4 @@
1
- <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= raw Time.current %> :
1
+ <%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= raw @timestamp %> :
2
2
 
3
3
  <%= @exception.message %>
4
4
  <%= @backtrace.first %>
@@ -1,4 +1,5 @@
1
1
  require 'action_dispatch'
2
+ require 'active_support/core_ext/time'
2
3
 
3
4
  module ExceptionNotifier
4
5
  class WebhookNotifier < BaseNotifier
@@ -5,8 +5,7 @@ class PostsControllerTest < ActionController::TestCase
5
5
  Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00')
6
6
  @email_notifier = ExceptionNotifier.registered_exception_notifier(:email)
7
7
  begin
8
- @post = posts(:one)
9
- post :create, :post => @post.attributes
8
+ post :create, method: :post, params: { secret: "secret" }
10
9
  rescue => e
11
10
  @exception = e
12
11
  @mail = @email_notifier.create_email(@exception, {:env => request.env, :data => {:message => 'My Custom Message'}})
@@ -71,23 +70,22 @@ class PostsControllerTest < ActionController::TestCase
71
70
 
72
71
  test "should not send notification if one of ignored exceptions" do
73
72
  begin
74
- get :show, :id => @post.to_param + "10"
73
+ get :invalid
75
74
  rescue => e
76
75
  @ignored_exception = e
77
76
  unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name)
78
- @ignored_mail = @email_notifier.create_email(@ignored_exception, {:env => request.env})
77
+ ignored_mail = @email_notifier.create_email(@ignored_exception, {:env => request.env})
79
78
  end
80
79
  end
81
80
 
82
- assert_equal @ignored_exception.class.inspect, "ActiveRecord::RecordNotFound"
83
- assert_nil @ignored_mail
81
+ assert_equal @ignored_exception.class.inspect, "ActionController::UrlGenerationError"
82
+ assert_nil ignored_mail
84
83
  end
85
84
 
86
85
  test "should filter session_id on secure requests" do
87
86
  request.env['HTTPS'] = 'on'
88
87
  begin
89
- @post = posts(:one)
90
- post :create, :post => @post.attributes
88
+ post :create, method: :post
91
89
  rescue => e
92
90
  @secured_mail = @email_notifier.create_email(e, {:env => request.env})
93
91
  end
@@ -99,8 +97,7 @@ class PostsControllerTest < ActionController::TestCase
99
97
  test "should ignore exception if from unwanted crawler" do
100
98
  request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
101
99
  begin
102
- @post = posts(:one)
103
- post :create, :post => @post.attributes
100
+ post :create, method: :post
104
101
  rescue => e
105
102
  @exception = e
106
103
  custom_env = request.env
@@ -108,17 +105,16 @@ class PostsControllerTest < ActionController::TestCase
108
105
  custom_env['exception_notifier.options'].merge!(:ignore_crawlers => %w(Googlebot))
109
106
  ignore_array = custom_env['exception_notifier.options'][:ignore_crawlers]
110
107
  unless ExceptionNotification::Rack.new(Dummy::Application, custom_env['exception_notifier.options']).send(:from_crawler, custom_env, ignore_array)
111
- @ignored_mail = @email_notifier.create_email(@exception, {:env => custom_env})
108
+ ignored_mail = @email_notifier.create_email(@exception, {:env => custom_env})
112
109
  end
113
110
  end
114
111
 
115
- assert_nil @ignored_mail
112
+ assert_nil ignored_mail
116
113
  end
117
114
 
118
115
  test "should send html email when selected html format" do
119
116
  begin
120
- @post = posts(:one)
121
- post :create, :post => @post.attributes
117
+ post :create, method: :post
122
118
  rescue => e
123
119
  @exception = e
124
120
  custom_env = request.env
@@ -136,8 +132,7 @@ class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase
136
132
  setup do
137
133
  @email_notifier = ExceptionNotifier::EmailNotifier.new(:verbose_subject => false)
138
134
  begin
139
- @post = posts(:one)
140
- post :create, :post => @post.attributes
135
+ post :create, method: :post
141
136
  rescue => e
142
137
  @exception = e
143
138
  @mail = @email_notifier.create_email(@exception, {:env => request.env})
@@ -145,7 +140,9 @@ class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase
145
140
  end
146
141
 
147
142
  test "should not include exception message in subject" do
148
- assert_equal "[ERROR] # (NoMethodError)", @mail.subject
143
+ assert_includes @mail.subject, '[ERROR]'
144
+ assert_includes @mail.subject, '(NoMethodError)'
145
+ refute_includes @mail.subject, 'undefined method'
149
146
  end
150
147
  end
151
148
 
@@ -158,8 +155,7 @@ class PostsControllerTestWithSmtpSettings < ActionController::TestCase
158
155
  })
159
156
 
160
157
  begin
161
- @post = posts(:one)
162
- post :create, :post => @post.attributes
158
+ post :create, method: :post
163
159
  rescue => e
164
160
  @exception = e
165
161
  @mail = @email_notifier.create_email(@exception, {:env => request.env})
@@ -178,47 +174,45 @@ class PostsControllerTestWithSmtpSettings < ActionController::TestCase
178
174
  end
179
175
  end
180
176
 
181
- class PostsControllerTestBadRequestData < ActionController::TestCase
177
+ class PostsControllerTestBackgroundNotification < ActionController::TestCase
182
178
  tests PostsController
183
179
  setup do
184
180
  @email_notifier = ExceptionNotifier.registered_exception_notifier(:email)
185
181
  begin
186
- # This might seem synthetic, but the point is that the data used by
187
- # ExceptionNotification could be rendered "invalid" by e.g. a badly
188
- # behaving middleware, and we want to test that ExceptionNotification
189
- # still manages to send off an email in those cases.
190
- #
191
- # The trick here is to trigger an exception in the template used by
192
- # ExceptionNotification. (The original test stuffed request.env with
193
- # badly encoded strings, but that only works in Ruby 1.9+.)
194
- request.send :instance_variable_set, :@env, {}
195
-
196
- @post = posts(:one)
197
- post :create, :post => @post.attributes
198
- rescue => e
199
- @exception = e
200
- @mail = @email_notifier.create_email(@exception, {:env => request.env})
182
+ post :create, method: :post
183
+ rescue => exception
184
+ @mail = @email_notifier.create_email(exception)
201
185
  end
202
186
  end
203
187
 
204
- test "should include error message in body" do
205
- assert_match /ERROR: Failed to generate exception summary/, @mail.encoded.to_s
188
+ test "mail should contain the specified section" do
189
+ assert_includes @mail.encoded, "* New background section for testing"
206
190
  end
207
191
  end
208
192
 
209
- class PostsControllerTestBackgroundNotification < ActionController::TestCase
193
+ class PostsControllerTestWithExceptionRecipientsAsProc < ActionController::TestCase
210
194
  tests PostsController
211
195
  setup do
212
- @email_notifier = ExceptionNotifier.registered_exception_notifier(:email)
213
- begin
214
- @post = posts(:one)
215
- post :create, :post => @post.attributes
216
- rescue => exception
217
- @mail = @email_notifier.create_email(exception)
196
+ exception_recipients = %w{first@example.com second@example.com}
197
+
198
+ @email_notifier = ExceptionNotifier::EmailNotifier.new(
199
+ exception_recipients: -> { [ exception_recipients.shift ] }
200
+ )
201
+
202
+ @action = proc do
203
+ begin
204
+ post :create, method: :post
205
+ rescue => e
206
+ @exception = e
207
+ @mail = @email_notifier.create_email(@exception, {:env => request.env})
208
+ end
218
209
  end
219
210
  end
220
211
 
221
- test "mail should contain the specified section" do
222
- assert_includes @mail.encoded, "* New background section for testing"
212
+ test "should lazily evaluate exception_recipients" do
213
+ @action.call
214
+ assert_equal [ "first@example.com" ], @mail.to
215
+ @action.call
216
+ assert_equal [ "second@example.com" ], @mail.to
223
217
  end
224
218
  end