honeybadger 1.13.2 → 1.14.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.
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