Skip to content

Commit

Permalink
Collect backtrace
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo committed Jun 7, 2024
1 parent c4db672 commit a7bce46
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 88 deletions.
2 changes: 2 additions & 0 deletions lib/telebugs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
require_relative "telebugs/sender"
require_relative "telebugs/wrapped_error"
require_relative "telebugs/notice"
require_relative "telebugs/error_message"
require_relative "telebugs/backtrace"

module Telebugs
# The general error that this library uses when it wants to raise.
Expand Down
156 changes: 156 additions & 0 deletions lib/telebugs/backtrace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# frozen_string_literal: true

module Telebugs
# Represents a cross-Ruby backtrace from exceptions (including JRuby Java
# exceptions). Provides information about stack frames (such as line number,
# file and method) in convenient for Telebugs format.
module Backtrace
module Patterns
# The pattern that matches standard Ruby stack frames, such as
# ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
RUBY = %r{\A
(?<file>.+) # Matches './spec/notice_spec.rb'
:
(?<line>\d+) # Matches '43'
:in\s
`(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
\z}x

# The pattern that matches JRuby Java stack frames, such as
# org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
JAVA = %r{\A
(?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret'
\(
(?<file>
(?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb'
|
(?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...'
|
[^:]+ # Matches 'NewlineNode.java'
)
:?
(?<line>\d+)? # Matches '105'
\)
\z}x

# The pattern that tries to assume what a generic stack frame might look
# like, when exception's backtrace is set manually.
GENERIC = %r{\A
(?:from\s)?
(?<file>.+) # Matches '/foo/bar/baz.ext'
:
(?<line>\d+)? # Matches '43' or nothing
(?:
in\s`(?<function>.+)' # Matches "in `func'"
|
:in\s(?<function>.+) # Matches ":in func"
)? # ... or nothing
\z}x

# The pattern that matches exceptions from PL/SQL such as
# ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
# @note This is raised by https://github.com/kubo/ruby-oci8
OCI = /\A
(?:
ORA-\d{5}
:\sat\s
(?:"(?<function>.+)",\s)?
line\s(?<line>\d+)
|
#{GENERIC}
)
\z/x

# The pattern that matches CoffeeScript backtraces usually coming from
# Rails & ExecJS
EXECJS = /\A
(?:
# Matches 'compile ((execjs):6692:19)'
(?<function>.+)\s\((?<file>.+):(?<line>\d+):\d+\)
|
# Matches 'bootstrap_node.js:467:3'
(?<file>.+):(?<line>\d+):\d+(?<function>)
|
# Matches the Ruby part of the backtrace
#{RUBY}
)
\z/x
end

def self.parse(error)
return [] if error.backtrace.nil? || error.backtrace.none?

parse_backtrace(error)
end

# Checks whether the given exception was generated by JRuby's VM.
def self.java_exception?(error)
if defined?(Java::JavaLang::Throwable) &&
error.is_a?(Java::JavaLang::Throwable)
return true
end

return false unless error.respond_to?(:backtrace)

(Patterns::JAVA =~ error.backtrace.first) != nil
end

class << self
private

def best_regexp_for(error)
if java_exception?(error)
Patterns::JAVA
elsif oci_exception?(error)
Patterns::OCI
elsif execjs_exception?(error)
Patterns::EXECJS
else
Patterns::RUBY
end
end

def oci_exception?(error)
defined?(OCIError) && error.is_a?(OCIError)
end

def execjs_exception?(error)
return false unless defined?(ExecJS::RuntimeError)
return true if error.is_a?(ExecJS::RuntimeError)
return true if error.cause && error.cause.is_a?(ExecJS::RuntimeError)

false
end

def stack_frame(regexp, stackframe)
if (match = match_frame(regexp, stackframe))
return {
file: match[:file],
line: (Integer(match[:line]) if match[:line]),
function: match[:function]
}
end

{file: nil, line: nil, function: stackframe}
end

def match_frame(regexp, stackframe)
match = regexp.match(stackframe)
return match if match

Patterns::GENERIC.match(stackframe)
end

def parse_backtrace(error)
regexp = best_regexp_for(error)

error.backtrace.map.with_index do |stackframe, i|
frame = stack_frame(regexp, stackframe)
next(frame) unless frame[:file]

frame
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/telebugs/error_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Telebugs
# Parses error messages to make them more consistent.
module ErrorMessage
# On Ruby 3.1+, the error highlighting gem can produce messages that can
# span over multiple lines. We don't want to display multiline error titles.
# Therefore, we want to strip out the higlighting part so that the errors
# look consistent.
RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n"

# The options for +String#encode+
ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze

def self.parse(error)
return unless (msg = error.message)

msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS)
.split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
.first
end
end
end
20 changes: 2 additions & 18 deletions lib/telebugs/notice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,6 @@ class Notice
Encoding::UndefinedConversionError
].freeze

# On Ruby 3.1+, the error highlighting gem can produce messages that can
# span over multiple lines. We don't want to display multiline error titles.
# Therefore, we want to strip out the higlighting part so that the errors
# look consistent.
RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n"

# The options for +String#encode+
ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze

# The maxium size of the JSON payload in bytes
MAX_NOTICE_SIZE = 64000

Expand Down Expand Up @@ -54,19 +45,12 @@ def errors_as_json(error)
WrappedError.new(error).unwrap.map do |e|
{
type: e.class.name,
message: message(e)
message: ErrorMessage.parse(e),
backtrace: Backtrace.parse(e)
}
end
end

def message(error)
return unless (msg = error.message)

msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS)
.split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
.first
end

def truncate
0
end
Expand Down
Loading

0 comments on commit a7bce46

Please sign in to comment.