forked from thoughtbot/hoptoad_notifier
/
hoptoad_notifier.rb
289 lines (244 loc) · 9.27 KB
/
hoptoad_notifier.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
require 'net/http'
# Plugin for applications to automatically post errors to the Hoptoad of their choice.
module HoptoadNotifier
IGNORE_DEFAULT = ['ActiveRecord::RecordNotFound',
'ActionController::RoutingError',
'ActionController::InvalidAuthenticityToken',
'CGI::Session::CookieStore::TamperedWithCookie']
# Some of these don't exist for Rails 1.2.*, so we have to consider that.
IGNORE_DEFAULT.map!{|e| eval(e) rescue nil }.compact!
IGNORE_DEFAULT.freeze
class << self
attr_accessor :host, :port, :secure, :api_key, :filter_params
attr_reader :backtrace_filters
# Takes a block and adds it to the list of backtrace filters. When the filters
# run, the block will be handed each line of the backtrace and can modify
# it as necessary. For example, by default a path matching the RAILS_ROOT
# constant will be transformed into "[RAILS_ROOT]"
def filter_backtrace &block
(@backtrace_filters ||= []) << block
end
# The port on which your Hoptoad server runs.
def port
@port || (secure ? 443 : 80)
end
# The host to connect to.
def host
@host ||= 'hoptoadapp.com'
end
# Returns the list of errors that are being ignored. The array can be appended to.
def ignore
@ignore ||= (HoptoadNotifier::IGNORE_DEFAULT.dup)
@ignore.flatten!
@ignore
end
# Sets the list of ignored errors to only what is passed in here. This method
# can be passed a single error or a list of errors.
def ignore_only=(names)
@ignore = [names].flatten
end
# Returns a list of parameters that should be filtered out of what is sent to Hoptoad.
# By default, all "password" attributes will have their contents replaced.
def params_filters
@params_filters ||= %w(password)
end
def environment_filters
@environment_filters ||= %w()
end
# Call this method to modify defaults in your initializers.
def configure
yield self
end
def protocol #:nodoc:
secure ? "https" : "http"
end
def url #:nodoc:
URI.parse("#{protocol}://#{host}:#{port}/notices/")
end
def default_notice_options #:nodoc:
{
:api_key => HoptoadNotifier.api_key,
:error_message => 'Notification',
:backtrace => caller,
:request => {},
:session => {},
:environment => ENV.to_hash
}
end
# You can send an exception manually using this method, even when you are not in a
# controller. You can pass an exception or a hash that contains the attributes that
# would be sent to Hoptoad:
# * api_key: The API key for this project. The API key is a unique identifier that Hoptoad
# uses for identification.
# * error_message: The error returned by the exception (or the message you want to log).
# * backtrace: A backtrace, usually obtained with +caller+.
# * request: The controller's request object.
# * session: The contents of the user's session.
# * environment: ENV merged with the contents of the request's environment.
def notify notice = {}
Sender.new.notify_hoptoad( notice )
end
end
filter_backtrace do |line|
line.gsub(/#{RAILS_ROOT}/, "[RAILS_ROOT]")
end
filter_backtrace do |line|
line.gsub(/^\.\//, "")
end
filter_backtrace do |line|
Gem.path.inject(line) do |line, path|
line.gsub(/#{path}/, "[GEM_ROOT]")
end
end
# Include this module in Controllers in which you want to be notified of errors.
module Catcher
def self.included(base) #:nodoc:
if base.instance_methods.include? 'rescue_action_in_public' and !base.instance_methods.include? 'rescue_action_in_public_without_hoptoad'
base.send(:alias_method, :rescue_action_in_public_without_hoptoad, :rescue_action_in_public)
base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_hoptoad)
end
end
# Overrides the rescue_action method in ActionController::Base, but does not inhibit
# any custom processing that is defined with Rails 2's exception helpers.
def rescue_action_in_public_with_hoptoad exception
notify_hoptoad(exception) unless ignore?(exception)
rescue_action_in_public_without_hoptoad(exception)
end
# This method should be used for sending manual notifications while you are still
# inside the controller. Otherwise it works like HoptoadNotifier.notify.
def notify_hoptoad hash_or_exception
if public_environment?
notice = normalize_notice(hash_or_exception)
notice = clean_notice(notice)
send_to_hoptoad(:notice => notice)
end
end
alias_method :inform_hoptoad, :notify_hoptoad
# Returns the default logger or a logger that prints to STDOUT. Necessary for manual
# notifications outside of controllers.
def logger
ActiveRecord::Base.logger
rescue
@logger ||= Logger.new(STDERR)
end
private
def public_environment? #nodoc:
defined?(RAILS_ENV) and !['development', 'test'].include?(RAILS_ENV)
end
def ignore?(exception) #:nodoc:
ignore_these = HoptoadNotifier.ignore.flatten
ignore_these.include?(exception.class) || ignore_these.include?(exception.class.name)
end
def exception_to_data exception #:nodoc:
data = {
:api_key => HoptoadNotifier.api_key,
:error_class => exception.class.name,
:error_message => "#{exception.class.name}: #{exception.message}",
:backtrace => exception.backtrace,
:environment => ENV.to_hash
}
if self.respond_to? :request
data[:request] = {
:params => request.parameters.to_hash,
:rails_root => File.expand_path(RAILS_ROOT),
:url => "#{request.protocol}#{request.host}#{request.request_uri}"
}
data[:environment].merge!(request.env.to_hash)
end
if self.respond_to? :session
data[:session] = {
:key => session.instance_variable_get("@session_id"),
:data => session.instance_variable_get("@data")
}
end
data
end
def normalize_notice(notice) #:nodoc:
case notice
when Hash
HoptoadNotifier.default_notice_options.merge(notice)
when Exception
HoptoadNotifier.default_notice_options.merge(exception_to_data(notice))
end
end
def clean_notice(notice) #:nodoc:
notice[:backtrace] = clean_hoptoad_backtrace(notice[:backtrace])
if notice[:request].is_a?(Hash) && notice[:request][:params].is_a?(Hash)
notice[:request][:params] = clean_hoptoad_params(notice[:request][:params])
end
if notice[:environment].is_a?(Hash)
notice[:environment] = clean_hoptoad_environment(notice[:environment])
end
clean_non_serializable_data(notice)
end
def send_to_hoptoad data #:nodoc:
url = HoptoadNotifier.url
Net::HTTP.start(url.host, url.port) do |http|
headers = {
'Content-type' => 'application/x-yaml',
'Accept' => 'text/xml, application/xml'
}
http.read_timeout = 5 # seconds
http.open_timeout = 2 # seconds
# http.use_ssl = HoptoadNotifier.secure
response = begin
http.post(url.path, stringify_keys(data).to_yaml, headers)
rescue TimeoutError => e
logger.error "Timeout while contacting the Hoptoad server."
nil
end
case response
when Net::HTTPSuccess then
logger.info "Hoptoad Success: #{response.class}"
else
logger.error "Hoptoad Failure: #{response.class}\n#{response.body if response.respond_to? :body}"
end
end
end
def clean_hoptoad_backtrace backtrace #:nodoc:
if backtrace.to_a.size == 1
backtrace = backtrace.to_a.first.split(/\n\s*/)
end
backtrace.to_a.map do |line|
HoptoadNotifier.backtrace_filters.inject(line) do |line, proc|
proc.call(line)
end
end
end
def clean_hoptoad_params params #:nodoc:
params.each do |k, v|
params[k] = "<filtered>" if HoptoadNotifier.params_filters.any? do |filter|
k.to_s.match(/#{filter}/)
end
end
end
def clean_hoptoad_environment env #:nodoc:
env.each do |k, v|
env[k] = "<filtered>" if HoptoadNotifier.environment_filters.any? do |filter|
k.to_s.match(/#{filter}/)
end
end
end
def clean_non_serializable_data(notice) #:nodoc:
notice.select{|k,v| serialzable?(v) }.inject({}) do |h, pair|
h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
h
end
end
def serialzable?(value) #:nodoc:
!(value.is_a?(Module) || value.kind_of?(IO))
end
def stringify_keys(hash) #:nodoc:
hash.inject({}) do |h, pair|
h[pair.first.to_s] = pair.last.is_a?(Hash) ? stringify_keys(pair.last) : pair.last
h
end
end
end
# A dummy class for sending notifications manually outside of a controller.
class Sender
def rescue_action_in_public(exception)
end
include HoptoadNotifier::Catcher
end
end