pzingg-hoptoad_notifier 1.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,64 @@
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
+ To enable hoptoad in your application, you must modify your application.rb to
43
+ include the module HoptoadNotifier::Catcher into your application controller.
44
+
45
+ Example...
46
+
47
+ class ApplicationController < ActionController::Base
48
+ include HoptoadNotifier::Catcher
49
+ end
50
+
51
+ After this is in place, all exceptions will be logged to Hoptoad where they can
52
+ be aggregated, filtered, sorted, analyzed, massaged, and searched.
53
+
54
+ ** NOTE FOR RAILS 1.2.* USERS: **
55
+ You will need to copy the hoptoad_notifier_tasks.rake file into your
56
+ RAILS_ROOT/lib/tasks directory in order for the following to work:
57
+
58
+ You can test that hoptoad is working in your production environment by using
59
+ this rake task (from RAILS_ROOT):
60
+
61
+ rake hoptoad:test
62
+
63
+ If everything is configured properly, that task will send a notice to hoptoad
64
+ 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,57 @@
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_controller'
6
+
7
+ request = ActionController::TestRequest.new
8
+ request.query_parameters = {
9
+ 'action' => 'verify',
10
+ 'controller' => 'hoptoad_verification'
11
+ }
12
+
13
+ response = ActionController::TestResponse.new
14
+
15
+ class HoptoadTestingException < RuntimeError; end
16
+
17
+ in_controller = ApplicationController.included_modules.include? HoptoadNotifier::Catcher
18
+ in_base = ActionController::Base.included_modules.include? HoptoadNotifier::Catcher
19
+ unless in_controller and not in_base
20
+ puts "HoptoadNotifier::Catcher must be included inside your ApplicationController class."
21
+ exit
22
+ end
23
+
24
+ puts 'Setting up the Controller.'
25
+ class ApplicationController
26
+ # This is to bypass any filters that may prevent access to the action.
27
+ prepend_before_filter :test_hoptoad
28
+ def test_hoptoad
29
+ puts "Raising '#{exception_class.name}' to simulate application failure."
30
+ raise exception_class.new, 'Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
31
+ end
32
+
33
+ def rescue_action exception
34
+ rescue_action_in_public exception
35
+ end
36
+
37
+ def public_environment?
38
+ true
39
+ end
40
+
41
+ # Ensure we actually have an action to go to.
42
+ def verify; end
43
+
44
+ def exception_class
45
+ exception_name = ENV['EXCEPTION'] || "HoptoadTestingException"
46
+ Object.const_get(exception_name)
47
+ rescue
48
+ Object.const_set(exception_name, Class.new(Exception))
49
+ end
50
+ end
51
+
52
+ puts 'Processing request.'
53
+ class HoptoadVerificationController < ApplicationController; end
54
+ HoptoadVerificationController.new.process(request, response)
55
+ end
56
+ end
57
+
@@ -0,0 +1,526 @@
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 'active_record/base'
10
+ require 'active_support/testing/core_ext/test/unit/assertions'
11
+ require File.join(File.dirname(__FILE__), "..", "lib", "hoptoad_notifier")
12
+
13
+ RAILS_ROOT = File.join( File.dirname(__FILE__), "rails_root" )
14
+ RAILS_ENV = "test"
15
+
16
+ class HoptoadController < ActionController::Base
17
+ def rescue_action e
18
+ raise e
19
+ end
20
+
21
+ def do_raise
22
+ raise "Hoptoad"
23
+ end
24
+
25
+ def do_not_raise
26
+ render :text => "Success"
27
+ end
28
+
29
+ def do_raise_ignored
30
+ raise ActiveRecord::RecordNotFound.new("404")
31
+ end
32
+
33
+ def do_raise_not_ignored
34
+ raise ActiveRecord::StatementInvalid.new("Statement invalid")
35
+ end
36
+
37
+ def manual_notify
38
+ notify_hoptoad(Exception.new)
39
+ render :text => "Success"
40
+ end
41
+
42
+ def manual_notify_ignored
43
+ notify_hoptoad(ActiveRecord::RecordNotFound.new("404"))
44
+ render :text => "Success"
45
+ end
46
+ end
47
+
48
+ class HoptoadNotifierTest < Test::Unit::TestCase
49
+ def request(action = nil, method = :get)
50
+ @request = ActionController::TestRequest.new
51
+ @request.action = action ? action.to_s : ""
52
+ @response = ActionController::TestResponse.new
53
+ @controller.process(@request, @response)
54
+ end
55
+
56
+ context "Hoptoad inclusion" do
57
+ should "be able to occur even outside Rails controllers" do
58
+ assert_nothing_raised do
59
+ class MyHoptoad
60
+ include HoptoadNotifier::Catcher
61
+ end
62
+ end
63
+ my = MyHoptoad.new
64
+ assert my.respond_to?(:notify_hoptoad)
65
+ end
66
+ end
67
+
68
+ context "HoptoadNotifier configuration" do
69
+ setup do
70
+ @controller = HoptoadController.new
71
+ class ::HoptoadController
72
+ include HoptoadNotifier::Catcher
73
+ def rescue_action e
74
+ rescue_action_in_public e
75
+ end
76
+ end
77
+ assert @controller.methods.include?("notify_hoptoad")
78
+ end
79
+
80
+ should "be done with a block" do
81
+ HoptoadNotifier.configure do |config|
82
+ config.host = "host"
83
+ config.port = 3333
84
+ config.secure = true
85
+ config.api_key = "1234567890abcdef"
86
+ config.ignore << [ RuntimeError ]
87
+ config.proxy_host = 'proxyhost1'
88
+ config.proxy_port = '80'
89
+ config.proxy_user = 'user'
90
+ config.proxy_pass = 'secret'
91
+ config.http_open_timeout = 2
92
+ config.http_read_timeout = 5
93
+ end
94
+
95
+ assert_equal "host", HoptoadNotifier.host
96
+ assert_equal 3333, HoptoadNotifier.port
97
+ assert_equal true, HoptoadNotifier.secure
98
+ assert_equal "1234567890abcdef", HoptoadNotifier.api_key
99
+ assert_equal 'proxyhost1', HoptoadNotifier.proxy_host
100
+ assert_equal '80', HoptoadNotifier.proxy_port
101
+ assert_equal 'user', HoptoadNotifier.proxy_user
102
+ assert_equal 'secret', HoptoadNotifier.proxy_pass
103
+ assert_equal 2, HoptoadNotifier.http_open_timeout
104
+ assert_equal 5, HoptoadNotifier.http_read_timeout
105
+ assert_equal (HoptoadNotifier::IGNORE_DEFAULT + [RuntimeError]), HoptoadNotifier.ignore
106
+ end
107
+
108
+ should "set a default host" do
109
+ HoptoadNotifier.instance_variable_set("@host",nil)
110
+ assert_equal "hoptoadapp.com", HoptoadNotifier.host
111
+ end
112
+
113
+ should "add filters to the backtrace_filters" do
114
+ assert_difference "HoptoadNotifier.backtrace_filters.length" do
115
+ HoptoadNotifier.configure do |config|
116
+ config.filter_backtrace do |line|
117
+ line = "1234"
118
+ end
119
+ end
120
+ end
121
+
122
+ assert_equal %w( 1234 1234 ), @controller.send(:clean_hoptoad_backtrace, %w( foo bar ))
123
+ end
124
+
125
+ should "use standard rails logging filters on params and env" do
126
+ ::HoptoadController.class_eval do
127
+ filter_parameter_logging :ghi
128
+ end
129
+
130
+ expected = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "[FILTERED]"}},
131
+ "environment" => {"abc" => "123", "ghi" => "[FILTERED]"}}}
132
+ notice = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "789"}},
133
+ "environment" => {"abc" => "123", "ghi" => "789"}}}
134
+ assert @controller.respond_to?(:filter_parameters)
135
+ assert_equal( expected[:notice], @controller.send(:clean_notice, notice)[:notice] )
136
+ end
137
+
138
+ should "add filters to the params filters" do
139
+ assert_difference "HoptoadNotifier.params_filters.length", 2 do
140
+ HoptoadNotifier.configure do |config|
141
+ config.params_filters << "abc"
142
+ config.params_filters << "def"
143
+ end
144
+ end
145
+
146
+ assert HoptoadNotifier.params_filters.include?( "abc" )
147
+ assert HoptoadNotifier.params_filters.include?( "def" )
148
+
149
+ assert_equal( {:abc => "[FILTERED]", :def => "[FILTERED]", :ghi => "789"},
150
+ @controller.send(:clean_hoptoad_params, :abc => "123", :def => "456", :ghi => "789" ) )
151
+ end
152
+
153
+ should "add filters to the environment filters" do
154
+ assert_difference "HoptoadNotifier.environment_filters.length", 2 do
155
+ HoptoadNotifier.configure do |config|
156
+ config.environment_filters << "secret"
157
+ config.environment_filters << "supersecret"
158
+ end
159
+ end
160
+
161
+ assert HoptoadNotifier.environment_filters.include?( "secret" )
162
+ assert HoptoadNotifier.environment_filters.include?( "supersecret" )
163
+
164
+ assert_equal( {:secret => "[FILTERED]", :supersecret => "[FILTERED]", :ghi => "789"},
165
+ @controller.send(:clean_hoptoad_environment, :secret => "123", :supersecret => "456", :ghi => "789" ) )
166
+ end
167
+
168
+ should "have at default ignored exceptions" do
169
+ assert HoptoadNotifier::IGNORE_DEFAULT.any?
170
+ end
171
+ end
172
+
173
+ context "The hoptoad test controller" do
174
+ setup do
175
+ @controller = ::HoptoadController.new
176
+ class ::HoptoadController
177
+ def rescue_action e
178
+ raise e
179
+ end
180
+ end
181
+ end
182
+
183
+ context "with no notifier catcher" do
184
+ should "not prevent raises" do
185
+ assert_raises RuntimeError do
186
+ request("do_raise")
187
+ end
188
+ end
189
+
190
+ should "allow a non-raising action to complete" do
191
+ assert_nothing_raised do
192
+ request("do_not_raise")
193
+ end
194
+ end
195
+ end
196
+
197
+ context "with the notifier installed" do
198
+ setup do
199
+ class ::HoptoadController
200
+ include HoptoadNotifier::Catcher
201
+ def rescue_action e
202
+ rescue_action_in_public e
203
+ end
204
+ end
205
+ HoptoadNotifier.ignore_only = HoptoadNotifier::IGNORE_DEFAULT
206
+ @controller.stubs(:public_environment?).returns(true)
207
+ @controller.stubs(:send_to_hoptoad)
208
+ end
209
+
210
+ should "have inserted its methods into the controller" do
211
+ assert @controller.methods.include?("inform_hoptoad")
212
+ end
213
+
214
+ should "prevent raises and send the error to hoptoad" do
215
+ @controller.expects(:notify_hoptoad)
216
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
217
+ assert_nothing_raised do
218
+ request("do_raise")
219
+ end
220
+ end
221
+
222
+ should "allow a non-raising action to complete" do
223
+ assert_nothing_raised do
224
+ request("do_not_raise")
225
+ end
226
+ end
227
+
228
+ should "allow manual sending of exceptions" do
229
+ @controller.expects(:notify_hoptoad)
230
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
231
+ assert_nothing_raised do
232
+ request("manual_notify")
233
+ end
234
+ end
235
+
236
+ should "disable manual sending of exceptions in a non-public (development or test) environment" do
237
+ @controller.stubs(:public_environment?).returns(false)
238
+ @controller.expects(:send_to_hoptoad).never
239
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
240
+ assert_nothing_raised do
241
+ request("manual_notify")
242
+ end
243
+ end
244
+
245
+ should "send even ignored exceptions if told manually" do
246
+ @controller.expects(:notify_hoptoad)
247
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
248
+ assert_nothing_raised do
249
+ request("manual_notify_ignored")
250
+ end
251
+ end
252
+
253
+ should "ignore default exceptions" do
254
+ @controller.expects(:notify_hoptoad).never
255
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
256
+ assert_nothing_raised do
257
+ request("do_raise_ignored")
258
+ end
259
+ end
260
+
261
+ should "filter non-serializable data" do
262
+ File.open(__FILE__) do |file|
263
+ assert_equal( {:ghi => "789"},
264
+ @controller.send(:clean_non_serializable_data, :ghi => "789", :class => Class.new, :file => file) )
265
+ end
266
+ end
267
+
268
+ should "apply all params, environment and technical filters" do
269
+ params_hash = {:abc => 123}
270
+ environment_hash = {:def => 456}
271
+ backtrace_data = :backtrace_data
272
+
273
+ raw_notice = {:request => {:params => params_hash},
274
+ :environment => environment_hash,
275
+ :backtrace => backtrace_data}
276
+
277
+ processed_notice = {:backtrace => :backtrace_data,
278
+ :request => {:params => :params_data},
279
+ :environment => :environment_data}
280
+
281
+ @controller.expects(:clean_hoptoad_backtrace).with(backtrace_data).returns(:backtrace_data)
282
+ @controller.expects(:clean_hoptoad_params).with(params_hash).returns(:params_data)
283
+ @controller.expects(:clean_hoptoad_environment).with(environment_hash).returns(:environment_data)
284
+ @controller.expects(:clean_non_serializable_data).with(processed_notice).returns(:serializable_data)
285
+
286
+ assert_equal(:serializable_data, @controller.send(:clean_notice, raw_notice))
287
+ end
288
+
289
+ context "and configured to ignore additional exceptions" do
290
+ setup do
291
+ HoptoadNotifier.ignore << ActiveRecord::StatementInvalid
292
+ end
293
+
294
+ should "still ignore default exceptions" do
295
+ @controller.expects(:notify_hoptoad).never
296
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
297
+ assert_nothing_raised do
298
+ request("do_raise_ignored")
299
+ end
300
+ end
301
+
302
+ should "ignore specified exceptions" do
303
+ @controller.expects(:notify_hoptoad).never
304
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
305
+ assert_nothing_raised do
306
+ request("do_raise_not_ignored")
307
+ end
308
+ end
309
+
310
+ should "not ignore unspecified, non-default exceptions" do
311
+ @controller.expects(:notify_hoptoad)
312
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
313
+ assert_nothing_raised do
314
+ request("do_raise")
315
+ end
316
+ end
317
+ end
318
+
319
+ context "and configured to ignore only certain exceptions" do
320
+ setup do
321
+ HoptoadNotifier.ignore_only = [ActiveRecord::StatementInvalid]
322
+ end
323
+
324
+ should "no longer ignore default exceptions" do
325
+ @controller.expects(:notify_hoptoad)
326
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
327
+ assert_nothing_raised do
328
+ request("do_raise_ignored")
329
+ end
330
+ end
331
+
332
+ should "ignore specified exceptions" do
333
+ @controller.expects(:notify_hoptoad).never
334
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
335
+ assert_nothing_raised do
336
+ request("do_raise_not_ignored")
337
+ end
338
+ end
339
+
340
+ should "not ignore unspecified, non-default exceptions" do
341
+ @controller.expects(:notify_hoptoad)
342
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
343
+ assert_nothing_raised do
344
+ request("do_raise")
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+
351
+ context "Sending a notice" do
352
+ context "with an exception" do
353
+ setup do
354
+ @sender = HoptoadNotifier::Sender.new
355
+ @backtrace = caller
356
+ @exception = begin
357
+ raise
358
+ rescue => caught_exception
359
+ caught_exception
360
+ end
361
+ @options = {:error_message => "123",
362
+ :backtrace => @backtrace}
363
+ HoptoadNotifier.instance_variable_set("@backtrace_filters", [])
364
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
365
+ @sender.stubs(:public_environment?).returns(true)
366
+ end
367
+
368
+ context "when using an HTTP Proxy" do
369
+ setup do
370
+ @body = 'body'
371
+ @response = stub(:body => @body)
372
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
373
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
374
+ @proxy = stub
375
+ @proxy.stubs(:new).returns(@http)
376
+
377
+ HoptoadNotifier.port = nil
378
+ HoptoadNotifier.host = nil
379
+ HoptoadNotifier.secure = false
380
+
381
+ Net::HTTP.expects(:Proxy).with(
382
+ HoptoadNotifier.proxy_host,
383
+ HoptoadNotifier.proxy_port,
384
+ HoptoadNotifier.proxy_user,
385
+ HoptoadNotifier.proxy_pass
386
+ ).returns(@proxy)
387
+ end
388
+
389
+ context "on notify" do
390
+ setup { HoptoadNotifier.notify(@exception) }
391
+
392
+ before_should "post to Hoptoad" do
393
+ url = "http://hoptoadapp.com:80/notices/"
394
+ uri = URI.parse(url)
395
+ URI.expects(:parse).with(url).returns(uri)
396
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
397
+ end
398
+ end
399
+ end
400
+
401
+ context "when stubbing out Net::HTTP" do
402
+ setup do
403
+ @body = 'body'
404
+ @response = stub(:body => @body)
405
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
406
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
407
+ Net::HTTP.stubs(:new).returns(@http)
408
+ HoptoadNotifier.port = nil
409
+ HoptoadNotifier.host = nil
410
+ HoptoadNotifier.proxy_host = nil
411
+ end
412
+
413
+ context "on notify" do
414
+ setup { HoptoadNotifier.notify(@exception) }
415
+
416
+ before_should "post to the right url for non-ssl" do
417
+ HoptoadNotifier.secure = false
418
+ url = "http://hoptoadapp.com:80/notices/"
419
+ uri = URI.parse(url)
420
+ URI.expects(:parse).with(url).returns(uri)
421
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
422
+ end
423
+
424
+ before_should "post to the right path" do
425
+ @http.expects(:post).with("/notices/", anything, anything).returns(@response)
426
+ end
427
+
428
+ before_should "call send_to_hoptoad" do
429
+ @sender.expects(:send_to_hoptoad)
430
+ end
431
+
432
+ before_should "default the open timeout to 2 seconds" do
433
+ HoptoadNotifier.http_open_timeout = nil
434
+ @http.expects(:open_timeout=).with(2)
435
+ end
436
+
437
+ before_should "default the read timeout to 5 seconds" do
438
+ HoptoadNotifier.http_read_timeout = nil
439
+ @http.expects(:read_timeout=).with(5)
440
+ end
441
+
442
+ before_should "allow override of the open timeout" do
443
+ HoptoadNotifier.http_open_timeout = 4
444
+ @http.expects(:open_timeout=).with(4)
445
+ end
446
+
447
+ before_should "allow override of the read timeout" do
448
+ HoptoadNotifier.http_read_timeout = 10
449
+ @http.expects(:read_timeout=).with(10)
450
+ end
451
+
452
+ before_should "connect to the right port for ssl" do
453
+ HoptoadNotifier.secure = true
454
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 443).returns(@http)
455
+ end
456
+
457
+ before_should "connect to the right port for non-ssl" do
458
+ HoptoadNotifier.secure = false
459
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 80).returns(@http)
460
+ end
461
+
462
+ before_should "use ssl if secure" do
463
+ HoptoadNotifier.secure = true
464
+ HoptoadNotifier.host = 'example.org'
465
+ Net::HTTP.expects(:new).with('example.org', 443).returns(@http)
466
+ end
467
+
468
+ before_should "not use ssl if not secure" do
469
+ HoptoadNotifier.secure = nil
470
+ HoptoadNotifier.host = 'example.org'
471
+ Net::HTTP.expects(:new).with('example.org', 80).returns(@http)
472
+ end
473
+ end
474
+ end
475
+
476
+ should "send as if it were a normally caught exception" do
477
+ @sender.expects(:notify_hoptoad).with(@exception)
478
+ HoptoadNotifier.notify(@exception)
479
+ end
480
+
481
+ should "make sure the exception is munged into a hash" do
482
+ options = HoptoadNotifier.default_notice_options.merge({
483
+ :backtrace => @exception.backtrace,
484
+ :environment => ENV.to_hash,
485
+ :error_class => @exception.class.name,
486
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
487
+ :api_key => HoptoadNotifier.api_key,
488
+ })
489
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
490
+ HoptoadNotifier.notify(@exception)
491
+ end
492
+
493
+ should "parse massive one-line exceptions into multiple lines" do
494
+ @original_backtrace = "one big line\n separated\n by new lines\nand some spaces"
495
+ @expected_backtrace = ["one big line", "separated", "by new lines", "and some spaces"]
496
+ @exception.set_backtrace [@original_backtrace]
497
+
498
+ options = HoptoadNotifier.default_notice_options.merge({
499
+ :backtrace => @expected_backtrace,
500
+ :environment => ENV.to_hash,
501
+ :error_class => @exception.class.name,
502
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
503
+ :api_key => HoptoadNotifier.api_key,
504
+ })
505
+
506
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
507
+ HoptoadNotifier.notify(@exception)
508
+ end
509
+ end
510
+
511
+ context "without an exception" do
512
+ setup do
513
+ @sender = HoptoadNotifier::Sender.new
514
+ @backtrace = caller
515
+ @options = {:error_message => "123",
516
+ :backtrace => @backtrace}
517
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
518
+ end
519
+
520
+ should "send sensible defaults" do
521
+ @sender.expects(:notify_hoptoad).with(@options)
522
+ HoptoadNotifier.notify(:error_message => "123", :backtrace => @backtrace)
523
+ end
524
+ end
525
+ end
526
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pzingg-hoptoad_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Thoughtbot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-19 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