honeybadger 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +713 -701
  3. data/LICENSE +19 -19
  4. data/README.md +57 -57
  5. data/TROUBLESHOOTING.md +3 -3
  6. data/bin/honeybadger +5 -5
  7. data/lib/honeybadger/agent.rb +488 -488
  8. data/lib/honeybadger/backend/base.rb +116 -116
  9. data/lib/honeybadger/backend/debug.rb +22 -22
  10. data/lib/honeybadger/backend/null.rb +29 -29
  11. data/lib/honeybadger/backend/server.rb +62 -62
  12. data/lib/honeybadger/backend/test.rb +46 -46
  13. data/lib/honeybadger/backend.rb +27 -27
  14. data/lib/honeybadger/backtrace.rb +181 -181
  15. data/lib/honeybadger/breadcrumbs/active_support.rb +119 -119
  16. data/lib/honeybadger/breadcrumbs/breadcrumb.rb +53 -53
  17. data/lib/honeybadger/breadcrumbs/collector.rb +82 -82
  18. data/lib/honeybadger/breadcrumbs/logging.rb +51 -51
  19. data/lib/honeybadger/breadcrumbs/ring_buffer.rb +44 -44
  20. data/lib/honeybadger/breadcrumbs.rb +8 -8
  21. data/lib/honeybadger/cli/deploy.rb +43 -43
  22. data/lib/honeybadger/cli/exec.rb +143 -143
  23. data/lib/honeybadger/cli/helpers.rb +28 -28
  24. data/lib/honeybadger/cli/heroku.rb +129 -129
  25. data/lib/honeybadger/cli/install.rb +101 -101
  26. data/lib/honeybadger/cli/main.rb +237 -237
  27. data/lib/honeybadger/cli/notify.rb +67 -67
  28. data/lib/honeybadger/cli/test.rb +267 -267
  29. data/lib/honeybadger/cli.rb +14 -14
  30. data/lib/honeybadger/config/defaults.rb +336 -333
  31. data/lib/honeybadger/config/env.rb +42 -42
  32. data/lib/honeybadger/config/ruby.rb +146 -146
  33. data/lib/honeybadger/config/yaml.rb +76 -76
  34. data/lib/honeybadger/config.rb +413 -413
  35. data/lib/honeybadger/const.rb +20 -20
  36. data/lib/honeybadger/context_manager.rb +55 -55
  37. data/lib/honeybadger/conversions.rb +16 -16
  38. data/lib/honeybadger/init/rails.rb +38 -38
  39. data/lib/honeybadger/init/rake.rb +66 -66
  40. data/lib/honeybadger/init/ruby.rb +11 -11
  41. data/lib/honeybadger/init/sinatra.rb +51 -51
  42. data/lib/honeybadger/logging.rb +177 -177
  43. data/lib/honeybadger/notice.rb +579 -568
  44. data/lib/honeybadger/plugin.rb +210 -210
  45. data/lib/honeybadger/plugins/breadcrumbs.rb +111 -111
  46. data/lib/honeybadger/plugins/delayed_job/plugin.rb +56 -56
  47. data/lib/honeybadger/plugins/delayed_job.rb +22 -22
  48. data/lib/honeybadger/plugins/faktory.rb +52 -52
  49. data/lib/honeybadger/plugins/lambda.rb +71 -71
  50. data/lib/honeybadger/plugins/local_variables.rb +44 -44
  51. data/lib/honeybadger/plugins/passenger.rb +23 -23
  52. data/lib/honeybadger/plugins/rails.rb +72 -63
  53. data/lib/honeybadger/plugins/resque.rb +72 -72
  54. data/lib/honeybadger/plugins/shoryuken.rb +52 -52
  55. data/lib/honeybadger/plugins/sidekiq.rb +71 -62
  56. data/lib/honeybadger/plugins/sucker_punch.rb +18 -18
  57. data/lib/honeybadger/plugins/thor.rb +32 -32
  58. data/lib/honeybadger/plugins/warden.rb +19 -19
  59. data/lib/honeybadger/rack/error_notifier.rb +92 -92
  60. data/lib/honeybadger/rack/user_feedback.rb +88 -88
  61. data/lib/honeybadger/rack/user_informer.rb +45 -45
  62. data/lib/honeybadger/ruby.rb +2 -2
  63. data/lib/honeybadger/singleton.rb +103 -103
  64. data/lib/honeybadger/tasks.rb +22 -22
  65. data/lib/honeybadger/templates/feedback_form.erb +84 -84
  66. data/lib/honeybadger/util/http.rb +92 -92
  67. data/lib/honeybadger/util/lambda.rb +32 -32
  68. data/lib/honeybadger/util/request_hash.rb +73 -73
  69. data/lib/honeybadger/util/request_payload.rb +41 -41
  70. data/lib/honeybadger/util/revision.rb +39 -39
  71. data/lib/honeybadger/util/sanitizer.rb +214 -214
  72. data/lib/honeybadger/util/sql.rb +34 -34
  73. data/lib/honeybadger/util/stats.rb +50 -50
  74. data/lib/honeybadger/version.rb +4 -4
  75. data/lib/honeybadger/worker.rb +253 -253
  76. data/lib/honeybadger.rb +11 -11
  77. data/resources/ca-bundle.crt +3376 -3376
  78. data/vendor/capistrano-honeybadger/lib/capistrano/honeybadger.rb +5 -5
  79. data/vendor/capistrano-honeybadger/lib/capistrano/tasks/deploy.cap +89 -89
  80. data/vendor/capistrano-honeybadger/lib/honeybadger/capistrano/legacy.rb +47 -47
  81. data/vendor/capistrano-honeybadger/lib/honeybadger/capistrano.rb +2 -2
  82. data/vendor/cli/inifile.rb +628 -628
  83. data/vendor/cli/thor/actions/create_file.rb +103 -103
  84. data/vendor/cli/thor/actions/create_link.rb +59 -59
  85. data/vendor/cli/thor/actions/directory.rb +118 -118
  86. data/vendor/cli/thor/actions/empty_directory.rb +135 -135
  87. data/vendor/cli/thor/actions/file_manipulation.rb +316 -316
  88. data/vendor/cli/thor/actions/inject_into_file.rb +107 -107
  89. data/vendor/cli/thor/actions.rb +319 -319
  90. data/vendor/cli/thor/base.rb +656 -656
  91. data/vendor/cli/thor/command.rb +133 -133
  92. data/vendor/cli/thor/core_ext/hash_with_indifferent_access.rb +77 -77
  93. data/vendor/cli/thor/core_ext/io_binary_read.rb +10 -10
  94. data/vendor/cli/thor/core_ext/ordered_hash.rb +98 -98
  95. data/vendor/cli/thor/error.rb +32 -32
  96. data/vendor/cli/thor/group.rb +281 -281
  97. data/vendor/cli/thor/invocation.rb +178 -178
  98. data/vendor/cli/thor/line_editor/basic.rb +35 -35
  99. data/vendor/cli/thor/line_editor/readline.rb +88 -88
  100. data/vendor/cli/thor/line_editor.rb +17 -17
  101. data/vendor/cli/thor/parser/argument.rb +73 -73
  102. data/vendor/cli/thor/parser/arguments.rb +175 -175
  103. data/vendor/cli/thor/parser/option.rb +125 -125
  104. data/vendor/cli/thor/parser/options.rb +218 -218
  105. data/vendor/cli/thor/parser.rb +4 -4
  106. data/vendor/cli/thor/rake_compat.rb +71 -71
  107. data/vendor/cli/thor/runner.rb +322 -322
  108. data/vendor/cli/thor/shell/basic.rb +421 -421
  109. data/vendor/cli/thor/shell/color.rb +149 -149
  110. data/vendor/cli/thor/shell/html.rb +126 -126
  111. data/vendor/cli/thor/shell.rb +81 -81
  112. data/vendor/cli/thor/util.rb +267 -267
  113. data/vendor/cli/thor/version.rb +3 -3
  114. data/vendor/cli/thor.rb +484 -484
  115. metadata +10 -5
@@ -1,568 +1,579 @@
1
- require 'json'
2
- require 'securerandom'
3
- require 'forwardable'
4
-
5
- require 'honeybadger/version'
6
- require 'honeybadger/backtrace'
7
- require 'honeybadger/conversions'
8
- require 'honeybadger/util/stats'
9
- require 'honeybadger/util/sanitizer'
10
- require 'honeybadger/util/request_hash'
11
- require 'honeybadger/util/request_payload'
12
-
13
- module Honeybadger
14
- # @api private
15
- NOTIFIER = {
16
- name: 'honeybadger-ruby'.freeze,
17
- url: 'https://github.com/honeybadger-io/honeybadger-ruby'.freeze,
18
- version: VERSION,
19
- language: 'ruby'.freeze
20
- }.freeze
21
-
22
- # @api private
23
- # Substitution for gem root in backtrace lines.
24
- GEM_ROOT = '[GEM_ROOT]'.freeze
25
-
26
- # @api private
27
- # Substitution for project root in backtrace lines.
28
- PROJECT_ROOT = '[PROJECT_ROOT]'.freeze
29
-
30
- # @api private
31
- # Empty String (used for equality comparisons and assignment).
32
- STRING_EMPTY = ''.freeze
33
-
34
- # @api private
35
- # A Regexp which matches non-blank characters.
36
- NOT_BLANK = /\S/.freeze
37
-
38
- # @api private
39
- # Matches lines beginning with ./
40
- RELATIVE_ROOT = Regexp.new('^\.\/').freeze
41
-
42
- # @api private
43
- MAX_EXCEPTION_CAUSES = 5
44
-
45
- # @api private
46
- # Binding#source_location was added in Ruby 2.6.
47
- BINDING_HAS_SOURCE_LOCATION = Binding.method_defined?(:source_location)
48
-
49
- class Notice
50
- extend Forwardable
51
-
52
- include Conversions
53
-
54
- # @api private
55
- # The String character used to split tag strings.
56
- TAG_SEPERATOR = /,|\s/.freeze
57
-
58
- # @api private
59
- # The Regexp used to strip invalid characters from individual tags.
60
- TAG_SANITIZER = /\s/.freeze
61
-
62
- # @api private
63
- class Cause
64
- attr_accessor :error_class, :error_message, :backtrace
65
-
66
- def initialize(cause)
67
- self.error_class = cause.class.name
68
- self.error_message = cause.message
69
- self.backtrace = cause.backtrace
70
- end
71
- end
72
-
73
- # The unique ID of this notice which can be used to reference the error in
74
- # Honeybadger.
75
- attr_reader :id
76
-
77
- # The exception that caused this notice, if any.
78
- attr_reader :exception
79
-
80
- # The exception cause if available.
81
- attr_reader :cause
82
- def cause=(cause)
83
- @cause = cause
84
- @causes = unwrap_causes(cause)
85
- end
86
-
87
- # @return [Cause] A list of exception causes (see {Cause})
88
- attr_reader :causes
89
-
90
- # The backtrace from the given exception or hash.
91
- attr_accessor :backtrace
92
-
93
- # Custom fingerprint for error, used to group similar errors together.
94
- attr_accessor :fingerprint
95
-
96
- # Tags which will be applied to error.
97
- attr_reader :tags
98
- def tags=(tags)
99
- @tags = construct_tags(tags)
100
- end
101
-
102
- # The name of the class of error (example: RuntimeError).
103
- attr_accessor :error_class
104
-
105
- # The message from the exception, or a general description of the error.
106
- attr_accessor :error_message
107
-
108
- # The context Hash.
109
- attr_accessor :context
110
-
111
- # CGI variables such as HTTP_METHOD.
112
- attr_accessor :cgi_data
113
-
114
- # A hash of parameters from the query string or post body.
115
- attr_accessor :params
116
- alias_method :parameters, :params
117
-
118
- # The component (if any) which was used in this request (usually the controller).
119
- attr_accessor :component
120
- alias_method :controller, :component
121
- alias_method :controller=, :component=
122
-
123
- # The action (if any) that was called in this request.
124
- attr_accessor :action
125
-
126
- # A hash of session data from the request.
127
- attr_accessor :session
128
-
129
- # The URL at which the error occurred (if any).
130
- attr_accessor :url
131
-
132
- # Local variables are extracted from first frame of backtrace.
133
- attr_accessor :local_variables
134
-
135
- # The API key used to deliver this notice.
136
- attr_accessor :api_key
137
-
138
- # Deprecated: Excerpt from source file.
139
- attr_reader :source
140
-
141
- # @return [Breadcrumbs::Collector] The collection of captured breadcrumbs
142
- attr_accessor :breadcrumbs
143
-
144
- # Custom details data
145
- attr_accessor :details
146
-
147
- # @api private
148
- # Cache project path substitutions for backtrace lines.
149
- PROJECT_ROOT_CACHE = {}
150
-
151
- # @api private
152
- # Cache gem path substitutions for backtrace lines.
153
- GEM_ROOT_CACHE = {}
154
-
155
- # @api private
156
- # A list of backtrace filters to run all the time.
157
- BACKTRACE_FILTERS = [
158
- lambda { |line|
159
- return line unless defined?(Gem)
160
- GEM_ROOT_CACHE[line] ||= Gem.path.reduce(line) do |line, path|
161
- line.sub(path, GEM_ROOT)
162
- end
163
- },
164
- lambda { |line, config|
165
- return line unless config
166
- c = (PROJECT_ROOT_CACHE[config[:root]] ||= {})
167
- return c[line] if c.has_key?(line)
168
- c[line] ||= if config.root_regexp
169
- line.sub(config.root_regexp, PROJECT_ROOT)
170
- else
171
- line
172
- end
173
- },
174
- lambda { |line| line.sub(RELATIVE_ROOT, STRING_EMPTY) },
175
- lambda { |line| line if line !~ %r{lib/honeybadger} }
176
- ].freeze
177
-
178
- # @api private
179
- def initialize(config, opts = {})
180
- @now = Time.now.utc
181
- @pid = Process.pid
182
- @id = SecureRandom.uuid
183
- @stats = Util::Stats.all
184
-
185
- @opts = opts
186
- @config = config
187
-
188
- @rack_env = opts.fetch(:rack_env, nil)
189
- @request_sanitizer = Util::Sanitizer.new(filters: params_filters)
190
-
191
- @exception = unwrap_exception(opts[:exception])
192
-
193
- self.error_class = exception_attribute(:error_class, 'Notice') {|exception| exception.class.name }
194
- self.error_message = exception_attribute(:error_message, 'No message provided') do |exception|
195
- "#{exception.class.name}: #{exception.message}"
196
- end
197
- self.backtrace = exception_attribute(:backtrace, caller)
198
- self.cause = opts.key?(:cause) ? opts[:cause] : (exception_cause(@exception) || $!)
199
-
200
- self.context = construct_context_hash(opts, exception)
201
- self.local_variables = local_variables_from_exception(exception, config)
202
- self.api_key = opts[:api_key] || config[:api_key]
203
- self.tags = construct_tags(opts[:tags]) | construct_tags(context[:tags])
204
-
205
- self.url = opts[:url] || request_hash[:url] || nil
206
- self.action = opts[:action] || request_hash[:action] || nil
207
- self.component = opts[:controller] || opts[:component] || request_hash[:component] || nil
208
- self.params = opts[:parameters] || opts[:params] || request_hash[:params] || {}
209
- self.session = opts[:session] || request_hash[:session] || {}
210
- self.cgi_data = opts[:cgi_data] || request_hash[:cgi_data] || {}
211
- self.details = opts[:details] || {}
212
-
213
- self.session = opts[:session][:data] if opts[:session] && opts[:session][:data]
214
-
215
- self.breadcrumbs = opts[:breadcrumbs] || Breadcrumbs::Collector.new(config)
216
-
217
- # Fingerprint must be calculated last since callback operates on `self`.
218
- self.fingerprint = fingerprint_from_opts(opts)
219
- end
220
-
221
- # @api private
222
- # Template used to create JSON payload.
223
- #
224
- # @return [Hash] JSON representation of notice.
225
- def as_json(*args)
226
- request = construct_request_hash
227
- request[:context] = s(context)
228
- request[:local_variables] = local_variables if local_variables
229
-
230
- {
231
- api_key: s(api_key),
232
- notifier: NOTIFIER,
233
- breadcrumbs: sanitized_breadcrumbs,
234
- error: {
235
- token: id,
236
- class: s(error_class),
237
- message: s(error_message),
238
- backtrace: s(parse_backtrace(backtrace)),
239
- fingerprint: fingerprint_hash,
240
- tags: s(tags),
241
- causes: s(prepare_causes(causes))
242
- },
243
- details: s(details),
244
- request: request,
245
- server: {
246
- project_root: s(config[:root]),
247
- revision: s(config[:revision]),
248
- environment_name: s(config[:env]),
249
- hostname: s(config[:hostname]),
250
- stats: stats,
251
- time: now,
252
- pid: pid
253
- }
254
- }
255
- end
256
-
257
- # Converts the notice to JSON.
258
- #
259
- # @return [Hash] The JSON representation of the notice.
260
- def to_json(*a)
261
- ::JSON.generate(as_json(*a))
262
- end
263
-
264
- # @api private
265
- # Determines if this notice should be ignored.
266
- def ignore?
267
- ignore_by_origin? || ignore_by_class? || ignore_by_callbacks?
268
- end
269
-
270
- # Halts the notice and the before_notify callback chain.
271
- #
272
- # Returns nothing.
273
- def halt!
274
- @halted ||= true
275
- end
276
-
277
- # @api private
278
- # Determines if this notice will be discarded.
279
- def halted?
280
- !!@halted
281
- end
282
-
283
- private
284
-
285
- attr_reader :config, :opts, :stats, :now, :pid, :request_sanitizer,
286
- :rack_env
287
-
288
- def ignore_by_origin?
289
- return false if opts[:origin] != :rake
290
- return false if config[:'exceptions.rescue_rake']
291
- true
292
- end
293
-
294
- def ignore_by_callbacks?
295
- config.exception_filter &&
296
- config.exception_filter.call(self)
297
- end
298
-
299
- # Gets a property named "attribute" of an exception, either from
300
- # the #args hash or actual exception (in order of precidence).
301
- #
302
- # attribute - A Symbol existing as a key in #args and/or attribute on
303
- # Exception.
304
- # default - Default value if no other value is found (optional).
305
- # block - An optional block which receives an Exception and returns the
306
- # desired value.
307
- #
308
- # Returns attribute value from args or exception, otherwise default.
309
- def exception_attribute(attribute, default = nil, &block)
310
- opts[attribute] || (exception && from_exception(attribute, &block)) || default
311
- end
312
-
313
- # Gets a property named +attribute+ from an exception.
314
- #
315
- # If a block is given, it will be used when getting the property from an
316
- # exception. The block should accept and exception and return the value for
317
- # the property.
318
- #
319
- # If no block is given, a method with the same name as +attribute+ will be
320
- # invoked for the value.
321
- def from_exception(attribute)
322
- return unless exception
323
-
324
- if block_given?
325
- yield(exception)
326
- else
327
- exception.send(attribute)
328
- end
329
- end
330
-
331
- # Determines if error class should be ignored.
332
- #
333
- # ignored_class_name - The name of the ignored class. May be a
334
- # string or regexp (optional).
335
- #
336
- # Returns true or false.
337
- def ignore_by_class?(ignored_class = nil)
338
- @ignore_by_class ||= Proc.new do |ignored_class|
339
- case error_class
340
- when (ignored_class.respond_to?(:name) ? ignored_class.name : ignored_class)
341
- true
342
- else
343
- exception && ignored_class.is_a?(Class) && exception.class < ignored_class
344
- end
345
- end
346
-
347
- ignored_class ? @ignore_by_class.call(ignored_class) : config.ignored_classes.any?(&@ignore_by_class)
348
- end
349
-
350
- def construct_backtrace_filters(opts)
351
- [
352
- config.backtrace_filter
353
- ].compact | BACKTRACE_FILTERS
354
- end
355
-
356
- def request_hash
357
- @request_hash ||= Util::RequestHash.from_env(rack_env)
358
- end
359
-
360
- # Construct the request data.
361
- #
362
- # Returns Hash request data.
363
- def construct_request_hash
364
- request = {
365
- url: url,
366
- component: component,
367
- action: action,
368
- params: params,
369
- session: session,
370
- cgi_data: cgi_data,
371
- sanitizer: request_sanitizer
372
- }
373
- request.delete_if {|k,v| config.excluded_request_keys.include?(k) }
374
- Util::RequestPayload.build(request)
375
- end
376
-
377
- # Get optional context from exception.
378
- #
379
- # Returns the Hash context.
380
- def exception_context(exception)
381
- # This extra check exists because the exception itself is not expected to
382
- # convert to a hash.
383
- object = exception if exception.respond_to?(:to_honeybadger_context)
384
- object ||= {}.freeze
385
-
386
- Context(object)
387
- end
388
-
389
- # Sanitize metadata to keep it at a single level and remove any filtered
390
- # parameters
391
- def sanitized_breadcrumbs
392
- sanitizer = Util::Sanitizer.new(max_depth: 1, filters: params_filters)
393
- breadcrumbs.each do |breadcrumb|
394
- breadcrumb.metadata = sanitizer.sanitize(breadcrumb.metadata)
395
- end
396
-
397
- breadcrumbs.to_h
398
- end
399
-
400
- def construct_context_hash(opts, exception)
401
- context = {}
402
- context.merge!(Context(opts[:global_context]))
403
- context.merge!(exception_context(exception))
404
- context.merge!(Context(opts[:context]))
405
- context
406
- end
407
-
408
- def fingerprint_from_opts(opts)
409
- callback = opts[:fingerprint]
410
- callback ||= config.exception_fingerprint
411
-
412
- if callback.respond_to?(:call)
413
- callback.call(self)
414
- else
415
- callback
416
- end
417
- end
418
-
419
- def fingerprint_hash
420
- return unless fingerprint
421
- Digest::SHA1.hexdigest(fingerprint.to_s)
422
- end
423
-
424
- def construct_tags(tags)
425
- ret = []
426
- Array(tags).flatten.each do |val|
427
- val.to_s.split(TAG_SEPERATOR).each do |tag|
428
- tag.gsub!(TAG_SANITIZER, STRING_EMPTY)
429
- ret << tag if tag =~ NOT_BLANK
430
- end
431
- end
432
-
433
- ret
434
- end
435
-
436
- def s(data)
437
- Util::Sanitizer.sanitize(data)
438
- end
439
-
440
- # Fetch local variables from first frame of backtrace.
441
- #
442
- # exception - The Exception containing the bindings stack.
443
- #
444
- # Returns a Hash of local variables.
445
- def local_variables_from_exception(exception, config)
446
- return nil unless send_local_variables?(config)
447
- return {} unless Exception === exception
448
- return {} unless exception.respond_to?(:__honeybadger_bindings_stack)
449
- return {} if exception.__honeybadger_bindings_stack.empty?
450
-
451
- if config[:root]
452
- binding = exception.__honeybadger_bindings_stack.find { |b|
453
- if BINDING_HAS_SOURCE_LOCATION
454
- b.source_location[0]
455
- else
456
- b.eval('__FILE__')
457
- end =~ /^#{Regexp.escape(config[:root].to_s)}/
458
- }
459
- end
460
-
461
- binding ||= exception.__honeybadger_bindings_stack[0]
462
-
463
- vars = binding.eval('local_variables')
464
- results =
465
- vars.inject([]) { |acc, arg|
466
- begin
467
- result = binding.eval(arg.to_s)
468
- acc << [arg, result]
469
- rescue NameError
470
- # Do Nothing
471
- end
472
-
473
- acc
474
- }
475
-
476
- result_hash = Hash[results]
477
- request_sanitizer.sanitize(result_hash)
478
- end
479
-
480
- # Should local variables be sent?
481
- #
482
- # Returns true to send local_variables.
483
- def send_local_variables?(config)
484
- config[:'exceptions.local_variables']
485
- end
486
-
487
- # Parse Backtrace from exception backtrace.
488
- #
489
- # backtrace - The Array backtrace from exception.
490
- #
491
- # Returns the Backtrace.
492
- def parse_backtrace(backtrace)
493
- Backtrace.parse(
494
- backtrace,
495
- filters: construct_backtrace_filters(opts),
496
- config: config,
497
- source_radius: config[:'exceptions.source_radius']
498
- ).to_a
499
- end
500
-
501
- # Unwrap the exception so that original exception is ignored or
502
- # reported.
503
- #
504
- # exception - The exception which was rescued.
505
- #
506
- # Returns the Exception to report.
507
- def unwrap_exception(exception)
508
- return exception unless config[:'exceptions.unwrap']
509
- exception_cause(exception) || exception
510
- end
511
-
512
- # Fetch cause from exception.
513
- #
514
- # exception - Exception to fetch cause from.
515
- #
516
- # Returns the Exception cause.
517
- def exception_cause(exception)
518
- e = exception
519
- if e.respond_to?(:cause) && e.cause && e.cause.is_a?(Exception)
520
- e.cause
521
- elsif e.respond_to?(:original_exception) && e.original_exception && e.original_exception.is_a?(Exception)
522
- e.original_exception
523
- elsif e.respond_to?(:continued_exception) && e.continued_exception && e.continued_exception.is_a?(Exception)
524
- e.continued_exception
525
- end
526
- end
527
-
528
- # Create a list of causes.
529
- #
530
- # cause - The first cause to unwrap.
531
- #
532
- # Returns the Array of Cause instances.
533
- def unwrap_causes(cause)
534
- causes, c, i = [], cause, 0
535
-
536
- while c && i < MAX_EXCEPTION_CAUSES
537
- causes << Cause.new(c)
538
- i += 1
539
- c = exception_cause(c)
540
- end
541
-
542
- causes
543
- end
544
-
545
- # Convert list of causes into payload format.
546
- #
547
- # causes - Array of Cause instances.
548
- #
549
- # Returns the Array of causes in Hash payload format.
550
- def prepare_causes(causes)
551
- causes.map {|c|
552
- {
553
- class: c.error_class,
554
- message: c.error_message,
555
- backtrace: parse_backtrace(c.backtrace)
556
- }
557
- }
558
- end
559
-
560
- def params_filters
561
- config.params_filters + rails_params_filters
562
- end
563
-
564
- def rails_params_filters
565
- rack_env && Array(rack_env['action_dispatch.parameter_filter']) or []
566
- end
567
- end
568
- end
1
+ require 'json'
2
+ require 'securerandom'
3
+ require 'forwardable'
4
+
5
+ require 'honeybadger/version'
6
+ require 'honeybadger/backtrace'
7
+ require 'honeybadger/conversions'
8
+ require 'honeybadger/util/stats'
9
+ require 'honeybadger/util/sanitizer'
10
+ require 'honeybadger/util/request_hash'
11
+ require 'honeybadger/util/request_payload'
12
+
13
+ module Honeybadger
14
+ # @api private
15
+ NOTIFIER = {
16
+ name: 'honeybadger-ruby'.freeze,
17
+ url: 'https://github.com/honeybadger-io/honeybadger-ruby'.freeze,
18
+ version: VERSION,
19
+ language: 'ruby'.freeze
20
+ }.freeze
21
+
22
+ # @api private
23
+ # Substitution for gem root in backtrace lines.
24
+ GEM_ROOT = '[GEM_ROOT]'.freeze
25
+
26
+ # @api private
27
+ # Substitution for project root in backtrace lines.
28
+ PROJECT_ROOT = '[PROJECT_ROOT]'.freeze
29
+
30
+ # @api private
31
+ # Empty String (used for equality comparisons and assignment).
32
+ STRING_EMPTY = ''.freeze
33
+
34
+ # @api private
35
+ # A Regexp which matches non-blank characters.
36
+ NOT_BLANK = /\S/.freeze
37
+
38
+ # @api private
39
+ # Matches lines beginning with ./
40
+ RELATIVE_ROOT = Regexp.new('^\.\/').freeze
41
+
42
+ # @api private
43
+ MAX_EXCEPTION_CAUSES = 5
44
+
45
+ # @api private
46
+ # Binding#source_location was added in Ruby 2.6.
47
+ BINDING_HAS_SOURCE_LOCATION = Binding.method_defined?(:source_location)
48
+
49
+ class Notice
50
+ extend Forwardable
51
+
52
+ include Conversions
53
+
54
+ # @api private
55
+ # The String character used to split tag strings.
56
+ TAG_SEPERATOR = /,|\s/.freeze
57
+
58
+ # @api private
59
+ # The Regexp used to strip invalid characters from individual tags.
60
+ TAG_SANITIZER = /\s/.freeze
61
+
62
+ # @api private
63
+ class Cause
64
+ attr_accessor :error_class, :error_message, :backtrace
65
+
66
+ def initialize(cause)
67
+ self.error_class = cause.class.name
68
+ self.error_message = cause.message
69
+ self.backtrace = cause.backtrace
70
+ end
71
+ end
72
+
73
+ # The unique ID of this notice which can be used to reference the error in
74
+ # Honeybadger.
75
+ attr_reader :id
76
+
77
+ # The exception that caused this notice, if any.
78
+ attr_reader :exception
79
+
80
+ # The exception cause if available.
81
+ attr_reader :cause
82
+ def cause=(cause)
83
+ @cause = cause
84
+ @causes = unwrap_causes(cause)
85
+ end
86
+
87
+ # @return [Cause] A list of exception causes (see {Cause})
88
+ attr_reader :causes
89
+
90
+ # The backtrace from the given exception or hash.
91
+ attr_accessor :backtrace
92
+
93
+ # Custom fingerprint for error, used to group similar errors together.
94
+ attr_accessor :fingerprint
95
+
96
+ # Tags which will be applied to error.
97
+ attr_reader :tags
98
+ def tags=(tags)
99
+ @tags = construct_tags(tags)
100
+ end
101
+
102
+ # The name of the class of error (example: RuntimeError).
103
+ attr_accessor :error_class
104
+
105
+ # The message from the exception, or a general description of the error.
106
+ attr_accessor :error_message
107
+
108
+ # The context Hash.
109
+ attr_accessor :context
110
+
111
+ # CGI variables such as HTTP_METHOD.
112
+ attr_accessor :cgi_data
113
+
114
+ # A hash of parameters from the query string or post body.
115
+ attr_accessor :params
116
+ alias_method :parameters, :params
117
+
118
+ # The component (if any) which was used in this request (usually the controller).
119
+ attr_accessor :component
120
+ alias_method :controller, :component
121
+ alias_method :controller=, :component=
122
+
123
+ # The action (if any) that was called in this request.
124
+ attr_accessor :action
125
+
126
+ # A hash of session data from the request.
127
+ attr_accessor :session
128
+
129
+ # The URL at which the error occurred (if any).
130
+ attr_accessor :url
131
+
132
+ # Local variables are extracted from first frame of backtrace.
133
+ attr_accessor :local_variables
134
+
135
+ # The API key used to deliver this notice.
136
+ attr_accessor :api_key
137
+
138
+ # Deprecated: Excerpt from source file.
139
+ attr_reader :source
140
+
141
+ # @return [Breadcrumbs::Collector] The collection of captured breadcrumbs
142
+ attr_accessor :breadcrumbs
143
+
144
+ # Custom details data
145
+ attr_accessor :details
146
+
147
+ # The parsed exception backtrace. Lines in this backtrace that are from installed gems
148
+ # have the base path for gem installs replaced by "[GEM_ROOT]", while those in the project
149
+ # have "[PROJECT_ROOT]".
150
+ # @return [Array<{:number, :file, :method => String}>]
151
+ def parsed_backtrace
152
+ @parsed_backtrace ||= parse_backtrace(backtrace)
153
+ end
154
+
155
+ # @api private
156
+ # Cache project path substitutions for backtrace lines.
157
+ PROJECT_ROOT_CACHE = {}
158
+
159
+ # @api private
160
+ # Cache gem path substitutions for backtrace lines.
161
+ GEM_ROOT_CACHE = {}
162
+
163
+ # @api private
164
+ # A list of backtrace filters to run all the time.
165
+ BACKTRACE_FILTERS = [
166
+ lambda { |line|
167
+ return line unless defined?(Gem)
168
+ GEM_ROOT_CACHE[line] ||= Gem.path.reduce(line) do |line, path|
169
+ line.sub(path, GEM_ROOT)
170
+ end
171
+ },
172
+ lambda { |line, config|
173
+ return line unless config
174
+ c = (PROJECT_ROOT_CACHE[config[:root]] ||= {})
175
+ return c[line] if c.has_key?(line)
176
+ c[line] ||= if config.root_regexp
177
+ line.sub(config.root_regexp, PROJECT_ROOT)
178
+ else
179
+ line
180
+ end
181
+ },
182
+ lambda { |line| line.sub(RELATIVE_ROOT, STRING_EMPTY) },
183
+ lambda { |line| line if line !~ %r{lib/honeybadger} }
184
+ ].freeze
185
+
186
+ # @api private
187
+ def initialize(config, opts = {})
188
+ @now = Time.now.utc
189
+ @pid = Process.pid
190
+ @id = SecureRandom.uuid
191
+ @stats = Util::Stats.all
192
+
193
+ @opts = opts
194
+ @config = config
195
+
196
+ @rack_env = opts.fetch(:rack_env, nil)
197
+ @request_sanitizer = Util::Sanitizer.new(filters: params_filters)
198
+
199
+ @exception = unwrap_exception(opts[:exception])
200
+
201
+ self.error_class = exception_attribute(:error_class, 'Notice') {|exception| exception.class.name }
202
+ self.error_message = exception_attribute(:error_message, 'No message provided') do |exception|
203
+ message = exception.respond_to?(:detailed_message) ?
204
+ exception.detailed_message.sub(" (#{exception.class.name})", '') # Gems like error_highlight append the exception class name
205
+ : exception.message
206
+ "#{exception.class.name}: #{message}"
207
+ end
208
+ self.backtrace = exception_attribute(:backtrace, caller)
209
+ self.cause = opts.key?(:cause) ? opts[:cause] : (exception_cause(@exception) || $!)
210
+
211
+ self.context = construct_context_hash(opts, exception)
212
+ self.local_variables = local_variables_from_exception(exception, config)
213
+ self.api_key = opts[:api_key] || config[:api_key]
214
+ self.tags = construct_tags(opts[:tags]) | construct_tags(context[:tags])
215
+
216
+ self.url = opts[:url] || request_hash[:url] || nil
217
+ self.action = opts[:action] || request_hash[:action] || nil
218
+ self.component = opts[:controller] || opts[:component] || request_hash[:component] || nil
219
+ self.params = opts[:parameters] || opts[:params] || request_hash[:params] || {}
220
+ self.session = opts[:session] || request_hash[:session] || {}
221
+ self.cgi_data = opts[:cgi_data] || request_hash[:cgi_data] || {}
222
+ self.details = opts[:details] || {}
223
+
224
+ self.session = opts[:session][:data] if opts[:session] && opts[:session][:data]
225
+
226
+ self.breadcrumbs = opts[:breadcrumbs] || Breadcrumbs::Collector.new(config)
227
+
228
+ # Fingerprint must be calculated last since callback operates on `self`.
229
+ self.fingerprint = fingerprint_from_opts(opts)
230
+ end
231
+
232
+ # @api private
233
+ # Template used to create JSON payload.
234
+ #
235
+ # @return [Hash] JSON representation of notice.
236
+ def as_json(*args)
237
+ request = construct_request_hash
238
+ request[:context] = s(context)
239
+ request[:local_variables] = local_variables if local_variables
240
+
241
+ {
242
+ api_key: s(api_key),
243
+ notifier: NOTIFIER,
244
+ breadcrumbs: sanitized_breadcrumbs,
245
+ error: {
246
+ token: id,
247
+ class: s(error_class),
248
+ message: s(error_message),
249
+ backtrace: s(parsed_backtrace),
250
+ fingerprint: fingerprint_hash,
251
+ tags: s(tags),
252
+ causes: s(prepare_causes(causes))
253
+ },
254
+ details: s(details),
255
+ request: request,
256
+ server: {
257
+ project_root: s(config[:root]),
258
+ revision: s(config[:revision]),
259
+ environment_name: s(config[:env]),
260
+ hostname: s(config[:hostname]),
261
+ stats: stats,
262
+ time: now,
263
+ pid: pid
264
+ }
265
+ }
266
+ end
267
+
268
+ # Converts the notice to JSON.
269
+ #
270
+ # @return [Hash] The JSON representation of the notice.
271
+ def to_json(*a)
272
+ ::JSON.generate(as_json(*a))
273
+ end
274
+
275
+ # @api private
276
+ # Determines if this notice should be ignored.
277
+ def ignore?
278
+ ignore_by_origin? || ignore_by_class? || ignore_by_callbacks?
279
+ end
280
+
281
+ # Halts the notice and the before_notify callback chain.
282
+ #
283
+ # Returns nothing.
284
+ def halt!
285
+ @halted ||= true
286
+ end
287
+
288
+ # @api private
289
+ # Determines if this notice will be discarded.
290
+ def halted?
291
+ !!@halted
292
+ end
293
+
294
+ private
295
+
296
+ attr_reader :config, :opts, :stats, :now, :pid, :request_sanitizer,
297
+ :rack_env
298
+
299
+ def ignore_by_origin?
300
+ return false if opts[:origin] != :rake
301
+ return false if config[:'exceptions.rescue_rake']
302
+ true
303
+ end
304
+
305
+ def ignore_by_callbacks?
306
+ config.exception_filter &&
307
+ config.exception_filter.call(self)
308
+ end
309
+
310
+ # Gets a property named "attribute" of an exception, either from
311
+ # the #args hash or actual exception (in order of precedence).
312
+ #
313
+ # attribute - A Symbol existing as a key in #args and/or attribute on
314
+ # Exception.
315
+ # default - Default value if no other value is found (optional).
316
+ # block - An optional block which receives an Exception and returns the
317
+ # desired value.
318
+ #
319
+ # Returns attribute value from args or exception, otherwise default.
320
+ def exception_attribute(attribute, default = nil, &block)
321
+ opts[attribute] || (exception && from_exception(attribute, &block)) || default
322
+ end
323
+
324
+ # Gets a property named +attribute+ from an exception.
325
+ #
326
+ # If a block is given, it will be used when getting the property from an
327
+ # exception. The block should accept and exception and return the value for
328
+ # the property.
329
+ #
330
+ # If no block is given, a method with the same name as +attribute+ will be
331
+ # invoked for the value.
332
+ def from_exception(attribute)
333
+ return unless exception
334
+
335
+ if block_given?
336
+ yield(exception)
337
+ else
338
+ exception.send(attribute)
339
+ end
340
+ end
341
+
342
+ # Determines if error class should be ignored.
343
+ #
344
+ # ignored_class_name - The name of the ignored class. May be a
345
+ # string or regexp (optional).
346
+ #
347
+ # Returns true or false.
348
+ def ignore_by_class?(ignored_class = nil)
349
+ @ignore_by_class ||= Proc.new do |ignored_class|
350
+ case error_class
351
+ when (ignored_class.respond_to?(:name) ? ignored_class.name : ignored_class)
352
+ true
353
+ else
354
+ exception && ignored_class.is_a?(Class) && exception.class < ignored_class
355
+ end
356
+ end
357
+
358
+ ignored_class ? @ignore_by_class.call(ignored_class) : config.ignored_classes.any?(&@ignore_by_class)
359
+ end
360
+
361
+ def construct_backtrace_filters(opts)
362
+ [
363
+ config.backtrace_filter
364
+ ].compact | BACKTRACE_FILTERS
365
+ end
366
+
367
+ def request_hash
368
+ @request_hash ||= Util::RequestHash.from_env(rack_env)
369
+ end
370
+
371
+ # Construct the request data.
372
+ #
373
+ # Returns Hash request data.
374
+ def construct_request_hash
375
+ request = {
376
+ url: url,
377
+ component: component,
378
+ action: action,
379
+ params: params,
380
+ session: session,
381
+ cgi_data: cgi_data,
382
+ sanitizer: request_sanitizer
383
+ }
384
+ request.delete_if {|k,v| config.excluded_request_keys.include?(k) }
385
+ Util::RequestPayload.build(request)
386
+ end
387
+
388
+ # Get optional context from exception.
389
+ #
390
+ # Returns the Hash context.
391
+ def exception_context(exception)
392
+ # This extra check exists because the exception itself is not expected to
393
+ # convert to a hash.
394
+ object = exception if exception.respond_to?(:to_honeybadger_context)
395
+ object ||= {}.freeze
396
+
397
+ Context(object)
398
+ end
399
+
400
+ # Sanitize metadata to keep it at a single level and remove any filtered
401
+ # parameters
402
+ def sanitized_breadcrumbs
403
+ sanitizer = Util::Sanitizer.new(max_depth: 1, filters: params_filters)
404
+ breadcrumbs.each do |breadcrumb|
405
+ breadcrumb.metadata = sanitizer.sanitize(breadcrumb.metadata)
406
+ end
407
+
408
+ breadcrumbs.to_h
409
+ end
410
+
411
+ def construct_context_hash(opts, exception)
412
+ context = {}
413
+ context.merge!(Context(opts[:global_context]))
414
+ context.merge!(exception_context(exception))
415
+ context.merge!(Context(opts[:context]))
416
+ context
417
+ end
418
+
419
+ def fingerprint_from_opts(opts)
420
+ callback = opts[:fingerprint]
421
+ callback ||= config.exception_fingerprint
422
+
423
+ if callback.respond_to?(:call)
424
+ callback.call(self)
425
+ else
426
+ callback
427
+ end
428
+ end
429
+
430
+ def fingerprint_hash
431
+ return unless fingerprint
432
+ Digest::SHA1.hexdigest(fingerprint.to_s)
433
+ end
434
+
435
+ def construct_tags(tags)
436
+ ret = []
437
+ Array(tags).flatten.each do |val|
438
+ val.to_s.split(TAG_SEPERATOR).each do |tag|
439
+ tag.gsub!(TAG_SANITIZER, STRING_EMPTY)
440
+ ret << tag if tag =~ NOT_BLANK
441
+ end
442
+ end
443
+
444
+ ret
445
+ end
446
+
447
+ def s(data)
448
+ Util::Sanitizer.sanitize(data)
449
+ end
450
+
451
+ # Fetch local variables from first frame of backtrace.
452
+ #
453
+ # exception - The Exception containing the bindings stack.
454
+ #
455
+ # Returns a Hash of local variables.
456
+ def local_variables_from_exception(exception, config)
457
+ return nil unless send_local_variables?(config)
458
+ return {} unless Exception === exception
459
+ return {} unless exception.respond_to?(:__honeybadger_bindings_stack)
460
+ return {} if exception.__honeybadger_bindings_stack.empty?
461
+
462
+ if config[:root]
463
+ binding = exception.__honeybadger_bindings_stack.find { |b|
464
+ if BINDING_HAS_SOURCE_LOCATION
465
+ b.source_location[0]
466
+ else
467
+ b.eval('__FILE__')
468
+ end =~ /^#{Regexp.escape(config[:root].to_s)}/
469
+ }
470
+ end
471
+
472
+ binding ||= exception.__honeybadger_bindings_stack[0]
473
+
474
+ vars = binding.eval('local_variables')
475
+ results =
476
+ vars.inject([]) { |acc, arg|
477
+ begin
478
+ result = binding.eval(arg.to_s)
479
+ acc << [arg, result]
480
+ rescue NameError
481
+ # Do Nothing
482
+ end
483
+
484
+ acc
485
+ }
486
+
487
+ result_hash = Hash[results]
488
+ request_sanitizer.sanitize(result_hash)
489
+ end
490
+
491
+ # Should local variables be sent?
492
+ #
493
+ # Returns true to send local_variables.
494
+ def send_local_variables?(config)
495
+ config[:'exceptions.local_variables']
496
+ end
497
+
498
+ # Parse Backtrace from exception backtrace.
499
+ #
500
+ # backtrace - The Array backtrace from exception.
501
+ #
502
+ # Returns the Backtrace.
503
+ def parse_backtrace(backtrace)
504
+ Backtrace.parse(
505
+ backtrace,
506
+ filters: construct_backtrace_filters(opts),
507
+ config: config,
508
+ source_radius: config[:'exceptions.source_radius']
509
+ ).to_a
510
+ end
511
+
512
+ # Unwrap the exception so that original exception is ignored or
513
+ # reported.
514
+ #
515
+ # exception - The exception which was rescued.
516
+ #
517
+ # Returns the Exception to report.
518
+ def unwrap_exception(exception)
519
+ return exception unless config[:'exceptions.unwrap']
520
+ exception_cause(exception) || exception
521
+ end
522
+
523
+ # Fetch cause from exception.
524
+ #
525
+ # exception - Exception to fetch cause from.
526
+ #
527
+ # Returns the Exception cause.
528
+ def exception_cause(exception)
529
+ e = exception
530
+ if e.respond_to?(:cause) && e.cause && e.cause.is_a?(Exception)
531
+ e.cause
532
+ elsif e.respond_to?(:original_exception) && e.original_exception && e.original_exception.is_a?(Exception)
533
+ e.original_exception
534
+ elsif e.respond_to?(:continued_exception) && e.continued_exception && e.continued_exception.is_a?(Exception)
535
+ e.continued_exception
536
+ end
537
+ end
538
+
539
+ # Create a list of causes.
540
+ #
541
+ # cause - The first cause to unwrap.
542
+ #
543
+ # Returns the Array of Cause instances.
544
+ def unwrap_causes(cause)
545
+ causes, c, i = [], cause, 0
546
+
547
+ while c && i < MAX_EXCEPTION_CAUSES
548
+ causes << Cause.new(c)
549
+ i += 1
550
+ c = exception_cause(c)
551
+ end
552
+
553
+ causes
554
+ end
555
+
556
+ # Convert list of causes into payload format.
557
+ #
558
+ # causes - Array of Cause instances.
559
+ #
560
+ # Returns the Array of causes in Hash payload format.
561
+ def prepare_causes(causes)
562
+ causes.map {|c|
563
+ {
564
+ class: c.error_class,
565
+ message: c.error_message,
566
+ backtrace: parse_backtrace(c.backtrace)
567
+ }
568
+ }
569
+ end
570
+
571
+ def params_filters
572
+ config.params_filters + rails_params_filters
573
+ end
574
+
575
+ def rails_params_filters
576
+ rack_env && Array(rack_env['action_dispatch.parameter_filter']) or []
577
+ end
578
+ end
579
+ end