square-hoptoad_notifier 2.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/CHANGELOG +427 -0
  2. data/INSTALL +25 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README.md +435 -0
  5. data/README_FOR_HEROKU_ADDON.md +93 -0
  6. data/Rakefile +227 -0
  7. data/SUPPORTED_RAILS_VERSIONS +10 -0
  8. data/TESTING.rdoc +8 -0
  9. data/generators/hoptoad/hoptoad_generator.rb +88 -0
  10. data/generators/hoptoad/lib/insert_commands.rb +34 -0
  11. data/generators/hoptoad/lib/rake_commands.rb +24 -0
  12. data/generators/hoptoad/templates/capistrano_hook.rb +6 -0
  13. data/generators/hoptoad/templates/hoptoad_notifier_tasks.rake +25 -0
  14. data/generators/hoptoad/templates/initializer.rb +6 -0
  15. data/lib/hoptoad_notifier.rb +153 -0
  16. data/lib/hoptoad_notifier/backtrace.rb +99 -0
  17. data/lib/hoptoad_notifier/capistrano.rb +20 -0
  18. data/lib/hoptoad_notifier/configuration.rb +242 -0
  19. data/lib/hoptoad_notifier/notice.rb +337 -0
  20. data/lib/hoptoad_notifier/rack.rb +42 -0
  21. data/lib/hoptoad_notifier/rails.rb +41 -0
  22. data/lib/hoptoad_notifier/rails/action_controller_catcher.rb +30 -0
  23. data/lib/hoptoad_notifier/rails/controller_methods.rb +68 -0
  24. data/lib/hoptoad_notifier/rails/error_lookup.rb +33 -0
  25. data/lib/hoptoad_notifier/rails/javascript_notifier.rb +42 -0
  26. data/lib/hoptoad_notifier/rails3_tasks.rb +82 -0
  27. data/lib/hoptoad_notifier/railtie.rb +32 -0
  28. data/lib/hoptoad_notifier/sender.rb +83 -0
  29. data/lib/hoptoad_notifier/shared_tasks.rb +29 -0
  30. data/lib/hoptoad_notifier/tasks.rb +83 -0
  31. data/lib/hoptoad_notifier/user_informer.rb +23 -0
  32. data/lib/hoptoad_notifier/version.rb +3 -0
  33. data/lib/hoptoad_tasks.rb +44 -0
  34. data/lib/rails/generators/hoptoad/hoptoad_generator.rb +94 -0
  35. data/lib/templates/javascript_notifier.erb +13 -0
  36. data/lib/templates/rescue.erb +91 -0
  37. data/rails/init.rb +1 -0
  38. data/script/integration_test.rb +38 -0
  39. data/test/backtrace_test.rb +118 -0
  40. data/test/catcher_test.rb +331 -0
  41. data/test/configuration_test.rb +216 -0
  42. data/test/helper.rb +248 -0
  43. data/test/hoptoad_tasks_test.rb +152 -0
  44. data/test/javascript_notifier_test.rb +52 -0
  45. data/test/logger_test.rb +85 -0
  46. data/test/notice_test.rb +448 -0
  47. data/test/notifier_test.rb +222 -0
  48. data/test/rack_test.rb +58 -0
  49. data/test/rails_initializer_test.rb +36 -0
  50. data/test/sender_test.rb +161 -0
  51. data/test/user_informer_test.rb +29 -0
  52. metadata +225 -0
@@ -0,0 +1,337 @@
1
+ require 'builder'
2
+
3
+ module HoptoadNotifier
4
+ class Notice
5
+
6
+ # The exception that caused this notice, if any
7
+ attr_reader :exception
8
+
9
+ # The API key for the project to which this notice should be sent
10
+ attr_reader :api_key
11
+
12
+ # The backtrace from the given exception or hash.
13
+ attr_reader :backtrace
14
+
15
+ # The name of the class of error (such as RuntimeError)
16
+ attr_reader :error_class
17
+
18
+ # The name of the server environment (such as "production")
19
+ attr_reader :environment_name
20
+
21
+ # CGI variables such as HTTP_METHOD
22
+ attr_reader :cgi_data
23
+
24
+ # The message from the exception, or a general description of the error
25
+ attr_reader :error_message
26
+
27
+ # See Configuration#backtrace_filters
28
+ attr_reader :backtrace_filters
29
+
30
+ # See Configuration#params_filters
31
+ attr_reader :params_filters
32
+
33
+ # A hash of parameters from the query string or post body.
34
+ attr_reader :parameters
35
+ alias_method :params, :parameters
36
+
37
+ # The component (if any) which was used in this request (usually the controller)
38
+ attr_reader :component
39
+ alias_method :controller, :component
40
+
41
+ # The action (if any) that was called in this request
42
+ attr_reader :action
43
+
44
+ # A hash of session data from the request
45
+ attr_reader :session_data
46
+
47
+ # The path to the project that caused the error (usually RAILS_ROOT)
48
+ attr_reader :project_root
49
+
50
+ # The URL at which the error occurred (if any)
51
+ attr_reader :url
52
+
53
+ # See Configuration#ignore
54
+ attr_reader :ignore
55
+
56
+ # See Configuration#ignore_by_filters
57
+ attr_reader :ignore_by_filters
58
+
59
+ # The name of the notifier library sending this notice, such as "Hoptoad Notifier"
60
+ attr_reader :notifier_name
61
+
62
+ # The version number of the notifier library sending this notice, such as "2.1.3"
63
+ attr_reader :notifier_version
64
+
65
+ # A URL for more information about the notifier library sending this notice
66
+ attr_reader :notifier_url
67
+
68
+ def initialize(args)
69
+ self.args = args
70
+ self.exception = args[:exception]
71
+ self.api_key = args[:api_key]
72
+ self.project_root = args[:project_root]
73
+ self.url = args[:url] || rack_env(:url)
74
+
75
+ self.notifier_name = args[:notifier_name]
76
+ self.notifier_version = args[:notifier_version]
77
+ self.notifier_url = args[:notifier_url]
78
+
79
+ self.ignore = args[:ignore] || []
80
+ self.ignore_by_filters = args[:ignore_by_filters] || []
81
+ self.backtrace_filters = args[:backtrace_filters] || []
82
+ self.params_filters = args[:params_filters] || []
83
+ self.parameters = args[:parameters] ||
84
+ action_dispatch_params ||
85
+ rack_env(:params) ||
86
+ {}
87
+ self.component = args[:component] || args[:controller] || parameters['controller']
88
+ self.action = args[:action] || parameters['action']
89
+
90
+ self.environment_name = args[:environment_name]
91
+ self.cgi_data = args[:cgi_data] || args[:rack_env]
92
+ self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters)
93
+ self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
94
+ self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
95
+ "#{exception.class.name}: #{exception.message}"
96
+ end
97
+
98
+ also_use_rack_params_filters
99
+ find_session_data
100
+ clean_params
101
+ clean_rack_request_data
102
+ end
103
+
104
+ # Converts the given notice to XML
105
+ def to_xml
106
+ builder = Builder::XmlMarkup.new
107
+ builder.instruct!
108
+ xml = builder.notice(:version => HoptoadNotifier::API_VERSION) do |notice|
109
+ notice.tag!("api-key", api_key)
110
+ notice.notifier do |notifier|
111
+ notifier.name(notifier_name)
112
+ notifier.version(notifier_version)
113
+ notifier.url(notifier_url)
114
+ end
115
+ notice.error do |error|
116
+ error.tag!('class', error_class)
117
+ error.message(error_message)
118
+ error.backtrace do |backtrace|
119
+ self.backtrace.lines.each do |line|
120
+ backtrace.line(:number => line.number,
121
+ :file => line.file,
122
+ :method => line.method)
123
+ end
124
+ end
125
+ end
126
+ if url ||
127
+ controller ||
128
+ action ||
129
+ !parameters.blank? ||
130
+ !cgi_data.blank? ||
131
+ !session_data.blank?
132
+ notice.request do |request|
133
+ request.url(url)
134
+ request.component(controller)
135
+ request.action(action)
136
+ unless parameters.nil? || parameters.empty?
137
+ request.params do |params|
138
+ xml_vars_for(params, parameters)
139
+ end
140
+ end
141
+ unless session_data.nil? || session_data.empty?
142
+ request.session do |session|
143
+ xml_vars_for(session, session_data)
144
+ end
145
+ end
146
+ unless cgi_data.nil? || cgi_data.empty?
147
+ request.tag!("cgi-data") do |cgi_datum|
148
+ xml_vars_for(cgi_datum, cgi_data)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ notice.tag!("server-environment") do |env|
154
+ env.tag!("project-root", project_root)
155
+ env.tag!("environment-name", environment_name)
156
+ end
157
+ end
158
+ xml.to_s
159
+ end
160
+
161
+ # Determines if this notice should be ignored
162
+ def ignore?
163
+ ignored_class_names.include?(error_class) ||
164
+ ignore_by_filters.any? {|filter| filter.call(self) }
165
+ end
166
+
167
+ # Allows properties to be accessed using a hash-like syntax
168
+ #
169
+ # @example
170
+ # notice[:error_message]
171
+ # @param [String] method The given key for an attribute
172
+ # @return The attribute value, or self if given +:request+
173
+ def [](method)
174
+ case method
175
+ when :request
176
+ self
177
+ else
178
+ send(method)
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
185
+ :backtrace_filters, :parameters, :params_filters,
186
+ :environment_filters, :session_data, :project_root, :url, :ignore,
187
+ :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
188
+ :component, :action, :cgi_data, :environment_name
189
+
190
+ # Arguments given in the initializer
191
+ attr_accessor :args
192
+
193
+ # Gets a property named +attribute+ of an exception, either from an actual
194
+ # exception or a hash.
195
+ #
196
+ # If an exception is available, #from_exception will be used. Otherwise,
197
+ # a key named +attribute+ will be used from the #args.
198
+ #
199
+ # If no exception or hash key is available, +default+ will be used.
200
+ def exception_attribute(attribute, default = nil, &block)
201
+ (exception && from_exception(attribute, &block)) || args[attribute] || default
202
+ end
203
+
204
+ # Gets a property named +attribute+ from an exception.
205
+ #
206
+ # If a block is given, it will be used when getting the property from an
207
+ # exception. The block should accept and exception and return the value for
208
+ # the property.
209
+ #
210
+ # If no block is given, a method with the same name as +attribute+ will be
211
+ # invoked for the value.
212
+ def from_exception(attribute)
213
+ if block_given?
214
+ yield(exception)
215
+ else
216
+ exception.send(attribute)
217
+ end
218
+ end
219
+
220
+ # Removes non-serializable data from the given attribute.
221
+ # See #clean_unserializable_data
222
+ def clean_unserializable_data_from(attribute)
223
+ self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
224
+ end
225
+
226
+ # Removes non-serializable data. Allowed data types are strings, arrays,
227
+ # and hashes. All other types are converted to strings.
228
+ # TODO: move this onto Hash
229
+ def clean_unserializable_data(data, stack = [])
230
+ return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id }
231
+
232
+ if data.respond_to?(:to_hash)
233
+ data.to_hash.inject({}) do |result, (key, value)|
234
+ result.merge(key => clean_unserializable_data(value, stack + [data.object_id]))
235
+ end
236
+ elsif data.respond_to?(:to_ary)
237
+ data.collect do |value|
238
+ clean_unserializable_data(value, stack + [data.object_id])
239
+ end
240
+ else
241
+ data.to_s
242
+ end
243
+ end
244
+
245
+ # Replaces the contents of params that match params_filters.
246
+ # TODO: extract this to a different class
247
+ def clean_params
248
+ clean_unserializable_data_from(:parameters)
249
+ filter(parameters)
250
+ if cgi_data
251
+ clean_unserializable_data_from(:cgi_data)
252
+ filter(cgi_data)
253
+ end
254
+ if session_data
255
+ clean_unserializable_data_from(:session_data)
256
+ filter(session_data)
257
+ end
258
+ end
259
+
260
+ def clean_rack_request_data
261
+ if cgi_data
262
+ cgi_data.delete("rack.request.form_vars")
263
+ end
264
+ end
265
+
266
+ def filter(hash)
267
+ if params_filters
268
+ hash.each do |key, value|
269
+ if filter_key?(key)
270
+ hash[key] = "[FILTERED]"
271
+ elsif value.respond_to?(:to_hash)
272
+ filter(hash[key])
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ def filter_key?(key)
279
+ params_filters.any? do |filter|
280
+ key.to_s.include?(filter.to_s)
281
+ end
282
+ end
283
+
284
+ def find_session_data
285
+ self.session_data = args[:session_data] || args[:session] || rack_session || {}
286
+ self.session_data = session_data[:data] if session_data[:data]
287
+ end
288
+
289
+ # Converts the mixed class instances and class names into just names
290
+ # TODO: move this into Configuration or another class
291
+ def ignored_class_names
292
+ ignore.collect do |string_or_class|
293
+ if string_or_class.respond_to?(:name)
294
+ string_or_class.name
295
+ else
296
+ string_or_class
297
+ end
298
+ end
299
+ end
300
+
301
+ def xml_vars_for(builder, hash)
302
+ hash.each do |key, value|
303
+ if value.respond_to?(:to_hash)
304
+ builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) }
305
+ else
306
+ builder.var(value.to_s, :key => key)
307
+ end
308
+ end
309
+ end
310
+
311
+ def rack_env(method)
312
+ rack_request.send(method) if rack_request
313
+ end
314
+
315
+ def rack_request
316
+ @rack_request ||= if args[:rack_env]
317
+ ::Rack::Request.new(args[:rack_env])
318
+ end
319
+ end
320
+
321
+ def action_dispatch_params
322
+ args[:rack_env]['action_dispatch.request.parameters'] if args[:rack_env]
323
+ end
324
+
325
+ def rack_session
326
+ args[:rack_env]['rack.session'] if args[:rack_env]
327
+ end
328
+
329
+ def also_use_rack_params_filters
330
+ if args[:rack_env]
331
+ @params_filters ||= []
332
+ @params_filters += rack_request.env["action_dispatch.parameter_filter"] || []
333
+ end
334
+ end
335
+
336
+ end
337
+ end
@@ -0,0 +1,42 @@
1
+ module HoptoadNotifier
2
+ # Middleware for Rack applications. Any errors raised by the upstream
3
+ # application will be delivered to Hoptoad and re-raised.
4
+ #
5
+ # Synopsis:
6
+ #
7
+ # require 'rack'
8
+ # require 'hoptoad_notifier'
9
+ #
10
+ # HoptoadNotifier.configure do |config|
11
+ # config.api_key = 'my_api_key'
12
+ # end
13
+ #
14
+ # app = Rack::Builder.app do
15
+ # use HoptoadNotifier::Rack
16
+ # run lambda { |env| raise "Rack down" }
17
+ # end
18
+ #
19
+ # Use a standard HoptoadNotifier.configure call to configure your api key.
20
+ class Rack
21
+ def initialize(app)
22
+ @app = app
23
+ end
24
+
25
+ def call(env)
26
+ begin
27
+ response = @app.call(env)
28
+ rescue Exception => raised
29
+ error_id = HoptoadNotifier.notify_or_ignore(raised, :rack_env => env)
30
+ env['hoptoad.error_id'] = error_id
31
+ raise
32
+ end
33
+
34
+ if env['rack.exception']
35
+ error_id = HoptoadNotifier.notify_or_ignore(env['rack.exception'], :rack_env => env)
36
+ env['hoptoad.error_id'] = error_id
37
+ end
38
+
39
+ response
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require 'hoptoad_notifier'
2
+ require 'hoptoad_notifier/rails/controller_methods'
3
+ require 'hoptoad_notifier/rails/action_controller_catcher'
4
+ require 'hoptoad_notifier/rails/error_lookup'
5
+ require 'hoptoad_notifier/rails/javascript_notifier'
6
+
7
+ module HoptoadNotifier
8
+ module Rails
9
+ def self.initialize
10
+ if defined?(ActionController::Base)
11
+ ActionController::Base.send(:include, HoptoadNotifier::Rails::ActionControllerCatcher)
12
+ ActionController::Base.send(:include, HoptoadNotifier::Rails::ErrorLookup)
13
+ ActionController::Base.send(:include, HoptoadNotifier::Rails::ControllerMethods)
14
+ ActionController::Base.send(:include, HoptoadNotifier::Rails::JavascriptNotifier)
15
+ end
16
+
17
+ rails_logger = if defined?(::Rails.logger)
18
+ ::Rails.logger
19
+ elsif defined?(RAILS_DEFAULT_LOGGER)
20
+ RAILS_DEFAULT_LOGGER
21
+ end
22
+
23
+ if defined?(::Rails.configuration) && ::Rails.configuration.respond_to?(:middleware)
24
+ ::Rails.configuration.middleware.insert_after 'ActionController::Failsafe',
25
+ HoptoadNotifier::Rack
26
+ ::Rails.configuration.middleware.insert_after 'Rack::Lock',
27
+ HoptoadNotifier::UserInformer
28
+ end
29
+
30
+ HoptoadNotifier.configure(true) do |config|
31
+ config.logger = rails_logger
32
+ config.environment_name = RAILS_ENV if defined?(RAILS_ENV)
33
+ config.project_root = RAILS_ROOT if defined?(RAILS_ROOT)
34
+ config.framework = "Rails: #{::Rails::VERSION::STRING}" if defined?(::Rails::VERSION)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ HoptoadNotifier::Rails.initialize
41
+
@@ -0,0 +1,30 @@
1
+ module HoptoadNotifier
2
+ module Rails
3
+ module ActionControllerCatcher
4
+
5
+ # Sets up an alias chain to catch exceptions when Rails does
6
+ def self.included(base) #:nodoc:
7
+ base.send(:alias_method, :rescue_action_in_public_without_hoptoad, :rescue_action_in_public)
8
+ base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_hoptoad)
9
+ end
10
+
11
+ private
12
+
13
+ # Overrides the rescue_action method in ActionController::Base, but does not inhibit
14
+ # any custom processing that is defined with Rails 2's exception helpers.
15
+ def rescue_action_in_public_with_hoptoad(exception)
16
+ unless hoptoad_ignore_user_agent?
17
+ error_id = HoptoadNotifier.notify_or_ignore(exception, hoptoad_request_data)
18
+ request.env['hoptoad.error_id'] = error_id
19
+ end
20
+ rescue_action_in_public_without_hoptoad(exception)
21
+ end
22
+
23
+ def hoptoad_ignore_user_agent? #:nodoc:
24
+ # Rails 1.2.6 doesn't have request.user_agent, so check for it here
25
+ user_agent = request.respond_to?(:user_agent) ? request.user_agent : request.env["HTTP_USER_AGENT"]
26
+ HoptoadNotifier.configuration.ignore_user_agent.flatten.any? { |ua| ua === user_agent }
27
+ end
28
+ end
29
+ end
30
+ end