square-hoptoad_notifier 2.4.8

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