honeybadger 5.2.0 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +722 -718
  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 +55 -55
  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 -336
  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 +416 -416
  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/hanami.rb +19 -19
  39. data/lib/honeybadger/init/rails.rb +38 -38
  40. data/lib/honeybadger/init/rake.rb +66 -66
  41. data/lib/honeybadger/init/ruby.rb +11 -11
  42. data/lib/honeybadger/init/sinatra.rb +57 -57
  43. data/lib/honeybadger/logging.rb +177 -177
  44. data/lib/honeybadger/notice.rb +579 -579
  45. data/lib/honeybadger/plugin.rb +210 -210
  46. data/lib/honeybadger/plugins/breadcrumbs.rb +111 -111
  47. data/lib/honeybadger/plugins/delayed_job/plugin.rb +56 -56
  48. data/lib/honeybadger/plugins/delayed_job.rb +22 -22
  49. data/lib/honeybadger/plugins/faktory.rb +52 -52
  50. data/lib/honeybadger/plugins/lambda.rb +71 -71
  51. data/lib/honeybadger/plugins/local_variables.rb +44 -44
  52. data/lib/honeybadger/plugins/passenger.rb +23 -23
  53. data/lib/honeybadger/plugins/rails.rb +72 -72
  54. data/lib/honeybadger/plugins/resque.rb +72 -72
  55. data/lib/honeybadger/plugins/shoryuken.rb +52 -52
  56. data/lib/honeybadger/plugins/sidekiq.rb +71 -71
  57. data/lib/honeybadger/plugins/sucker_punch.rb +18 -18
  58. data/lib/honeybadger/plugins/thor.rb +32 -32
  59. data/lib/honeybadger/plugins/warden.rb +19 -19
  60. data/lib/honeybadger/rack/error_notifier.rb +92 -92
  61. data/lib/honeybadger/rack/user_feedback.rb +88 -88
  62. data/lib/honeybadger/rack/user_informer.rb +45 -45
  63. data/lib/honeybadger/ruby.rb +2 -2
  64. data/lib/honeybadger/singleton.rb +103 -103
  65. data/lib/honeybadger/tasks.rb +22 -22
  66. data/lib/honeybadger/templates/feedback_form.erb +84 -84
  67. data/lib/honeybadger/util/http.rb +92 -92
  68. data/lib/honeybadger/util/lambda.rb +32 -32
  69. data/lib/honeybadger/util/request_hash.rb +73 -73
  70. data/lib/honeybadger/util/request_payload.rb +41 -41
  71. data/lib/honeybadger/util/revision.rb +39 -39
  72. data/lib/honeybadger/util/sanitizer.rb +214 -214
  73. data/lib/honeybadger/util/sql.rb +34 -34
  74. data/lib/honeybadger/util/stats.rb +50 -50
  75. data/lib/honeybadger/version.rb +4 -4
  76. data/lib/honeybadger/worker.rb +253 -253
  77. data/lib/honeybadger.rb +13 -13
  78. data/resources/ca-bundle.crt +3376 -3376
  79. data/vendor/capistrano-honeybadger/lib/capistrano/honeybadger.rb +5 -5
  80. data/vendor/capistrano-honeybadger/lib/capistrano/tasks/deploy.cap +89 -89
  81. data/vendor/capistrano-honeybadger/lib/honeybadger/capistrano/legacy.rb +47 -47
  82. data/vendor/capistrano-honeybadger/lib/honeybadger/capistrano.rb +2 -2
  83. data/vendor/cli/inifile.rb +628 -628
  84. data/vendor/cli/thor/actions/create_file.rb +103 -103
  85. data/vendor/cli/thor/actions/create_link.rb +59 -59
  86. data/vendor/cli/thor/actions/directory.rb +118 -118
  87. data/vendor/cli/thor/actions/empty_directory.rb +135 -135
  88. data/vendor/cli/thor/actions/file_manipulation.rb +316 -316
  89. data/vendor/cli/thor/actions/inject_into_file.rb +107 -107
  90. data/vendor/cli/thor/actions.rb +319 -319
  91. data/vendor/cli/thor/base.rb +656 -656
  92. data/vendor/cli/thor/command.rb +133 -133
  93. data/vendor/cli/thor/core_ext/hash_with_indifferent_access.rb +77 -77
  94. data/vendor/cli/thor/core_ext/io_binary_read.rb +10 -10
  95. data/vendor/cli/thor/core_ext/ordered_hash.rb +98 -98
  96. data/vendor/cli/thor/error.rb +32 -32
  97. data/vendor/cli/thor/group.rb +281 -281
  98. data/vendor/cli/thor/invocation.rb +178 -178
  99. data/vendor/cli/thor/line_editor/basic.rb +35 -35
  100. data/vendor/cli/thor/line_editor/readline.rb +88 -88
  101. data/vendor/cli/thor/line_editor.rb +17 -17
  102. data/vendor/cli/thor/parser/argument.rb +73 -73
  103. data/vendor/cli/thor/parser/arguments.rb +175 -175
  104. data/vendor/cli/thor/parser/option.rb +125 -125
  105. data/vendor/cli/thor/parser/options.rb +218 -218
  106. data/vendor/cli/thor/parser.rb +4 -4
  107. data/vendor/cli/thor/rake_compat.rb +71 -71
  108. data/vendor/cli/thor/runner.rb +322 -322
  109. data/vendor/cli/thor/shell/basic.rb +421 -421
  110. data/vendor/cli/thor/shell/color.rb +149 -149
  111. data/vendor/cli/thor/shell/html.rb +126 -126
  112. data/vendor/cli/thor/shell.rb +81 -81
  113. data/vendor/cli/thor/util.rb +267 -267
  114. data/vendor/cli/thor/version.rb +3 -3
  115. data/vendor/cli/thor.rb +484 -484
  116. metadata +3 -3
@@ -1,579 +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
- # 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
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(highlight: false).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