honeybadger 1.11.2 → 1.12.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +56 -46
  3. data/CHANGELOG.md +33 -0
  4. data/Gemfile.lock +1 -1
  5. data/MIT-LICENSE +2 -1
  6. data/Rakefile +3 -1
  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/support/env.rb +2 -0
  11. data/gemfiles/binding_of_caller.gemfile +8 -0
  12. data/gemfiles/rails.gemfile +11 -0
  13. data/gemfiles/standalone.gemfile +7 -0
  14. data/honeybadger.gemspec +22 -11
  15. data/lib/honeybadger.rb +15 -9
  16. data/lib/honeybadger/configuration.rb +9 -4
  17. data/lib/honeybadger/dependency.rb +65 -0
  18. data/lib/honeybadger/exception_extensions.rb +35 -0
  19. data/lib/honeybadger/integrations.rb +4 -0
  20. data/lib/honeybadger/integrations/delayed_job.rb +20 -0
  21. data/lib/honeybadger/integrations/delayed_job/plugin.rb +31 -0
  22. data/lib/honeybadger/integrations/sidekiq.rb +34 -0
  23. data/lib/honeybadger/notice.rb +48 -11
  24. data/lib/honeybadger/payload.rb +29 -0
  25. data/lib/honeybadger/rack.rb +8 -54
  26. data/lib/honeybadger/rack/error_notifier.rb +60 -0
  27. data/lib/honeybadger/rack/user_feedback.rb +74 -0
  28. data/lib/honeybadger/rack/user_informer.rb +28 -0
  29. data/lib/honeybadger/rails.rb +5 -4
  30. data/lib/honeybadger/railtie.rb +4 -3
  31. data/lib/honeybadger/user_feedback.rb +3 -67
  32. data/lib/honeybadger/user_informer.rb +3 -21
  33. data/spec/honeybadger/configuration_spec.rb +5 -1
  34. data/spec/honeybadger/dependency_spec.rb +134 -0
  35. data/spec/honeybadger/exception_extensions_spec.rb +40 -0
  36. data/spec/honeybadger/integrations/delayed_job_spec.rb +48 -0
  37. data/spec/honeybadger/integrations/sidekiq_spec.rb +60 -0
  38. data/spec/honeybadger/notice_spec.rb +176 -35
  39. data/spec/honeybadger/payload_spec.rb +27 -0
  40. data/spec/honeybadger/rails_spec.rb +4 -2
  41. metadata +24 -13
  42. data/gemfiles/rack.gemfile.lock +0 -125
  43. data/gemfiles/rails2.3.gemfile.lock +0 -141
  44. data/gemfiles/rails3.0.gemfile.lock +0 -193
  45. data/gemfiles/rails3.1.gemfile.lock +0 -203
  46. data/gemfiles/rails3.2.gemfile.lock +0 -201
  47. data/gemfiles/rails4.0.gemfile.lock +0 -197
  48. data/gemfiles/rails4.1.gemfile.lock +0 -202
  49. data/gemfiles/rake.gemfile.lock +0 -124
  50. 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,11 +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
20
 
18
21
  module Honeybadger
19
- VERSION = '1.11.2'
22
+ VERSION = '1.12.0.beta2'
20
23
  LOG_PREFIX = "** [Honeybadger] "
21
24
 
22
25
  HEADERS = {
@@ -161,11 +164,13 @@ module Honeybadger
161
164
  private
162
165
 
163
166
  def send_notice(notice)
167
+ return false unless sender
168
+
164
169
  if configuration.public?
165
170
  if configuration.async?
166
171
  configuration.async.call(notice)
167
172
  else
168
- notice.deliver
173
+ Honeybadger.sender.send_to_honeybadger(notice)
169
174
  end
170
175
  end
171
176
  end
@@ -178,13 +183,14 @@ module Honeybadger
178
183
  end
179
184
 
180
185
  def unwrap_exception(exception)
181
- if exception.respond_to?(:original_exception)
182
- exception.original_exception
183
- elsif exception.respond_to?(:continued_exception)
184
- exception.continued_exception
185
- else
186
- exception
187
- end
186
+ exception.respond_to?(:original_exception) && exception.original_exception ||
187
+ exception.respond_to?(:continued_exception) && exception.continued_exception ||
188
+ exception.respond_to?(:cause) && exception.cause ||
189
+ exception
188
190
  end
189
191
  end
190
192
  end
193
+
194
+ unless defined?(Rails)
195
+ Honeybadger::Dependency.inject!
196
+ 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,4 @@
1
+ module Honeybadger::Integrations; end
2
+
3
+ require 'honeybadger/integrations/delayed_job'
4
+ require 'honeybadger/integrations/sidekiq'
@@ -0,0 +1,20 @@
1
+ module Honeybadger
2
+ Dependency.register do
3
+ requirement { defined?(::Delayed::Plugins::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::Plugins::Plugin
5
+ callbacks do |lifecycle|
6
+ lifecycle.around(:invoke_job) do |job, *args, &block|
7
+ begin
8
+ block.call(job, *args)
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
@@ -0,0 +1,34 @@
1
+ module Honeybadger
2
+ module Integrations
3
+ module Sidekiq
4
+ class Middleware
5
+ def call(worker, msg, queue)
6
+ Honeybadger.context.clear!
7
+ yield
8
+ end
9
+ end
10
+ end
11
+ end
12
+
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
22
+ end
23
+ end
24
+
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
33
+ end
34
+ 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
 
@@ -124,7 +132,9 @@ module Honeybadger
124
132
  self.source_extract_radius = args[:source_extract_radius] || 2
125
133
  self.source_extract = extract_source_from_backtrace
126
134
 
127
- self.send_request_session = args[:send_request_session].nil? ? true : args[:send_request_session]
135
+ self.local_variables = send_local_variables? ? local_variables_from_exception(exception) : {}
136
+
137
+ self.send_request_session = args[:send_request_session].nil? ? true : args[:send_request_session]
128
138
 
129
139
  also_use_rack_params_filters
130
140
  find_session_data
@@ -133,10 +143,7 @@ module Honeybadger
133
143
  set_context
134
144
  end
135
145
 
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
146
+ # Deprecated. Remove in 2.0.
140
147
  def deliver
141
148
  return false unless Honeybadger.sender
142
149
  Honeybadger.sender.send_to_honeybadger(self)
@@ -146,7 +153,7 @@ module Honeybadger
146
153
  #
147
154
  # Returns JSON representation of notice
148
155
  def as_json(options = {})
149
- {
156
+ Payload.new({
150
157
  :api_key => api_key,
151
158
  :notifier => {
152
159
  :name => notifier_name,
@@ -168,7 +175,8 @@ module Honeybadger
168
175
  :params => parameters,
169
176
  :session => session_data,
170
177
  :cgi_data => cgi_data,
171
- :context => context
178
+ :context => context,
179
+ :local_variables => local_variables
172
180
  },
173
181
  :server => {
174
182
  :project_root => project_root,
@@ -176,7 +184,7 @@ module Honeybadger
176
184
  :hostname => hostname,
177
185
  :stats => stats
178
186
  }
179
- }
187
+ })
180
188
  end
181
189
 
182
190
  # Public: Creates JSON
@@ -235,8 +243,9 @@ module Honeybadger
235
243
  :error_message, :backtrace_filters, :parameters, :params_filters,
236
244
  :environment_filters, :session_data, :project_root, :url, :ignore,
237
245
  :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
246
+ :component, :action, :cgi_data, :environment_name, :hostname, :stats,
247
+ :context, :source_extract, :source_extract_radius, :send_request_session,
248
+ :api_key, :features, :local_variables
240
249
 
241
250
  # Private: Arguments given in the initializer
242
251
  attr_accessor :args
@@ -331,7 +340,7 @@ module Honeybadger
331
340
 
332
341
  def filter_cgi_data_params(cgi_data)
333
342
  cgi_data.each_pair do |key, value|
334
- next unless value.kind_of?(String) && key =~ /\A[A-Z_]+\Z/
343
+ next unless value.kind_of?(String) && key =~ /\A[A-Z_]+\Z/ && value =~ /\S/
335
344
  cgi_data[key] = filter_url(value)
336
345
  end
337
346
  end
@@ -340,6 +349,8 @@ module Honeybadger
340
349
  if cgi_data
341
350
  cgi_data.delete("rack.request.form_vars")
342
351
  cgi_data.delete("rack.request.query_string")
352
+ cgi_data.delete("action_dispatch.request.parameters")
353
+ cgi_data.delete("action_dispatch.request.request_parameters")
343
354
  end
344
355
  end
345
356
 
@@ -468,5 +479,31 @@ module Honeybadger
468
479
  input = input[0...bytes] if input.respond_to?(:size) && input.size > bytes
469
480
  input
470
481
  end
482
+
483
+ # Internal: Fetch local variables from first frame of backtrace.
484
+ #
485
+ # exception - The Exception containing the bindings stack.
486
+ #
487
+ # Returns a Hash of local variables
488
+ def local_variables_from_exception(exception)
489
+ return {} unless Exception === exception
490
+ return {} if exception.__honeybadger_bindings_stack.empty?
491
+
492
+ if project_root
493
+ binding = exception.__honeybadger_bindings_stack.find { |b| b.eval('__FILE__') =~ /^#{Regexp.escape(project_root.to_s)}/ }
494
+ end
495
+
496
+ binding ||= exception.__honeybadger_bindings_stack[0]
497
+
498
+ vars = binding.eval('local_variables')
499
+ Hash[vars.map {|arg| [arg, binding.eval(arg.to_s)]}]
500
+ end
501
+
502
+ # Internal: Should local variables be sent?
503
+ #
504
+ # Returns true to send local_variables
505
+ def send_local_variables?
506
+ args[:send_local_variables] && features['local_variables']
507
+ end
471
508
  end
472
509
  end
@@ -0,0 +1,29 @@
1
+ require 'delegate'
2
+
3
+ module Honeybadger
4
+ class Payload < SimpleDelegator
5
+ attr_reader :max_depth
6
+
7
+ def initialize(hash = {}, options = {})
8
+ fail ArgumentError, 'must be a Hash' unless hash.kind_of?(Hash)
9
+ @max_depth = options[:max_depth] || 20
10
+ super(hash)
11
+ sanitize(self)
12
+ end
13
+
14
+ private
15
+
16
+ def sanitize(hash, depth = 0)
17
+ hash.each_pair do |k,v|
18
+ next unless v.kind_of?(Hash)
19
+ if depth >= max_depth
20
+ hash.delete(k)
21
+ else
22
+ hash[k] = sanitize(v, depth+1)
23
+ end
24
+ end
25
+
26
+ hash
27
+ end
28
+ end
29
+ end