hoptoad_notifier 2.1.0

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.
@@ -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