thoughtbot-hoptoad_notifier 1.1

Sign up to get free protection for your applications and to get access to all the features.
data/INSTALL ADDED
@@ -0,0 +1,60 @@
1
+ HoptoadNotifier
2
+ ===============
3
+
4
+ This is the notifier plugin for integrating apps with Hoptoad.
5
+
6
+ When an uncaught exception occurs, HoptoadNotifier will POST the relevant data
7
+ to the Hoptoad server specified in your environment.
8
+
9
+
10
+ INSTALLATION
11
+ ------------
12
+
13
+ REMOVE EXCEPTION_NOTIFIER
14
+
15
+ In your ApplicationController, REMOVE this line:
16
+
17
+ include ExceptionNotifiable
18
+
19
+ In your config/environment* files, remove all references to ExceptionNotifier
20
+
21
+ Remove the vendor/plugins/exception_notifier directory.
22
+
23
+ INSTALL HOPTOAD_NOTIFIER
24
+
25
+ From your project's RAILS_ROOT, run:
26
+
27
+ script/plugin install git://github.com/thoughtbot/hoptoad_notifier.git
28
+
29
+ CONFIGURATION
30
+
31
+ You should have something like this in config/initializers/hoptoad.rb.
32
+
33
+ HoptoadNotifier.configure do |config|
34
+ config.api_key = '1234567890abcdef'
35
+ end
36
+
37
+ (Please note that this configuration should be in a global configuration, and
38
+ is *not* enrivonment-specific. Hoptoad is smart enough to know what errors are
39
+ caused by what environments, so your staging errors don't get mixed in with
40
+ your production errors.)
41
+
42
+ Then, to enable hoptoad in your appication, include this code...
43
+
44
+ include HoptoadNotifier::Catcher
45
+
46
+ ...at the top of your ApplicationController, and all exceptions will be logged
47
+ to Hoptoad where they can be aggregated, filtered, sorted, analyzed, massaged,
48
+ and searched.
49
+
50
+ ** NOTE FOR RAILS 1.2.* USERS: **
51
+ You will need to copy the hoptoad_notifier_tasks.rake file into your
52
+ RAILS_ROOT/lib/tasks directory in order for the following to work:
53
+
54
+ You can test that hoptoad is working in your production environment by using
55
+ this rake task (from RAILS_ROOT):
56
+
57
+ rake hoptoad:test
58
+
59
+ If everything is configured properly, that task will send a notice to hoptoad
60
+ which will be visible immediately.
data/README ADDED
@@ -0,0 +1,165 @@
1
+ HoptoadNotifier
2
+ ===============
3
+
4
+ This is the notifier plugin for integrating apps with Hoptoad.
5
+
6
+ When an uncaught exception occurs, HoptoadNotifier will POST the relevant data
7
+ to the Hoptoad server specified in your environment.
8
+
9
+ INSTALLATION
10
+ ------------
11
+
12
+ REMOVE EXCEPTION_NOTIFIER
13
+
14
+ In your ApplicationController, REMOVE this line:
15
+
16
+ include ExceptionNotifiable
17
+
18
+ In your config/environment* files, remove all references to ExceptionNotifier
19
+
20
+ Remove the vendor/plugins/exception_notifier directory.
21
+
22
+ INSTALL HOPTOAD_NOTIFIER
23
+
24
+ From your project's RAILS_ROOT, run:
25
+
26
+ script/plugin install git://github.com/thoughtbot/hoptoad_notifier.git
27
+
28
+ CONFIGURATION
29
+
30
+ You should have something like this in config/initializers/hoptoad.rb.
31
+
32
+ HoptoadNotifier.configure do |config|
33
+ config.api_key = '1234567890abcdef'
34
+ end
35
+
36
+ (Please note that this configuration should be in a global configuration, and
37
+ is *not* enrivonment-specific. Hoptoad is smart enough to know what errors are
38
+ caused by what environments, so your staging errors don't get mixed in with
39
+ your production errors.)
40
+
41
+ Then, to enable hoptoad in your application, include this code...
42
+
43
+ include HoptoadNotifier::Catcher
44
+
45
+ ...in your ApplicationController, and all exceptions will be logged to Hoptoad
46
+ where they can be aggregated, filtered, sorted, analyzed, massaged, and
47
+ searched.
48
+
49
+ ** NOTE FOR RAILS 1.2.* USERS: **
50
+ You will need to copy the hoptoad_notifier_tasks.rake file into your
51
+ RAILS_ROOT/lib/tasks directory in order for the following to work:
52
+
53
+ You can test that hoptoad is working in your production environment by using
54
+ this rake task (from RAILS_ROOT):
55
+
56
+ rake hoptoad:test
57
+
58
+ If everything is configured properly, that task will send a notice to hoptoad
59
+ which will be visible immediately.
60
+
61
+ USAGE
62
+
63
+ For the most part, hoptoad works for itself. Once you've included the notifier
64
+ in your ApplicationController, all errors will be rescued by the
65
+ #rescue_action_in_public provided by the plugin.
66
+
67
+ If you want to log arbitrary things which you've rescued yourself from a
68
+ controller, you can do something like this:
69
+
70
+ ...
71
+ rescue => ex
72
+ notify_hoptoad(ex)
73
+ flash[:failure] = 'Encryptions could not be rerouted, try again.'
74
+ end
75
+ ...
76
+
77
+ The #notify_hoptoad call will send the notice over to hoptoad for later
78
+ analysis.
79
+
80
+ GOING BEYOND EXCEPTIONS
81
+
82
+ You can also pass a hash to notify_hoptoad method and store whatever you want, not just an exception. And you can also use it anywhere, not just in controllers:
83
+
84
+ begin
85
+ params = {
86
+ # params that you pass to a method that can throw an exception
87
+ }
88
+ my_unpredicable_method(params)
89
+ rescue => e
90
+ HoptoadNotifier.notify(
91
+ :error_class => "Special Error",
92
+ :error_message => "Special Error: #{e.message}",
93
+ :request => { :params => params }
94
+ )
95
+ end
96
+
97
+ While in your controllers you use the notify_hoptoad method, anywhere else in your code, use HoptoadNotifier.notify. Hoptoad will get all the information about the error itself. As for a hash, these are the keys you should pass:
98
+
99
+ * :error_class – Use this to group similar errors together. When Hoptoad catches an exception it sends the class name of that exception object.
100
+ * :error_message – This is the title of the error you see in the errors list. For exceptions it is "#{exception.class.name}: #{exception.message}"
101
+ * :request – While there are several ways to send additional data to Hoptoad, passing a Hash with :params key as :request as in the example above is the most common use case. When Hoptoad catches an exception in a controller, the actual HTTP client request is being sent using this key.
102
+
103
+ Hoptoad merges the hash you pass with these default options:
104
+
105
+ def default_notice_options
106
+ {
107
+ :api_key => HoptoadNotifier.api_key,
108
+ :error_message => 'Notification',
109
+ :backtrace => caller,
110
+ :request => {},
111
+ :session => {},
112
+ :environment => ENV.to_hash
113
+ }
114
+ end
115
+
116
+ You can override any of those parameters.
117
+
118
+ FILTERING
119
+
120
+ You can specify a whitelist of errors, that Hoptoad will not report on. Use
121
+ this feature when you are so apathetic to certain errors that you don't want
122
+ them even logged.
123
+
124
+ This filter will only be applied to automatic notifications, not manual
125
+ notifications (when #notify is called directly).
126
+
127
+ Hoptoad ignores the following exceptions by default:
128
+ ActiveRecord::RecordNotFound
129
+ ActionController::RoutingError
130
+ ActionController::InvalidAuthenticityToken
131
+ CGI::Session::CookieStore::TamperedWithCookie
132
+
133
+ To ignore errors in addition to those, specify their names in your Hoptoad
134
+ configuration block.
135
+
136
+ HoptoadNotifier.configure do |config|
137
+ config.api_key = '1234567890abcdef'
138
+ config.ignore << ActiveRecord::IgnoreThisError
139
+ end
140
+
141
+ To ignore *only* certain errors (and override the defaults), use the
142
+ #ignore_only attribute.
143
+
144
+ HoptoadNotifier.configure do |config|
145
+ config.api_key = '1234567890abcdef'
146
+ config.ignore_only = [ActiveRecord::IgnoreThisError]
147
+ end
148
+
149
+ TESTING
150
+
151
+ When you run your tests, you might notice that the hoptoad service is recording
152
+ notices generated using #notify when you don't expect it to. You can
153
+ use code like this in your test_helper.rb to redefine that method so those
154
+ errors are not reported while running tests.
155
+
156
+ module HoptoadNotifier::Catcher
157
+ def notify(thing)
158
+ # do nothing.
159
+ end
160
+ end
161
+
162
+ THANKS
163
+
164
+ Thanks to Eugene Bolshakov for the excellent write-up on GOING BEYOND EXCEPTIONS, which we have included above.
165
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the hoptoad_notifier plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the hoptoad_notifier plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'HoptoadNotifier'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,322 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'rubygems'
4
+ require 'active_support'
5
+
6
+ # Plugin for applications to automatically post errors to the Hoptoad of their choice.
7
+ module HoptoadNotifier
8
+
9
+ IGNORE_DEFAULT = ['ActiveRecord::RecordNotFound',
10
+ 'ActionController::RoutingError',
11
+ 'ActionController::InvalidAuthenticityToken',
12
+ 'CGI::Session::CookieStore::TamperedWithCookie']
13
+
14
+ # Some of these don't exist for Rails 1.2.*, so we have to consider that.
15
+ IGNORE_DEFAULT.map!{|e| eval(e) rescue nil }.compact!
16
+ IGNORE_DEFAULT.freeze
17
+
18
+ class << self
19
+ attr_accessor :host, :port, :secure, :api_key, :http_open_timeout, :http_read_timeout,
20
+ :proxy_host, :proxy_port, :proxy_user, :proxy_pass
21
+ attr_reader :backtrace_filters
22
+
23
+ # Takes a block and adds it to the list of backtrace filters. When the filters
24
+ # run, the block will be handed each line of the backtrace and can modify
25
+ # it as necessary. For example, by default a path matching the RAILS_ROOT
26
+ # constant will be transformed into "[RAILS_ROOT]"
27
+ def filter_backtrace &block
28
+ (@backtrace_filters ||= []) << block
29
+ end
30
+
31
+ # The port on which your Hoptoad server runs.
32
+ def port
33
+ @port || (secure ? 443 : 80)
34
+ end
35
+
36
+ # The host to connect to.
37
+ def host
38
+ @host ||= 'hoptoadapp.com'
39
+ end
40
+
41
+ # The HTTP open timeout (defaults to 2 seconds).
42
+ def http_open_timeout
43
+ @http_open_timeout ||= 2
44
+ end
45
+
46
+ # The HTTP read timeout (defaults to 5 seconds).
47
+ def http_read_timeout
48
+ @http_read_timeout ||= 5
49
+ end
50
+
51
+ # Returns the list of errors that are being ignored. The array can be appended to.
52
+ def ignore
53
+ @ignore ||= (HoptoadNotifier::IGNORE_DEFAULT.dup)
54
+ @ignore.flatten!
55
+ @ignore
56
+ end
57
+
58
+ # Sets the list of ignored errors to only what is passed in here. This method
59
+ # can be passed a single error or a list of errors.
60
+ def ignore_only=(names)
61
+ @ignore = [names].flatten
62
+ end
63
+
64
+ # Returns a list of parameters that should be filtered out of what is sent to Hoptoad.
65
+ # By default, all "password" attributes will have their contents replaced.
66
+ def params_filters
67
+ @params_filters ||= %w(password)
68
+ end
69
+
70
+ def environment_filters
71
+ @environment_filters ||= %w()
72
+ end
73
+
74
+ # Call this method to modify defaults in your initializers.
75
+ #
76
+ # HoptoadNotifier.configure do |config|
77
+ # config.api_key = '1234567890abcdef'
78
+ # config.secure = false
79
+ # end
80
+ #
81
+ # NOTE: secure connections are not yet supported.
82
+ def configure
83
+ yield self
84
+ end
85
+
86
+ def protocol #:nodoc:
87
+ secure ? "https" : "http"
88
+ end
89
+
90
+ def url #:nodoc:
91
+ URI.parse("#{protocol}://#{host}:#{port}/notices/")
92
+ end
93
+
94
+ def default_notice_options #:nodoc:
95
+ {
96
+ :api_key => HoptoadNotifier.api_key,
97
+ :error_message => 'Notification',
98
+ :backtrace => caller,
99
+ :request => {},
100
+ :session => {},
101
+ :environment => ENV.to_hash
102
+ }
103
+ end
104
+
105
+ # You can send an exception manually using this method, even when you are not in a
106
+ # controller. You can pass an exception or a hash that contains the attributes that
107
+ # would be sent to Hoptoad:
108
+ # * api_key: The API key for this project. The API key is a unique identifier that Hoptoad
109
+ # uses for identification.
110
+ # * error_message: The error returned by the exception (or the message you want to log).
111
+ # * backtrace: A backtrace, usually obtained with +caller+.
112
+ # * request: The controller's request object.
113
+ # * session: The contents of the user's session.
114
+ # * environment: ENV merged with the contents of the request's environment.
115
+ def notify notice = {}
116
+ Sender.new.notify_hoptoad( notice )
117
+ end
118
+ end
119
+
120
+ filter_backtrace do |line|
121
+ line.gsub(/#{RAILS_ROOT}/, "[RAILS_ROOT]")
122
+ end
123
+
124
+ filter_backtrace do |line|
125
+ line.gsub(/^\.\//, "")
126
+ end
127
+
128
+ filter_backtrace do |line|
129
+ if defined?(Gem)
130
+ Gem.path.inject(line) do |line, path|
131
+ line.gsub(/#{path}/, "[GEM_ROOT]")
132
+ end
133
+ end
134
+ end
135
+
136
+ # Include this module in Controllers in which you want to be notified of errors.
137
+ module Catcher
138
+
139
+ def self.included(base) #:nodoc:
140
+ if base.instance_methods.include? 'rescue_action_in_public' and !base.instance_methods.include? 'rescue_action_in_public_without_hoptoad'
141
+ base.send(:alias_method, :rescue_action_in_public_without_hoptoad, :rescue_action_in_public)
142
+ base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_hoptoad)
143
+ end
144
+ end
145
+
146
+ # Overrides the rescue_action method in ActionController::Base, but does not inhibit
147
+ # any custom processing that is defined with Rails 2's exception helpers.
148
+ def rescue_action_in_public_with_hoptoad exception
149
+ notify_hoptoad(exception) unless ignore?(exception)
150
+ rescue_action_in_public_without_hoptoad(exception)
151
+ end
152
+
153
+ # This method should be used for sending manual notifications while you are still
154
+ # inside the controller. Otherwise it works like HoptoadNotifier.notify.
155
+ def notify_hoptoad hash_or_exception
156
+ if public_environment?
157
+ notice = normalize_notice(hash_or_exception)
158
+ notice = clean_notice(notice)
159
+ send_to_hoptoad(:notice => notice)
160
+ end
161
+ end
162
+
163
+ alias_method :inform_hoptoad, :notify_hoptoad
164
+
165
+ # Returns the default logger or a logger that prints to STDOUT. Necessary for manual
166
+ # notifications outside of controllers.
167
+ def logger
168
+ ActiveRecord::Base.logger
169
+ rescue
170
+ @logger ||= Logger.new(STDERR)
171
+ end
172
+
173
+ private
174
+
175
+ def public_environment? #nodoc:
176
+ defined?(RAILS_ENV) and !['development', 'test'].include?(RAILS_ENV)
177
+ end
178
+
179
+ def ignore?(exception) #:nodoc:
180
+ ignore_these = HoptoadNotifier.ignore.flatten
181
+ ignore_these.include?(exception.class) || ignore_these.include?(exception.class.name)
182
+ end
183
+
184
+ def exception_to_data exception #:nodoc:
185
+ data = {
186
+ :api_key => HoptoadNotifier.api_key,
187
+ :error_class => exception.class.name,
188
+ :error_message => "#{exception.class.name}: #{exception.message}",
189
+ :backtrace => exception.backtrace,
190
+ :environment => ENV.to_hash
191
+ }
192
+
193
+ if self.respond_to? :request
194
+ data[:request] = {
195
+ :params => request.parameters.to_hash,
196
+ :rails_root => File.expand_path(RAILS_ROOT),
197
+ :url => "#{request.protocol}#{request.host}#{request.request_uri}"
198
+ }
199
+ data[:environment].merge!(request.env.to_hash)
200
+ end
201
+
202
+ if self.respond_to? :session
203
+ data[:session] = {
204
+ :key => session.instance_variable_get("@session_id"),
205
+ :data => session.instance_variable_get("@data")
206
+ }
207
+ end
208
+
209
+ data
210
+ end
211
+
212
+ def normalize_notice(notice) #:nodoc:
213
+ case notice
214
+ when Hash
215
+ HoptoadNotifier.default_notice_options.merge(notice)
216
+ when Exception
217
+ HoptoadNotifier.default_notice_options.merge(exception_to_data(notice))
218
+ end
219
+ end
220
+
221
+ def clean_notice(notice) #:nodoc:
222
+ notice[:backtrace] = clean_hoptoad_backtrace(notice[:backtrace])
223
+ if notice[:request].is_a?(Hash) && notice[:request][:params].is_a?(Hash)
224
+ notice[:request][:params] = filter_parameters(notice[:request][:params]) if respond_to?(:filter_parameters)
225
+ notice[:request][:params] = clean_hoptoad_params(notice[:request][:params])
226
+ end
227
+ if notice[:environment].is_a?(Hash)
228
+ notice[:environment] = filter_parameters(notice[:environment]) if respond_to?(:filter_parameters)
229
+ notice[:environment] = clean_hoptoad_environment(notice[:environment])
230
+ end
231
+ clean_non_serializable_data(notice)
232
+ end
233
+
234
+ def send_to_hoptoad data #:nodoc:
235
+ headers = {
236
+ 'Content-type' => 'application/x-yaml',
237
+ 'Accept' => 'text/xml, application/xml'
238
+ }
239
+
240
+ url = HoptoadNotifier.url
241
+
242
+ http = Net::HTTP::Proxy(HoptoadNotifier.proxy_host,
243
+ HoptoadNotifier.proxy_port,
244
+ HoptoadNotifier.proxy_user,
245
+ HoptoadNotifier.proxy_pass).new(url.host, url.port)
246
+
247
+ http.use_ssl = true
248
+ http.read_timeout = HoptoadNotifier.http_read_timeout
249
+ http.open_timeout = HoptoadNotifier.http_open_timeout
250
+ http.use_ssl = !!HoptoadNotifier.secure
251
+
252
+ response = begin
253
+ http.post(url.path, stringify_keys(data).to_yaml, headers)
254
+ rescue TimeoutError => e
255
+ logger.error "Timeout while contacting the Hoptoad server."
256
+ nil
257
+ end
258
+
259
+ case response
260
+ when Net::HTTPSuccess then
261
+ logger.info "Hoptoad Success: #{response.class}"
262
+ else
263
+ logger.error "Hoptoad Failure: #{response.class}\n#{response.body if response.respond_to? :body}"
264
+ end
265
+ end
266
+
267
+ def clean_hoptoad_backtrace backtrace #:nodoc:
268
+ if backtrace.to_a.size == 1
269
+ backtrace = backtrace.to_a.first.split(/\n\s*/)
270
+ end
271
+
272
+ backtrace.to_a.map do |line|
273
+ HoptoadNotifier.backtrace_filters.inject(line) do |line, proc|
274
+ proc.call(line)
275
+ end
276
+ end
277
+ end
278
+
279
+ def clean_hoptoad_params params #:nodoc:
280
+ params.each do |k, v|
281
+ params[k] = "[FILTERED]" if HoptoadNotifier.params_filters.any? do |filter|
282
+ k.to_s.match(/#{filter}/)
283
+ end
284
+ end
285
+ end
286
+
287
+ def clean_hoptoad_environment env #:nodoc:
288
+ env.each do |k, v|
289
+ env[k] = "[FILTERED]" if HoptoadNotifier.environment_filters.any? do |filter|
290
+ k.to_s.match(/#{filter}/)
291
+ end
292
+ end
293
+ end
294
+
295
+ def clean_non_serializable_data(notice) #:nodoc:
296
+ notice.select{|k,v| serializable?(v) }.inject({}) do |h, pair|
297
+ h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
298
+ h
299
+ end
300
+ end
301
+
302
+ def serializable?(value) #:nodoc:
303
+ !(value.is_a?(Module) || value.kind_of?(IO))
304
+ end
305
+
306
+ def stringify_keys(hash) #:nodoc:
307
+ hash.inject({}) do |h, pair|
308
+ h[pair.first.to_s] = pair.last.is_a?(Hash) ? stringify_keys(pair.last) : pair.last
309
+ h
310
+ end
311
+ end
312
+
313
+ end
314
+
315
+ # A dummy class for sending notifications manually outside of a controller.
316
+ class Sender
317
+ def rescue_action_in_public(exception)
318
+ end
319
+
320
+ include HoptoadNotifier::Catcher
321
+ end
322
+ end
@@ -0,0 +1,55 @@
1
+ namespace :hoptoad do
2
+ desc "Verify your plugin installation by sending a test exception to the hoptoad service"
3
+ task :test => :environment do
4
+ require 'action_controller/test_process'
5
+ require 'application'
6
+
7
+ request = ActionController::TestRequest.new({
8
+ 'action' => 'verify',
9
+ 'controller' => 'hoptoad_verification',
10
+ '_method' => 'GET'
11
+ })
12
+
13
+ response = ActionController::TestResponse.new
14
+
15
+ class HoptoadTestingException < RuntimeError; end
16
+
17
+ unless ApplicationController.ancestors.include? HoptoadNotifier::Catcher
18
+ puts "You have not included HoptoadNotifier::Catcher in your ApplicationController!"
19
+ exit
20
+ end
21
+
22
+ puts 'Setting up the Controller.'
23
+ class ApplicationController
24
+ # This is to bypass any filters that may prevent access to the action.
25
+ prepend_before_filter :test_hoptoad
26
+ def test_hoptoad
27
+ puts "Raising '#{exception_class.name}' to simulate application failure."
28
+ raise exception_class.new, 'Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
29
+ end
30
+
31
+ def rescue_action exception
32
+ rescue_action_in_public exception
33
+ end
34
+
35
+ def public_environment?
36
+ true
37
+ end
38
+
39
+ # Ensure we actually have an action to go to.
40
+ def verify; end
41
+
42
+ def exception_class
43
+ exception_name = ENV['EXCEPTION'] || "HoptoadTestingException"
44
+ Object.const_get(exception_name)
45
+ rescue
46
+ Object.const_set(exception_name, Class.new(Exception))
47
+ end
48
+ end
49
+
50
+ puts 'Processing request.'
51
+ class HoptoadVerificationController < ApplicationController; end
52
+ HoptoadVerificationController.new.process(request, response)
53
+ end
54
+ end
55
+
@@ -0,0 +1,527 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'mocha'
4
+ gem 'thoughtbot-shoulda', ">= 2.0.0"
5
+ require 'shoulda'
6
+ require 'action_controller'
7
+ require 'action_controller/test_process'
8
+ require 'active_record'
9
+ require File.join(File.dirname(__FILE__), "..", "lib", "hoptoad_notifier")
10
+
11
+ RAILS_ROOT = File.join( File.dirname(__FILE__), "rails_root" )
12
+ RAILS_ENV = "test"
13
+
14
+ class HoptoadController < ActionController::Base
15
+ def rescue_action e
16
+ raise e
17
+ end
18
+
19
+ def do_raise
20
+ raise "Hoptoad"
21
+ end
22
+
23
+ def do_not_raise
24
+ render :text => "Success"
25
+ end
26
+
27
+ def do_raise_ignored
28
+ raise ActiveRecord::RecordNotFound.new("404")
29
+ end
30
+
31
+ def do_raise_not_ignored
32
+ raise ActiveRecord::StatementInvalid.new("Statement invalid")
33
+ end
34
+
35
+ def manual_notify
36
+ notify_hoptoad(Exception.new)
37
+ render :text => "Success"
38
+ end
39
+
40
+ def manual_notify_ignored
41
+ notify_hoptoad(ActiveRecord::RecordNotFound.new("404"))
42
+ render :text => "Success"
43
+ end
44
+ end
45
+
46
+ class HoptoadNotifierTest < Test::Unit::TestCase
47
+ def request(action = nil, method = :get)
48
+ @request = ActionController::TestRequest.new({
49
+ "controller" => "hoptoad",
50
+ "action" => action ? action.to_s : "",
51
+ "_method" => method.to_s
52
+ })
53
+ @response = ActionController::TestResponse.new
54
+ @controller.process(@request, @response)
55
+ end
56
+
57
+ context "Hoptoad inclusion" do
58
+ should "be able to occur even outside Rails controllers" do
59
+ assert_nothing_raised do
60
+ class MyHoptoad
61
+ include HoptoadNotifier::Catcher
62
+ end
63
+ end
64
+ my = MyHoptoad.new
65
+ assert my.respond_to?(:notify_hoptoad)
66
+ end
67
+ end
68
+
69
+ context "HoptoadNotifier configuration" do
70
+ setup do
71
+ @controller = HoptoadController.new
72
+ class ::HoptoadController
73
+ include HoptoadNotifier::Catcher
74
+ def rescue_action e
75
+ rescue_action_in_public e
76
+ end
77
+ end
78
+ assert @controller.methods.include?("notify_hoptoad")
79
+ end
80
+
81
+ should "be done with a block" do
82
+ HoptoadNotifier.configure do |config|
83
+ config.host = "host"
84
+ config.port = 3333
85
+ config.secure = true
86
+ config.api_key = "1234567890abcdef"
87
+ config.ignore << [ RuntimeError ]
88
+ config.proxy_host = 'proxyhost1'
89
+ config.proxy_port = '80'
90
+ config.proxy_user = 'user'
91
+ config.proxy_pass = 'secret'
92
+ config.http_open_timeout = 2
93
+ config.http_read_timeout = 5
94
+ end
95
+
96
+ assert_equal "host", HoptoadNotifier.host
97
+ assert_equal 3333, HoptoadNotifier.port
98
+ assert_equal true, HoptoadNotifier.secure
99
+ assert_equal "1234567890abcdef", HoptoadNotifier.api_key
100
+ assert_equal 'proxyhost1', HoptoadNotifier.proxy_host
101
+ assert_equal '80', HoptoadNotifier.proxy_port
102
+ assert_equal 'user', HoptoadNotifier.proxy_user
103
+ assert_equal 'secret', HoptoadNotifier.proxy_pass
104
+ assert_equal 2, HoptoadNotifier.http_open_timeout
105
+ assert_equal 5, HoptoadNotifier.http_read_timeout
106
+ assert_equal (HoptoadNotifier::IGNORE_DEFAULT + [RuntimeError]), HoptoadNotifier.ignore
107
+ end
108
+
109
+ should "set a default host" do
110
+ HoptoadNotifier.instance_variable_set("@host",nil)
111
+ assert_equal "hoptoadapp.com", HoptoadNotifier.host
112
+ end
113
+
114
+ should "add filters to the backtrace_filters" do
115
+ assert_difference "HoptoadNotifier.backtrace_filters.length" do
116
+ HoptoadNotifier.configure do |config|
117
+ config.filter_backtrace do |line|
118
+ line = "1234"
119
+ end
120
+ end
121
+ end
122
+
123
+ assert_equal %w( 1234 1234 ), @controller.send(:clean_hoptoad_backtrace, %w( foo bar ))
124
+ end
125
+
126
+ should "use standard rails logging filters on params and env" do
127
+ ::HoptoadController.class_eval do
128
+ filter_parameter_logging :ghi
129
+ end
130
+
131
+ expected = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "[FILTERED]"}},
132
+ "environment" => {"abc" => "123", "ghi" => "[FILTERED]"}}}
133
+ notice = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "789"}},
134
+ "environment" => {"abc" => "123", "ghi" => "789"}}}
135
+ assert @controller.respond_to?(:filter_parameters)
136
+ assert_equal( expected[:notice], @controller.send(:clean_notice, notice)[:notice] )
137
+ end
138
+
139
+ should "add filters to the params filters" do
140
+ assert_difference "HoptoadNotifier.params_filters.length", 2 do
141
+ HoptoadNotifier.configure do |config|
142
+ config.params_filters << "abc"
143
+ config.params_filters << "def"
144
+ end
145
+ end
146
+
147
+ assert HoptoadNotifier.params_filters.include?( "abc" )
148
+ assert HoptoadNotifier.params_filters.include?( "def" )
149
+
150
+ assert_equal( {:abc => "[FILTERED]", :def => "[FILTERED]", :ghi => "789"},
151
+ @controller.send(:clean_hoptoad_params, :abc => "123", :def => "456", :ghi => "789" ) )
152
+ end
153
+
154
+ should "add filters to the environment filters" do
155
+ assert_difference "HoptoadNotifier.environment_filters.length", 2 do
156
+ HoptoadNotifier.configure do |config|
157
+ config.environment_filters << "secret"
158
+ config.environment_filters << "supersecret"
159
+ end
160
+ end
161
+
162
+ assert HoptoadNotifier.environment_filters.include?( "secret" )
163
+ assert HoptoadNotifier.environment_filters.include?( "supersecret" )
164
+
165
+ assert_equal( {:secret => "[FILTERED]", :supersecret => "[FILTERED]", :ghi => "789"},
166
+ @controller.send(:clean_hoptoad_environment, :secret => "123", :supersecret => "456", :ghi => "789" ) )
167
+ end
168
+
169
+ should "have at default ignored exceptions" do
170
+ assert HoptoadNotifier::IGNORE_DEFAULT.any?
171
+ end
172
+ end
173
+
174
+ context "The hoptoad test controller" do
175
+ setup do
176
+ @controller = ::HoptoadController.new
177
+ class ::HoptoadController
178
+ def rescue_action e
179
+ raise e
180
+ end
181
+ end
182
+ end
183
+
184
+ context "with no notifier catcher" do
185
+ should "not prevent raises" do
186
+ assert_raises RuntimeError do
187
+ request("do_raise")
188
+ end
189
+ end
190
+
191
+ should "allow a non-raising action to complete" do
192
+ assert_nothing_raised do
193
+ request("do_not_raise")
194
+ end
195
+ end
196
+ end
197
+
198
+ context "with the notifier installed" do
199
+ setup do
200
+ class ::HoptoadController
201
+ include HoptoadNotifier::Catcher
202
+ def rescue_action e
203
+ rescue_action_in_public e
204
+ end
205
+ end
206
+ HoptoadNotifier.ignore_only = HoptoadNotifier::IGNORE_DEFAULT
207
+ @controller.stubs(:public_environment?).returns(true)
208
+ @controller.stubs(:send_to_hoptoad)
209
+ end
210
+
211
+ should "have inserted its methods into the controller" do
212
+ assert @controller.methods.include?("inform_hoptoad")
213
+ end
214
+
215
+ should "prevent raises and send the error to hoptoad" do
216
+ @controller.expects(:notify_hoptoad)
217
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
218
+ assert_nothing_raised do
219
+ request("do_raise")
220
+ end
221
+ end
222
+
223
+ should "allow a non-raising action to complete" do
224
+ assert_nothing_raised do
225
+ request("do_not_raise")
226
+ end
227
+ end
228
+
229
+ should "allow manual sending of exceptions" do
230
+ @controller.expects(:notify_hoptoad)
231
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
232
+ assert_nothing_raised do
233
+ request("manual_notify")
234
+ end
235
+ end
236
+
237
+ should "disable manual sending of exceptions in a non-public (development or test) environment" do
238
+ @controller.stubs(:public_environment?).returns(false)
239
+ @controller.expects(:send_to_hoptoad).never
240
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
241
+ assert_nothing_raised do
242
+ request("manual_notify")
243
+ end
244
+ end
245
+
246
+ should "send even ignored exceptions if told manually" do
247
+ @controller.expects(:notify_hoptoad)
248
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
249
+ assert_nothing_raised do
250
+ request("manual_notify_ignored")
251
+ end
252
+ end
253
+
254
+ should "ignore default exceptions" do
255
+ @controller.expects(:notify_hoptoad).never
256
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
257
+ assert_nothing_raised do
258
+ request("do_raise_ignored")
259
+ end
260
+ end
261
+
262
+ should "filter non-serializable data" do
263
+ File.open(__FILE__) do |file|
264
+ assert_equal( {:ghi => "789"},
265
+ @controller.send(:clean_non_serializable_data, :ghi => "789", :class => Class.new, :file => file) )
266
+ end
267
+ end
268
+
269
+ should "apply all params, environment and technical filters" do
270
+ params_hash = {:abc => 123}
271
+ environment_hash = {:def => 456}
272
+ backtrace_data = :backtrace_data
273
+
274
+ raw_notice = {:request => {:params => params_hash},
275
+ :environment => environment_hash,
276
+ :backtrace => backtrace_data}
277
+
278
+ processed_notice = {:backtrace => :backtrace_data,
279
+ :request => {:params => :params_data},
280
+ :environment => :environment_data}
281
+
282
+ @controller.expects(:clean_hoptoad_backtrace).with(backtrace_data).returns(:backtrace_data)
283
+ @controller.expects(:clean_hoptoad_params).with(params_hash).returns(:params_data)
284
+ @controller.expects(:clean_hoptoad_environment).with(environment_hash).returns(:environment_data)
285
+ @controller.expects(:clean_non_serializable_data).with(processed_notice).returns(:serializable_data)
286
+
287
+ assert_equal(:serializable_data, @controller.send(:clean_notice, raw_notice))
288
+ end
289
+
290
+ context "and configured to ignore additional exceptions" do
291
+ setup do
292
+ HoptoadNotifier.ignore << ActiveRecord::StatementInvalid
293
+ end
294
+
295
+ should "still ignore default exceptions" do
296
+ @controller.expects(:notify_hoptoad).never
297
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
298
+ assert_nothing_raised do
299
+ request("do_raise_ignored")
300
+ end
301
+ end
302
+
303
+ should "ignore specified exceptions" do
304
+ @controller.expects(:notify_hoptoad).never
305
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
306
+ assert_nothing_raised do
307
+ request("do_raise_not_ignored")
308
+ end
309
+ end
310
+
311
+ should "not ignore unspecified, non-default exceptions" do
312
+ @controller.expects(:notify_hoptoad)
313
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
314
+ assert_nothing_raised do
315
+ request("do_raise")
316
+ end
317
+ end
318
+ end
319
+
320
+ context "and configured to ignore only certain exceptions" do
321
+ setup do
322
+ HoptoadNotifier.ignore_only = [ActiveRecord::StatementInvalid]
323
+ end
324
+
325
+ should "no longer ignore default exceptions" do
326
+ @controller.expects(:notify_hoptoad)
327
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
328
+ assert_nothing_raised do
329
+ request("do_raise_ignored")
330
+ end
331
+ end
332
+
333
+ should "ignore specified exceptions" do
334
+ @controller.expects(:notify_hoptoad).never
335
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
336
+ assert_nothing_raised do
337
+ request("do_raise_not_ignored")
338
+ end
339
+ end
340
+
341
+ should "not ignore unspecified, non-default exceptions" do
342
+ @controller.expects(:notify_hoptoad)
343
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
344
+ assert_nothing_raised do
345
+ request("do_raise")
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ context "Sending a notice" do
353
+ context "with an exception" do
354
+ setup do
355
+ @sender = HoptoadNotifier::Sender.new
356
+ @backtrace = caller
357
+ @exception = begin
358
+ raise
359
+ rescue => caught_exception
360
+ caught_exception
361
+ end
362
+ @options = {:error_message => "123",
363
+ :backtrace => @backtrace}
364
+ HoptoadNotifier.instance_variable_set("@backtrace_filters", [])
365
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
366
+ @sender.stubs(:public_environment?).returns(true)
367
+ end
368
+
369
+ context "when using an HTTP Proxy" do
370
+ setup do
371
+ @body = 'body'
372
+ @response = stub(:body => @body)
373
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
374
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
375
+ @proxy = stub
376
+ @proxy.stubs(:new).returns(@http)
377
+
378
+ HoptoadNotifier.port = nil
379
+ HoptoadNotifier.host = nil
380
+ HoptoadNotifier.secure = false
381
+
382
+ Net::HTTP.expects(:Proxy).with(
383
+ HoptoadNotifier.proxy_host,
384
+ HoptoadNotifier.proxy_port,
385
+ HoptoadNotifier.proxy_user,
386
+ HoptoadNotifier.proxy_pass
387
+ ).returns(@proxy)
388
+ end
389
+
390
+ context "on notify" do
391
+ setup { HoptoadNotifier.notify(@exception) }
392
+
393
+ before_should "post to Hoptoad" do
394
+ url = "http://hoptoadapp.com:80/notices/"
395
+ uri = URI.parse(url)
396
+ URI.expects(:parse).with(url).returns(uri)
397
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
398
+ end
399
+ end
400
+ end
401
+
402
+ context "when stubbing out Net::HTTP" do
403
+ setup do
404
+ @body = 'body'
405
+ @response = stub(:body => @body)
406
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
407
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
408
+ Net::HTTP.stubs(:new).returns(@http)
409
+ HoptoadNotifier.port = nil
410
+ HoptoadNotifier.host = nil
411
+ HoptoadNotifier.proxy_host = nil
412
+ end
413
+
414
+ context "on notify" do
415
+ setup { HoptoadNotifier.notify(@exception) }
416
+
417
+ before_should "post to the right url for non-ssl" do
418
+ HoptoadNotifier.secure = false
419
+ url = "http://hoptoadapp.com:80/notices/"
420
+ uri = URI.parse(url)
421
+ URI.expects(:parse).with(url).returns(uri)
422
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
423
+ end
424
+
425
+ before_should "post to the right path" do
426
+ @http.expects(:post).with("/notices/", anything, anything).returns(@response)
427
+ end
428
+
429
+ before_should "call send_to_hoptoad" do
430
+ @sender.expects(:send_to_hoptoad)
431
+ end
432
+
433
+ before_should "default the open timeout to 2 seconds" do
434
+ HoptoadNotifier.http_open_timeout = nil
435
+ @http.expects(:open_timeout=).with(2)
436
+ end
437
+
438
+ before_should "default the read timeout to 5 seconds" do
439
+ HoptoadNotifier.http_read_timeout = nil
440
+ @http.expects(:read_timeout=).with(5)
441
+ end
442
+
443
+ before_should "allow override of the open timeout" do
444
+ HoptoadNotifier.http_open_timeout = 4
445
+ @http.expects(:open_timeout=).with(4)
446
+ end
447
+
448
+ before_should "allow override of the read timeout" do
449
+ HoptoadNotifier.http_read_timeout = 10
450
+ @http.expects(:read_timeout=).with(10)
451
+ end
452
+
453
+ before_should "connect to the right port for ssl" do
454
+ HoptoadNotifier.secure = true
455
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 443).returns(@http)
456
+ end
457
+
458
+ before_should "connect to the right port for non-ssl" do
459
+ HoptoadNotifier.secure = false
460
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 80).returns(@http)
461
+ end
462
+
463
+ before_should "use ssl if secure" do
464
+ HoptoadNotifier.secure = true
465
+ HoptoadNotifier.host = 'example.org'
466
+ Net::HTTP.expects(:new).with('example.org', 443).returns(@http)
467
+ end
468
+
469
+ before_should "not use ssl if not secure" do
470
+ HoptoadNotifier.secure = nil
471
+ HoptoadNotifier.host = 'example.org'
472
+ Net::HTTP.expects(:new).with('example.org', 80).returns(@http)
473
+ end
474
+ end
475
+ end
476
+
477
+ should "send as if it were a normally caught exception" do
478
+ @sender.expects(:notify_hoptoad).with(@exception)
479
+ HoptoadNotifier.notify(@exception)
480
+ end
481
+
482
+ should "make sure the exception is munged into a hash" do
483
+ options = HoptoadNotifier.default_notice_options.merge({
484
+ :backtrace => @exception.backtrace,
485
+ :environment => ENV.to_hash,
486
+ :error_class => @exception.class.name,
487
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
488
+ :api_key => HoptoadNotifier.api_key,
489
+ })
490
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
491
+ HoptoadNotifier.notify(@exception)
492
+ end
493
+
494
+ should "parse massive one-line exceptions into multiple lines" do
495
+ @original_backtrace = "one big line\n separated\n by new lines\nand some spaces"
496
+ @expected_backtrace = ["one big line", "separated", "by new lines", "and some spaces"]
497
+ @exception.set_backtrace [@original_backtrace]
498
+
499
+ options = HoptoadNotifier.default_notice_options.merge({
500
+ :backtrace => @expected_backtrace,
501
+ :environment => ENV.to_hash,
502
+ :error_class => @exception.class.name,
503
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
504
+ :api_key => HoptoadNotifier.api_key,
505
+ })
506
+
507
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
508
+ HoptoadNotifier.notify(@exception)
509
+ end
510
+ end
511
+
512
+ context "without an exception" do
513
+ setup do
514
+ @sender = HoptoadNotifier::Sender.new
515
+ @backtrace = caller
516
+ @options = {:error_message => "123",
517
+ :backtrace => @backtrace}
518
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
519
+ end
520
+
521
+ should "send sensible defaults" do
522
+ @sender.expects(:notify_hoptoad).with(@options)
523
+ HoptoadNotifier.notify(:error_message => "123", :backtrace => @backtrace)
524
+ end
525
+ end
526
+ end
527
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thoughtbot-hoptoad_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.1"
5
+ platform: ruby
6
+ authors:
7
+ - Thoughtbot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-31 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Rails plugin that reports exceptions to Hoptoad.
17
+ email: info@thoughtbot.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - INSTALL
26
+ - lib/hoptoad_notifier.rb
27
+ - Rakefile
28
+ - README
29
+ - tasks/hoptoad_notifier_tasks.rake
30
+ has_rdoc: true
31
+ homepage: http://github.com/thoughtbot/hoptoad_notifier
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: Rails plugin that reports exceptions to Hoptoad.
56
+ test_files:
57
+ - test/hoptoad_notifier_test.rb