exception_notification 4.1.4 → 4.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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