hoptoad_notifier 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,95 @@
1
+ module HoptoadNotifier
2
+ # Include this module in Controllers in which you want to be notified of errors.
3
+ module Catcher
4
+
5
+ # Sets up an alias chain to catch exceptions when Rails does
6
+ def self.included(base) #:nodoc:
7
+ if base.instance_methods.map(&:to_s).include? 'rescue_action_in_public' and !base.instance_methods.map(&:to_s).include? 'rescue_action_in_public_without_hoptoad'
8
+ base.send(:alias_method, :rescue_action_in_public_without_hoptoad, :rescue_action_in_public)
9
+ base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_hoptoad)
10
+ base.send(:alias_method, :rescue_action_locally_without_hoptoad, :rescue_action_locally)
11
+ base.send(:alias_method, :rescue_action_locally, :rescue_action_locally_with_hoptoad)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ # Overrides the rescue_action method in ActionController::Base, but does not inhibit
18
+ # any custom processing that is defined with Rails 2's exception helpers.
19
+ def rescue_action_in_public_with_hoptoad(exception)
20
+ unless hoptoad_ignore_user_agent?
21
+ HoptoadNotifier.notify_or_ignore(exception, hoptoad_request_data)
22
+ end
23
+ rescue_action_in_public_without_hoptoad(exception)
24
+ end
25
+
26
+ def rescue_action_locally_with_hoptoad(exception)
27
+ result = rescue_action_locally_without_hoptoad(exception)
28
+
29
+ if HoptoadNotifier.configuration.development_lookup
30
+ path = File.join(File.dirname(__FILE__), '..', 'templates', 'rescue.erb')
31
+ notice = HoptoadNotifier.build_lookup_hash_for(exception, hoptoad_request_data)
32
+
33
+ result << @template.render(
34
+ :file => path,
35
+ :use_full_path => false,
36
+ :locals => { :host => HoptoadNotifier.configuration.host,
37
+ :api_key => HoptoadNotifier.configuration.api_key,
38
+ :notice => notice })
39
+ end
40
+
41
+ result
42
+ end
43
+
44
+ # This method should be used for sending manual notifications while you are still
45
+ # inside the controller. Otherwise it works like HoptoadNotifier.notify.
46
+ def notify_hoptoad(hash_or_exception)
47
+ unless consider_all_requests_local || local_request?
48
+ HoptoadNotifier.notify(hash_or_exception, hoptoad_request_data)
49
+ end
50
+ end
51
+
52
+ def hoptoad_ignore_user_agent? #:nodoc:
53
+ # Rails 1.2.6 doesn't have request.user_agent, so check for it here
54
+ user_agent = request.respond_to?(:user_agent) ? request.user_agent : request.env["HTTP_USER_AGENT"]
55
+ HoptoadNotifier.configuration.ignore_user_agent.flatten.any? { |ua| ua === user_agent }
56
+ end
57
+
58
+ def hoptoad_request_data
59
+ { :parameters => hoptoad_filter_if_filtering(params.to_hash),
60
+ :session_data => hoptoad_session_data,
61
+ :controller => params[:controller],
62
+ :action => params[:action],
63
+ :url => hoptoad_request_url,
64
+ :cgi_data => hoptoad_filter_if_filtering(request.env),
65
+ :environment_vars => hoptoad_filter_if_filtering(ENV) }
66
+ end
67
+
68
+ def hoptoad_filter_if_filtering(hash)
69
+ if respond_to?(:filter_parameters)
70
+ filter_parameters(hash) rescue hash
71
+ else
72
+ hash
73
+ end
74
+ end
75
+
76
+ def hoptoad_session_data
77
+ if session.respond_to?(:to_hash)
78
+ session.to_hash
79
+ else
80
+ session.data
81
+ end
82
+ end
83
+
84
+ def hoptoad_request_url
85
+ url = "#{request.protocol}#{request.host}"
86
+
87
+ unless [80, 443].include?(request.port)
88
+ url << ":#{request.port}"
89
+ end
90
+
91
+ url << request.request_uri
92
+ url
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,228 @@
1
+ module HoptoadNotifier
2
+ # Used to set up and modify settings for the notifier.
3
+ class Configuration
4
+
5
+ OPTIONS = [:api_key, :backtrace_filters, :development_environments,
6
+ :development_lookup, :environment_name, :host,
7
+ :http_open_timeout, :http_read_timeout, :ignore, :ignore_by_filters,
8
+ :ignore_user_agent, :notifier_name, :notifier_url, :notifier_version,
9
+ :params_filters, :project_root, :port, :protocol, :proxy_host,
10
+ :proxy_pass, :proxy_port, :proxy_user, :secure].freeze
11
+
12
+ # The API key for your project, found on the project edit form.
13
+ attr_accessor :api_key
14
+
15
+ # The host to connect to (defaults to hoptoadapp.com).
16
+ attr_accessor :host
17
+
18
+ # The port on which your Hoptoad server runs (defaults to 443 for secure
19
+ # connections, 80 for insecure connections).
20
+ attr_accessor :port
21
+
22
+ # +true+ for https connections, +false+ for http connections.
23
+ attr_accessor :secure
24
+
25
+ # The HTTP open timeout in seconds (defaults to 2).
26
+ attr_accessor :http_open_timeout
27
+
28
+ # The HTTP read timeout in seconds (defaults to 5).
29
+ attr_accessor :http_read_timeout
30
+
31
+ # The hostname of your proxy server (if using a proxy)
32
+ attr_accessor :proxy_host
33
+
34
+ # The port of your proxy server (if using a proxy)
35
+ attr_accessor :proxy_port
36
+
37
+ # The username to use when logging into your proxy server (if using a proxy)
38
+ attr_accessor :proxy_user
39
+
40
+ # The password to use when logging into your proxy server (if using a proxy)
41
+ attr_accessor :proxy_pass
42
+
43
+ # A list of parameters that should be filtered out of what is sent to Hoptoad.
44
+ # By default, all "password" attributes will have their contents replaced.
45
+ attr_reader :params_filters
46
+
47
+ # A list of filters for cleaning and pruning the backtrace. See #filter_backtrace.
48
+ attr_reader :backtrace_filters
49
+
50
+ # A list of filters for ignoring exceptions. See #ignore_by_filter.
51
+ attr_reader :ignore_by_filters
52
+
53
+ # A list of exception classes to ignore. The array can be appended to.
54
+ attr_reader :ignore
55
+
56
+ # A list of user agents that are being ignored. The array can be appended to.
57
+ attr_reader :ignore_user_agent
58
+
59
+ # A list of environments in which notifications should not be sent.
60
+ attr_accessor :development_environments
61
+
62
+ # +true+ if you want to check for production errors matching development errors, +false+ otherwise.
63
+ attr_accessor :development_lookup
64
+
65
+ # The name of the environment the application is running in
66
+ attr_accessor :environment_name
67
+
68
+ # The path to the project in which the error occurred, such as the RAILS_ROOT
69
+ attr_accessor :project_root
70
+
71
+ # The name of the notifier library being used to send notifications (such as "Hoptoad Notifier")
72
+ attr_accessor :notifier_name
73
+
74
+ # The version of the notifier library being used to send notifications (such as "1.0.2")
75
+ attr_accessor :notifier_version
76
+
77
+ # The url of the notifier library being used to send notifications
78
+ attr_accessor :notifier_url
79
+
80
+ # The logger used by HoptoadNotifier
81
+ attr_accessor :logger
82
+
83
+ DEFAULT_PARAMS_FILTERS = %w(password password_confirmation).freeze
84
+
85
+ DEFAULT_BACKTRACE_FILTERS = [
86
+ lambda { |line|
87
+ if defined?(HoptoadNotifier.configuration.project_root) && HoptoadNotifier.configuration.project_root.to_s != ''
88
+ line.gsub(/#{HoptoadNotifier.configuration.project_root}/, "[PROJECT_ROOT]")
89
+ else
90
+ line
91
+ end
92
+ },
93
+ lambda { |line| line.gsub(/^\.\//, "") },
94
+ lambda { |line|
95
+ if defined?(Gem)
96
+ Gem.path.inject(line) do |line, path|
97
+ line.gsub(/#{path}/, "[GEM_ROOT]")
98
+ end
99
+ end
100
+ },
101
+ lambda { |line| line if line !~ %r{lib/hoptoad_notifier} }
102
+ ].freeze
103
+
104
+ IGNORE_DEFAULT = ['ActiveRecord::RecordNotFound',
105
+ 'ActionController::RoutingError',
106
+ 'ActionController::InvalidAuthenticityToken',
107
+ 'CGI::Session::CookieStore::TamperedWithCookie',
108
+ 'ActionController::UnknownAction']
109
+
110
+ alias_method :secure?, :secure
111
+
112
+ def initialize
113
+ @secure = false
114
+ @host = 'hoptoadapp.com'
115
+ @http_open_timeout = 2
116
+ @http_read_timeout = 5
117
+ @params_filters = DEFAULT_PARAMS_FILTERS.dup
118
+ @backtrace_filters = DEFAULT_BACKTRACE_FILTERS.dup
119
+ @ignore_by_filters = []
120
+ @ignore = IGNORE_DEFAULT.dup
121
+ @ignore_user_agent = []
122
+ @development_environments = %w(development test cucumber)
123
+ @development_lookup = true
124
+ @notifier_name = 'Hoptoad Notifier'
125
+ @notifier_version = VERSION
126
+ @notifier_url = 'http://hoptoadapp.com'
127
+ end
128
+
129
+ # Takes a block and adds it to the list of backtrace filters. When the filters
130
+ # run, the block will be handed each line of the backtrace and can modify
131
+ # it as necessary.
132
+ #
133
+ # @example
134
+ # config.filter_bracktrace do |line|
135
+ # line.gsub(/^#{Rails.root}/, "[RAILS_ROOT]")
136
+ # end
137
+ #
138
+ # @param [Proc] block The new backtrace filter.
139
+ # @yieldparam [String] line A line in the backtrace.
140
+ def filter_backtrace(&block)
141
+ self.backtrace_filters << block
142
+ end
143
+
144
+ # Takes a block and adds it to the list of ignore filters.
145
+ # When the filters run, the block will be handed the exception.
146
+ # @example
147
+ # config.ignore_by_filter do |exception_data|
148
+ # true if exception_data[:error_class] == "RuntimeError"
149
+ # end
150
+ #
151
+ # @param [Proc] block The new ignore filter
152
+ # @yieldparam [Hash] data The exception data given to +HoptoadNotifier.notify+
153
+ # @yieldreturn [Boolean] If the block returns true the exception will be ignored, otherwise it will be processed by hoptoad.
154
+ def ignore_by_filter(&block)
155
+ self.ignore_by_filters << block
156
+ end
157
+
158
+ # Overrides the list of default ignored errors.
159
+ #
160
+ # @param [Array<Exception>] names A list of exceptions to ignore.
161
+ def ignore_only=(names)
162
+ @ignore = [names].flatten
163
+ end
164
+
165
+ # Overrides the list of default ignored user agents
166
+ #
167
+ # @param [Array<String>] A list of user agents to ignore
168
+ def ignore_user_agent_only=(names)
169
+ @ignore_user_agent = [names].flatten
170
+ end
171
+
172
+ # Allows config options to be read like a hash
173
+ #
174
+ # @param [Symbol] option Key for a given attribute
175
+ def [](option)
176
+ send(option)
177
+ end
178
+
179
+ # Returns a hash of all configurable options
180
+ def to_hash
181
+ OPTIONS.inject({}) do |hash, option|
182
+ hash.merge(option.to_sym => send(option))
183
+ end
184
+ end
185
+
186
+ # Returns a hash of all configurable options merged with +hash+
187
+ #
188
+ # @param [Hash] hash A set of configuration options that will take precedence over the defaults
189
+ def merge(hash)
190
+ to_hash.merge(hash)
191
+ end
192
+
193
+ # Determines if the notifier will send notices.
194
+ # @return [Boolean] Returns +false+ if in a development environment, +true+ otherwise.
195
+ def public?
196
+ !development_environments.include?(environment_name)
197
+ end
198
+
199
+ def port
200
+ @port || default_port
201
+ end
202
+
203
+ def protocol
204
+ if secure?
205
+ 'https'
206
+ else
207
+ 'http'
208
+ end
209
+ end
210
+
211
+ def environment_filters
212
+ warn 'config.environment_filters has been deprecated and has no effect.'
213
+ []
214
+ end
215
+
216
+ private
217
+
218
+ def default_port
219
+ if secure?
220
+ 443
221
+ else
222
+ 80
223
+ end
224
+ end
225
+
226
+ end
227
+
228
+ end
@@ -0,0 +1,295 @@
1
+ module HoptoadNotifier
2
+ class Notice
3
+
4
+ # The exception that caused this notice, if any
5
+ attr_reader :exception
6
+
7
+ # The API key for the project to which this notice should be sent
8
+ attr_reader :api_key
9
+
10
+ # The backtrace from the given exception or hash.
11
+ attr_reader :backtrace
12
+
13
+ # The name of the class of error (such as RuntimeError)
14
+ attr_reader :error_class
15
+
16
+ # The name of the server environment (such as "production")
17
+ attr_reader :environment_name
18
+
19
+ # CGI variables such as HTTP_METHOD
20
+ attr_reader :cgi_data
21
+
22
+ # The message from the exception, or a general description of the error
23
+ attr_reader :error_message
24
+
25
+ # See Configuration#backtrace_filters
26
+ attr_reader :backtrace_filters
27
+
28
+ # See Configuration#params_filters
29
+ attr_reader :params_filters
30
+
31
+ # A hash of parameters from the query string or post body.
32
+ attr_reader :parameters
33
+ alias_method :params, :parameters
34
+
35
+ # The component (if any) which was used in this request (usually the controller)
36
+ attr_reader :component
37
+ alias_method :controller, :component
38
+
39
+ # The action (if any) that was called in this request
40
+ attr_reader :action
41
+
42
+ # A hash of session data from the request
43
+ attr_reader :session_data
44
+
45
+ # The path to the project that caused the error (usually RAILS_ROOT)
46
+ attr_reader :project_root
47
+
48
+ # The URL at which the error occurred (if any)
49
+ attr_reader :url
50
+
51
+ # See Configuration#ignore
52
+ attr_reader :ignore
53
+
54
+ # See Configuration#ignore_by_filters
55
+ attr_reader :ignore_by_filters
56
+
57
+ # The name of the notifier library sending this notice, such as "Hoptoad Notifier"
58
+ attr_reader :notifier_name
59
+
60
+ # The version number of the notifier library sending this notice, such as "2.1.3"
61
+ attr_reader :notifier_version
62
+
63
+ # A URL for more information about the notifier library sending this notice
64
+ attr_reader :notifier_url
65
+
66
+ def initialize(args)
67
+ self.args = args
68
+ self.exception = args[:exception]
69
+ self.api_key = args[:api_key]
70
+ self.project_root = args[:project_root]
71
+ self.url = args[:url]
72
+
73
+ self.notifier_name = args[:notifier_name]
74
+ self.notifier_version = args[:notifier_version]
75
+ self.notifier_url = args[:notifier_url]
76
+
77
+ self.ignore = args[:ignore] || []
78
+ self.ignore_by_filters = args[:ignore_by_filters] || []
79
+ self.backtrace_filters = args[:backtrace_filters] || []
80
+ self.params_filters = args[:params_filters] || []
81
+ self.parameters = args[:parameters] || {}
82
+ self.component = args[:component] || args[:controller]
83
+ self.action = args[:action]
84
+
85
+ self.environment_name = args[:environment_name]
86
+ self.cgi_data = args[:cgi_data]
87
+ self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters)
88
+ self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
89
+ self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
90
+ "#{exception.class.name}: #{exception.message}"
91
+ end
92
+
93
+ find_session_data
94
+ clean_params
95
+ end
96
+
97
+ # Converts the given notice to XML
98
+ def to_xml
99
+ builder = Builder::XmlMarkup.new
100
+ builder.instruct!
101
+ xml = builder.notice(:version => HoptoadNotifier::API_VERSION) do |notice|
102
+ notice.tag!("api-key", api_key)
103
+ notice.notifier do |notifier|
104
+ notifier.name(notifier_name)
105
+ notifier.version(notifier_version)
106
+ notifier.url(notifier_url)
107
+ end
108
+ notice.error do |error|
109
+ error.tag!('class', error_class)
110
+ error.message(error_message)
111
+ error.backtrace do |backtrace|
112
+ self.backtrace.lines.each do |line|
113
+ backtrace.line(:number => line.number,
114
+ :file => line.file,
115
+ :method => line.method)
116
+ end
117
+ end
118
+ end
119
+ if url ||
120
+ controller ||
121
+ action ||
122
+ !parameters.blank? ||
123
+ !cgi_data.blank? ||
124
+ !session_data.blank?
125
+ notice.request do |request|
126
+ request.url(url)
127
+ request.component(controller)
128
+ request.action(action)
129
+ unless parameters.blank?
130
+ request.params do |params|
131
+ xml_vars_for(params, parameters)
132
+ end
133
+ end
134
+ unless session_data.blank?
135
+ request.session do |session|
136
+ xml_vars_for(session, session_data)
137
+ end
138
+ end
139
+ unless cgi_data.blank?
140
+ request.tag!("cgi-data") do |cgi_datum|
141
+ xml_vars_for(cgi_datum, cgi_data)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ notice.tag!("server-environment") do |env|
147
+ env.tag!("project-root", project_root)
148
+ env.tag!("environment-name", environment_name)
149
+ end
150
+ end
151
+ xml.to_s
152
+ end
153
+
154
+ # Determines if this notice should be ignored
155
+ def ignore?
156
+ ignored_class_names.include?(error_class) ||
157
+ ignore_by_filters.any? {|filter| filter.call(self) }
158
+ end
159
+
160
+ # Allows properties to be accessed using a hash-like syntax
161
+ #
162
+ # @example
163
+ # notice[:error_message]
164
+ # @param [String] method The given key for an attribute
165
+ # @return The attribute value, or self if given +:request+
166
+ def [](method)
167
+ case method
168
+ when :request
169
+ self
170
+ else
171
+ send(method)
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
178
+ :backtrace_filters, :parameters, :params_filters,
179
+ :environment_filters, :session_data, :project_root, :url, :ignore,
180
+ :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
181
+ :component, :action, :cgi_data, :environment_name
182
+
183
+ # Arguments given in the initializer
184
+ attr_accessor :args
185
+
186
+ # Gets a property named +attribute+ of an exception, either from an actual
187
+ # exception or a hash.
188
+ #
189
+ # If an exception is available, #from_exception will be used. Otherwise,
190
+ # a key named +attribute+ will be used from the #args.
191
+ #
192
+ # If no exception or hash key is available, +default+ will be used.
193
+ def exception_attribute(attribute, default = nil, &block)
194
+ (exception && from_exception(attribute, &block)) || args[attribute] || default
195
+ end
196
+
197
+ # Gets a property named +attribute+ from an exception.
198
+ #
199
+ # If a block is given, it will be used when getting the property from an
200
+ # exception. The block should accept and exception and return the value for
201
+ # the property.
202
+ #
203
+ # If no block is given, a method with the same name as +attribute+ will be
204
+ # invoked for the value.
205
+ def from_exception(attribute)
206
+ if block_given?
207
+ yield(exception)
208
+ else
209
+ exception.send(attribute)
210
+ end
211
+ end
212
+
213
+ # Removes non-serializable data from the given attribute.
214
+ # See #clean_unserializable_data
215
+ def clean_unserializable_data_from(attribute)
216
+ self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
217
+ end
218
+
219
+ # Removes non-serializable data. Allowed data types are strings, arrays,
220
+ # and hashes. All other types are converted to strings.
221
+ # TODO: move this onto Hash
222
+ def clean_unserializable_data(data)
223
+ if data.respond_to?(:to_hash)
224
+ data.to_hash.inject({}) do |result, (key, value)|
225
+ result.merge(key => clean_unserializable_data(value))
226
+ end
227
+ elsif data.respond_to?(:to_ary)
228
+ data.collect do |value|
229
+ clean_unserializable_data(value)
230
+ end
231
+ else
232
+ data.to_s
233
+ end
234
+ end
235
+
236
+ # Replaces the contents of params that match params_filters.
237
+ # TODO: extract this to a different class
238
+ def clean_params
239
+ clean_unserializable_data_from(:parameters)
240
+ filter(parameters)
241
+ if cgi_data
242
+ clean_unserializable_data_from(:cgi_data)
243
+ filter(cgi_data)
244
+ end
245
+ if session_data
246
+ clean_unserializable_data_from(:session_data)
247
+ end
248
+ end
249
+
250
+ def filter(hash)
251
+ if params_filters
252
+ hash.each do |key, value|
253
+ if filter_key?(key)
254
+ hash[key] = "[FILTERED]"
255
+ elsif value.respond_to?(:to_hash)
256
+ filter(hash[key])
257
+ end
258
+ end
259
+ end
260
+ end
261
+
262
+ def filter_key?(key)
263
+ params_filters.any? do |filter|
264
+ key.to_s.include?(filter)
265
+ end
266
+ end
267
+
268
+ def find_session_data
269
+ self.session_data = args[:session_data] || args[:session] || {}
270
+ self.session_data = session_data[:data] if session_data[:data]
271
+ end
272
+
273
+ # Converts the mixed class instances and class names into just names
274
+ # TODO: move this into Configuration or another class
275
+ def ignored_class_names
276
+ ignore.collect do |string_or_class|
277
+ if string_or_class.respond_to?(:name)
278
+ string_or_class.name
279
+ else
280
+ string_or_class
281
+ end
282
+ end
283
+ end
284
+
285
+ def xml_vars_for(builder, hash)
286
+ hash.each do |key, value|
287
+ if value.respond_to?(:to_hash)
288
+ builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) }
289
+ else
290
+ builder.var(value.to_s, :key => key)
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,11 @@
1
+ if defined?(ActionController::Base) && !ActionController::Base.include?(HoptoadNotifier::Catcher)
2
+ ActionController::Base.send(:include, HoptoadNotifier::Catcher)
3
+ end
4
+
5
+ require 'hoptoad_notifier/rails_initializer'
6
+ HoptoadNotifier::RailsInitializer.initialize
7
+
8
+ HoptoadNotifier.configure(true) do |config|
9
+ config.environment_name = RAILS_ENV
10
+ config.project_root = RAILS_ROOT
11
+ end
@@ -0,0 +1,16 @@
1
+ module HoptoadNotifier
2
+ # used to initialize Rails-specific code
3
+ class RailsInitializer
4
+ def self.initialize
5
+ rails_logger = if defined?(Rails.logger)
6
+ Rails.logger
7
+ elsif defined?(RAILS_DEFAULT_LOGGER)
8
+ RAILS_DEFAULT_LOGGER
9
+ end
10
+
11
+ HoptoadNotifier.configure(true) do |config|
12
+ config.logger = rails_logger
13
+ end
14
+ end
15
+ end
16
+ end