honeybadger 1.13.2 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +60 -45
  3. data/CHANGELOG.md +30 -1
  4. data/Gemfile.lock +1 -1
  5. data/MIT-LICENSE +2 -1
  6. data/Rakefile +8 -4
  7. data/features/standalone.feature +73 -0
  8. data/features/step_definitions/rack_steps.rb +1 -2
  9. data/features/step_definitions/standalone_steps.rb +12 -0
  10. data/features/step_definitions/thor_steps.rb +4 -0
  11. data/features/support/env.rb +2 -0
  12. data/features/support/test.thor +22 -0
  13. data/features/thor.feature +5 -0
  14. data/gemfiles/binding_of_caller.gemfile +8 -0
  15. data/gemfiles/rails.gemfile +11 -0
  16. data/gemfiles/rake.gemfile +1 -1
  17. data/gemfiles/standalone.gemfile +7 -0
  18. data/gemfiles/thor.gemfile +8 -0
  19. data/honeybadger.gemspec +27 -11
  20. data/lib/honeybadger.rb +15 -10
  21. data/lib/honeybadger/configuration.rb +9 -4
  22. data/lib/honeybadger/dependency.rb +65 -0
  23. data/lib/honeybadger/exception_extensions.rb +35 -0
  24. data/lib/honeybadger/integrations.rb +5 -0
  25. data/lib/honeybadger/integrations/delayed_job.rb +20 -0
  26. data/lib/honeybadger/integrations/delayed_job/plugin.rb +31 -0
  27. data/lib/honeybadger/integrations/sidekiq.rb +17 -9
  28. data/lib/honeybadger/integrations/thor.rb +29 -0
  29. data/lib/honeybadger/notice.rb +47 -99
  30. data/lib/honeybadger/payload.rb +101 -0
  31. data/lib/honeybadger/rack.rb +8 -54
  32. data/lib/honeybadger/rack/error_notifier.rb +60 -0
  33. data/lib/honeybadger/rack/user_feedback.rb +74 -0
  34. data/lib/honeybadger/rack/user_informer.rb +28 -0
  35. data/lib/honeybadger/rails.rb +5 -4
  36. data/lib/honeybadger/railtie.rb +4 -3
  37. data/lib/honeybadger/user_feedback.rb +3 -67
  38. data/lib/honeybadger/user_informer.rb +3 -21
  39. data/spec/honeybadger/configuration_spec.rb +5 -1
  40. data/spec/honeybadger/dependency_spec.rb +134 -0
  41. data/spec/honeybadger/exception_extensions_spec.rb +40 -0
  42. data/spec/honeybadger/integrations/delayed_job_spec.rb +48 -0
  43. data/spec/honeybadger/integrations/sidekiq_spec.rb +60 -0
  44. data/spec/honeybadger/integrations/thor_spec.rb +29 -0
  45. data/spec/honeybadger/notice_spec.rb +137 -127
  46. data/spec/honeybadger/payload_spec.rb +162 -0
  47. data/spec/honeybadger/rack_spec.rb +6 -6
  48. data/spec/honeybadger/rails/action_controller_spec.rb +2 -0
  49. data/spec/honeybadger/rails_spec.rb +4 -2
  50. data/spec/honeybadger/user_feedback_spec.rb +2 -2
  51. data/spec/honeybadger/user_informer_spec.rb +3 -3
  52. metadata +49 -66
  53. data/gemfiles/rack.gemfile.lock +0 -125
  54. data/gemfiles/rails2.3.gemfile.lock +0 -141
  55. data/gemfiles/rails3.0.gemfile.lock +0 -193
  56. data/gemfiles/rails3.1.gemfile.lock +0 -203
  57. data/gemfiles/rails3.2.gemfile.lock +0 -201
  58. data/gemfiles/rails4.0.gemfile.lock +0 -197
  59. data/gemfiles/rails4.1.gemfile.lock +0 -202
  60. data/gemfiles/rake.gemfile.lock +0 -124
  61. data/gemfiles/sinatra.gemfile.lock +0 -124
data/lib/honeybadger.rb CHANGED
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'digest'
5
5
  require 'logger'
6
6
 
7
+ require 'honeybadger/dependency'
7
8
  require 'honeybadger/configuration'
8
9
  require 'honeybadger/backtrace'
9
10
  require 'honeybadger/notice'
@@ -12,12 +13,13 @@ require 'honeybadger/sender'
12
13
  require 'honeybadger/stats'
13
14
  require 'honeybadger/user_informer'
14
15
  require 'honeybadger/user_feedback'
16
+ require 'honeybadger/integrations'
17
+ require 'honeybadger/exception_extensions'
15
18
 
16
19
  require 'honeybadger/railtie' if defined?(Rails::Railtie)
17
- require 'honeybadger/integrations/sidekiq'
18
20
 
19
21
  module Honeybadger
20
- VERSION = '1.13.2'
22
+ VERSION = '1.14.0'
21
23
  LOG_PREFIX = "** [Honeybadger] "
22
24
 
23
25
  HEADERS = {
@@ -171,11 +173,13 @@ module Honeybadger
171
173
  private
172
174
 
173
175
  def send_notice(notice)
176
+ return false unless sender
177
+
174
178
  if configuration.public?
175
179
  if configuration.async?
176
180
  configuration.async.call(notice)
177
181
  else
178
- notice.deliver
182
+ Honeybadger.sender.send_to_honeybadger(notice)
179
183
  end
180
184
  end
181
185
  end
@@ -188,13 +192,14 @@ module Honeybadger
188
192
  end
189
193
 
190
194
  def unwrap_exception(exception)
191
- if exception.respond_to?(:original_exception)
192
- exception.original_exception
193
- elsif exception.respond_to?(:continued_exception)
194
- exception.continued_exception
195
- else
196
- exception
197
- end
195
+ exception.respond_to?(:original_exception) && exception.original_exception ||
196
+ exception.respond_to?(:continued_exception) && exception.continued_exception ||
197
+ exception.respond_to?(:cause) && exception.cause ||
198
+ exception
198
199
  end
199
200
  end
200
201
  end
202
+
203
+ unless defined?(Rails)
204
+ Honeybadger::Dependency.inject!
205
+ end
@@ -8,7 +8,8 @@ module Honeybadger
8
8
  :params_filters, :project_root, :port, :protocol, :proxy_host, :proxy_pass,
9
9
  :proxy_port, :proxy_user, :secure, :use_system_ssl_cert_chain, :framework,
10
10
  :user_information, :feedback, :rescue_rake_exceptions, :source_extract_radius,
11
- :send_request_session, :debug, :fingerprint, :hostname, :metrics, :log_exception_on_send_failure].freeze
11
+ :send_request_session, :debug, :fingerprint, :hostname, :features, :metrics,
12
+ :log_exception_on_send_failure, :send_local_variables].freeze
12
13
 
13
14
  # The API key for your project, found on the project edit form.
14
15
  attr_accessor :api_key
@@ -100,6 +101,9 @@ module Honeybadger
100
101
  # +true+ to send session data, +false+ to exclude
101
102
  attr_accessor :send_request_session
102
103
 
104
+ # +true+ to send local variables, +false+ to exclude
105
+ attr_accessor :send_local_variables
106
+
103
107
  # +true+ to log extra debug info, +false+ to suppress
104
108
  attr_accessor :debug
105
109
 
@@ -175,11 +179,12 @@ module Honeybadger
175
179
  @rescue_rake_exceptions = nil
176
180
  @source_extract_radius = 2
177
181
  @send_request_session = true
182
+ @send_local_variables = false
178
183
  @debug = false
179
184
  @log_exception_on_send_failure = false
180
185
  @hostname = Socket.gethostname
181
186
  @metrics = true
182
- @features = { 'notices' => true }
187
+ @features = {'notices' => true, 'local_variables' => true}
183
188
  @limit = nil
184
189
  @feedback = true
185
190
  end
@@ -283,10 +288,10 @@ module Honeybadger
283
288
  #
284
289
  # Examples
285
290
  #
286
- # config.async = Proc.new { |notice| Thread.new { notice.deliver } }
291
+ # config.async = Proc.new { |notice| Thread.new { Honeybadger.sender.send_to_honeybadger(notice) } }
287
292
  #
288
293
  # config.async do |notice|
289
- # Thread.new { notice.deliver }
294
+ # Thread.new { Honeybadger.sender.send_to_honeybadger(notice) }
290
295
  # end
291
296
  #
292
297
  # Returns configured async handler (should respond to #call(notice))
@@ -0,0 +1,65 @@
1
+ module Honeybadger
2
+ class Dependency
3
+ class << self
4
+ @@instances = []
5
+
6
+ def instances
7
+ @@instances
8
+ end
9
+
10
+ def register
11
+ instances << new.tap { |d| d.instance_eval(&Proc.new) }
12
+ end
13
+
14
+ def inject!
15
+ instances.each do |dependency|
16
+ dependency.inject! if dependency.ok?
17
+ end
18
+ end
19
+
20
+ def reset!
21
+ instances.each(&:reset!)
22
+ end
23
+ end
24
+
25
+ def initialize
26
+ @injected = false
27
+ @requirements = []
28
+ @injections = []
29
+ end
30
+
31
+ def requirement
32
+ @requirements << Proc.new
33
+ end
34
+
35
+ def injection
36
+ @injections << Proc.new
37
+ end
38
+
39
+ def ok?
40
+ !@injected && @requirements.all?(&:call)
41
+ rescue => e
42
+ Honeybadger.write_verbose_log("Exception caught while verifying dependency: #{e.class} -- #{e.message}", :error)
43
+ false
44
+ end
45
+
46
+ def inject!
47
+ @injections.each(&:call)
48
+ rescue => e
49
+ Honeybadger.write_verbose_log("Exception caught while injecting dependency: #{e.class} -- #{e.message}", :error)
50
+ false
51
+ ensure
52
+ @injected = true
53
+ end
54
+
55
+ def reset!
56
+ @injected = false
57
+ end
58
+
59
+ def injected?
60
+ @injected
61
+ end
62
+
63
+ attr_reader :requirements, :injections
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ module Honeybadger
2
+ module ExceptionExtensions
3
+ module Bindings
4
+ def self.included(base)
5
+ base.send(:alias_method, :set_backtrace_without_honeybadger, :set_backtrace)
6
+ base.send(:alias_method, :set_backtrace, :set_backtrace_with_honeybadger)
7
+ end
8
+
9
+ def set_backtrace_with_honeybadger(*args, &block)
10
+ if caller.none? { |loc| loc.match(Honeybadger::Backtrace::Line::INPUT_FORMAT) && Regexp.last_match(1) == __FILE__ }
11
+ @__honeybadger_bindings_stack = binding.callers.drop(1)
12
+ end
13
+
14
+ set_backtrace_without_honeybadger(*args, &block)
15
+ end
16
+
17
+ def __honeybadger_bindings_stack
18
+ @__honeybadger_bindings_stack || []
19
+ end
20
+ end
21
+
22
+ module NullBindings
23
+ def __honeybadger_bindings_stack
24
+ []
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ begin
31
+ require 'binding_of_caller'
32
+ Exception.send(:include, Honeybadger::ExceptionExtensions::Bindings)
33
+ rescue LoadError
34
+ Exception.send(:include, Honeybadger::ExceptionExtensions::NullBindings)
35
+ end
@@ -0,0 +1,5 @@
1
+ module Honeybadger::Integrations; end
2
+
3
+ require 'honeybadger/integrations/delayed_job'
4
+ require 'honeybadger/integrations/sidekiq'
5
+ require 'honeybadger/integrations/thor'
@@ -0,0 +1,20 @@
1
+ module Honeybadger
2
+ Dependency.register do
3
+ requirement { defined?(::Delayed::Plugin) }
4
+ requirement { defined?(::Delayed::Worker.plugins) }
5
+ requirement do
6
+ if delayed_job_honeybadger = defined?(::Delayed::Plugins::Honeybadger)
7
+ Honeybadger.write_verbose_log("Support for Delayed Job has been moved " \
8
+ "to the honeybadger gem. Please remove " \
9
+ "delayed_job_honeybadger from your " \
10
+ "Gemfile.", :warn)
11
+ end
12
+ !delayed_job_honeybadger
13
+ end
14
+
15
+ injection do
16
+ require 'honeybadger/integrations/delayed_job/plugin'
17
+ ::Delayed::Worker.plugins << Integrations::DelayedJob::Plugin
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ module Honeybadger
2
+ module Integrations
3
+ module DelayedJob
4
+ class Plugin < ::Delayed::Plugin
5
+ callbacks do |lifecycle|
6
+ lifecycle.around(:invoke_job) do |job, &block|
7
+ begin
8
+ block.call(job)
9
+ rescue Exception => error
10
+ ::Honeybadger.notify_or_ignore(
11
+ :error_class => error.class.name,
12
+ :error_message => "#{ error.class.name }: #{ error.message }",
13
+ :backtrace => error.backtrace,
14
+ :context => {
15
+ :job_id => job.id,
16
+ :handler => job.handler,
17
+ :last_error => job.last_error,
18
+ :attempts => job.attempts,
19
+ :queue => job.queue
20
+ }
21
+ )
22
+ raise error
23
+ ensure
24
+ ::Honeybadger.context.clear!
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -9,18 +9,26 @@ module Honeybadger
9
9
  end
10
10
  end
11
11
  end
12
- end
13
12
 
14
- if defined?(::Sidekiq)
15
- ::Sidekiq.configure_server do |config|
16
- config.server_middleware do |chain|
17
- chain.add Honeybadger::Integrations::Sidekiq::Middleware
13
+ Dependency.register do
14
+ requirement { defined?(::Sidekiq) }
15
+
16
+ injection do
17
+ ::Sidekiq.configure_server do |config|
18
+ config.server_middleware do |chain|
19
+ chain.add Integrations::Sidekiq::Middleware
20
+ end
21
+ end
18
22
  end
19
23
  end
20
- end
21
24
 
22
- if defined?(::Sidekiq::VERSION) && ::Sidekiq::VERSION > '3'
23
- ::Sidekiq.configure_server do |config|
24
- config.error_handlers << Proc.new {|ex,context| Honeybadger.notify_or_ignore(ex, :parameters => context) }
25
+ Dependency.register do
26
+ requirement { defined?(::Sidekiq::VERSION) && ::Sidekiq::VERSION > '3' }
27
+
28
+ injection do
29
+ ::Sidekiq.configure_server do |config|
30
+ config.error_handlers << Proc.new {|ex,context| Honeybadger.notify_or_ignore(ex, :parameters => context) }
31
+ end
32
+ end
25
33
  end
26
34
  end
@@ -0,0 +1,29 @@
1
+ module Honeybadger
2
+ module Integrations
3
+ module Thor
4
+ def self.included(base)
5
+ base.class_eval do
6
+ no_commands do
7
+ alias_method :invoke_command_without_honeybadger, :invoke_command
8
+ alias_method :invoke_command, :invoke_command_with_honeybadger
9
+ end
10
+ end
11
+ end
12
+
13
+ def invoke_command_with_honeybadger(*args)
14
+ invoke_command_without_honeybadger(*args)
15
+ rescue Exception => e
16
+ Honeybadger.notify_or_ignore(e)
17
+ raise
18
+ end
19
+ end
20
+ end
21
+
22
+ Dependency.register do
23
+ requirement { defined?(::Thor) }
24
+
25
+ injection do
26
+ Thor.send(:include, Integrations::Thor)
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,4 @@
1
+ require 'honeybadger/payload'
1
2
  require 'socket'
2
3
 
3
4
  module Honeybadger
@@ -85,8 +86,15 @@ module Honeybadger
85
86
  # The api_key to use when sending notice (optional)
86
87
  attr_reader :api_key
87
88
 
89
+ # Local variables are extracted from first frame of backtrace
90
+ attr_reader :local_variables
91
+
92
+ # Additional features to enable/disable
93
+ attr_reader :features
94
+
88
95
  def initialize(args)
89
96
  self.args = args
97
+ self.features = args[:features] || {}
90
98
  self.exception = args[:exception]
91
99
  self.project_root = args[:project_root]
92
100
 
@@ -116,7 +124,7 @@ module Honeybadger
116
124
  end
117
125
  end
118
126
 
119
- self.url = filter_url(args[:url] || rack_env(:url))
127
+ self.url = args[:url] || rack_env(:url)
120
128
  self.hostname = local_hostname
121
129
  self.stats = Stats.all
122
130
  self.api_key = args[:api_key]
@@ -124,19 +132,17 @@ module Honeybadger
124
132
  self.source_extract_radius = args[:source_extract_radius] || 2
125
133
  self.source_extract = extract_source_from_backtrace
126
134
 
135
+ self.local_variables = send_local_variables? ? local_variables_from_exception(exception) : {}
136
+
127
137
  self.send_request_session = args[:send_request_session].nil? ? true : args[:send_request_session]
128
138
 
129
139
  find_session_data
130
140
  clean_rack_request_data
131
141
  also_use_rack_params_filters
132
- clean_params
133
142
  set_context
134
143
  end
135
144
 
136
- # Public: Send the notice to Honeybadger using the configured sender
137
- #
138
- # Returns a reference to the error in Honeybadger, false if sender isn't
139
- # configured
145
+ # Deprecated. Remove in 2.0.
140
146
  def deliver
141
147
  return false unless Honeybadger.sender
142
148
  Honeybadger.sender.send_to_honeybadger(self)
@@ -146,7 +152,7 @@ module Honeybadger
146
152
  #
147
153
  # Returns JSON representation of notice
148
154
  def as_json(options = {})
149
- {
155
+ Payload.new({
150
156
  :api_key => api_key,
151
157
  :notifier => {
152
158
  :name => notifier_name,
@@ -168,7 +174,8 @@ module Honeybadger
168
174
  :params => parameters,
169
175
  :session => session_data,
170
176
  :cgi_data => cgi_data,
171
- :context => context
177
+ :context => context,
178
+ :local_variables => local_variables
172
179
  },
173
180
  :server => {
174
181
  :project_root => project_root,
@@ -176,7 +183,7 @@ module Honeybadger
176
183
  :hostname => hostname,
177
184
  :stats => stats
178
185
  }
179
- }
186
+ }, :filters => params_filters)
180
187
  end
181
188
 
182
189
  # Public: Creates JSON
@@ -235,8 +242,9 @@ module Honeybadger
235
242
  :error_message, :backtrace_filters, :parameters, :params_filters,
236
243
  :environment_filters, :session_data, :project_root, :url, :ignore,
237
244
  :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
238
- :component, :action, :cgi_data, :environment_name, :hostname, :stats, :context,
239
- :source_extract, :source_extract_radius, :send_request_session, :api_key
245
+ :component, :action, :cgi_data, :environment_name, :hostname, :stats,
246
+ :context, :source_extract, :source_extract_radius, :send_request_session,
247
+ :api_key, :features, :local_variables
240
248
 
241
249
  # Private: Arguments given in the initializer
242
250
  attr_accessor :args
@@ -271,71 +279,6 @@ module Honeybadger
271
279
  end
272
280
  end
273
281
 
274
- # Private: Removes non-serializable data from the given attribute.
275
- # See #clean_unserializable_data
276
- def clean_unserializable_data_from(attribute)
277
- self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
278
- end
279
-
280
- # Private: Removes non-serializable data. Allowed data types are strings, arrays,
281
- # and hashes. All other types are converted to strings.
282
- # TODO: move this onto Hash
283
- def clean_unserializable_data(data, stack = [])
284
- return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id }
285
-
286
- if data.respond_to?(:to_hash)
287
- data.to_hash.inject({}) do |result, (key, value)|
288
- result.merge(key => clean_unserializable_data(value, stack + [data.object_id]))
289
- end
290
- elsif data.respond_to?(:to_ary)
291
- data.to_ary.collect do |value|
292
- clean_unserializable_data(value, stack + [data.object_id])
293
- end
294
- else
295
- data.to_s
296
- end
297
- end
298
-
299
- # Internal: Filters query parameters from URL
300
- #
301
- # url - String URL to filter
302
- #
303
- # Returns filtered String URL
304
- def filter_url(url)
305
- return nil unless url =~ /\S/
306
-
307
- url = url.dup
308
- url.scan(/(?:^|&|\?)([^=?&]+)=([^&]+)/).each do |m|
309
- next unless filter_key?(m[0])
310
- url.gsub!(/#{m[1]}/, '[FILTERED]')
311
- end
312
-
313
- url
314
- end
315
-
316
- # Internal: Replaces the contents of params that match params_filters.
317
- # TODO: extract this to a different class
318
- def clean_params
319
- clean_unserializable_data_from(:parameters)
320
- filter(parameters)
321
- if cgi_data
322
- clean_unserializable_data_from(:cgi_data)
323
- filter(cgi_data)
324
- filter_cgi_data_params(cgi_data)
325
- end
326
- if session_data
327
- clean_unserializable_data_from(:session_data)
328
- filter(session_data)
329
- end
330
- end
331
-
332
- def filter_cgi_data_params(cgi_data)
333
- cgi_data.each_pair do |key, value|
334
- next unless value.kind_of?(String) && key =~ /\A[A-Z_]+\Z/ && value =~ /\S/
335
- cgi_data[key] = filter_url(value)
336
- end
337
- end
338
-
339
282
  def clean_rack_request_data
340
283
  if cgi_data
341
284
  self.cgi_data = cgi_data.dup
@@ -381,28 +324,6 @@ module Honeybadger
381
324
  end
382
325
  end
383
326
 
384
- def filter(hash)
385
- if params_filters
386
- hash.each do |key, value|
387
- if filter_key?(key)
388
- hash[key] = "[FILTERED]"
389
- elsif value.respond_to?(:to_hash)
390
- filter(hash[key])
391
- end
392
- end
393
- end
394
- end
395
-
396
- def filter_key?(key)
397
- params_filters.any? do |filter|
398
- if filter.is_a?(Regexp)
399
- key.to_s =~ filter
400
- else
401
- key.to_s.eql?(filter.to_s)
402
- end
403
- end
404
- end
405
-
406
327
  def find_session_data
407
328
  if send_request_session
408
329
  self.session_data = args[:session_data] || args[:session] || rack_session || {}
@@ -416,7 +337,8 @@ module Honeybadger
416
337
  end
417
338
 
418
339
  def set_context
419
- self.context = Thread.current[:honeybadger_context] || {}
340
+ self.context = {}
341
+ self.context.merge!(Thread.current[:honeybadger_context]) if Thread.current[:honeybadger_context]
420
342
  self.context.merge!(args[:context]) if args[:context]
421
343
  self.context = nil if context.empty?
422
344
  end
@@ -477,5 +399,31 @@ module Honeybadger
477
399
  input = input[0...bytes] if input.respond_to?(:size) && input.size > bytes
478
400
  input
479
401
  end
402
+
403
+ # Internal: Fetch local variables from first frame of backtrace.
404
+ #
405
+ # exception - The Exception containing the bindings stack.
406
+ #
407
+ # Returns a Hash of local variables
408
+ def local_variables_from_exception(exception)
409
+ return {} unless Exception === exception
410
+ return {} if exception.__honeybadger_bindings_stack.empty?
411
+
412
+ if project_root
413
+ binding = exception.__honeybadger_bindings_stack.find { |b| b.eval('__FILE__') =~ /^#{Regexp.escape(project_root.to_s)}/ }
414
+ end
415
+
416
+ binding ||= exception.__honeybadger_bindings_stack[0]
417
+
418
+ vars = binding.eval('local_variables')
419
+ Hash[vars.map {|arg| [arg, binding.eval(arg.to_s)]}]
420
+ end
421
+
422
+ # Internal: Should local variables be sent?
423
+ #
424
+ # Returns true to send local_variables
425
+ def send_local_variables?
426
+ args[:send_local_variables] && features['local_variables']
427
+ end
480
428
  end
481
429
  end