Skip to content

Commit

Permalink
Merge pull request #110 from airbrake/cause-1-9-3-fix
Browse files Browse the repository at this point in the history
backtrace: fix ExecJS backtraces support for Ruby 1.9.3
  • Loading branch information
kyrylo authored Aug 17, 2016
2 parents 47c97cf + 5ca1e28 commit 622356f
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 82 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Airbrake Ruby Changelog

### master

* Fixed support for ExecJS backtraces for Ruby 1.9.3 sometimes resulting in
`NameError` ([#110](https://github.com/airbrake/airbrake-ruby/pull/110))

### [v1.4.5][v1.4.5] (August 15, 2016)

* Added support for CoffeeScript/ExecJS backtraces
Expand Down
5 changes: 5 additions & 0 deletions lib/airbrake-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ module Airbrake
# @return [String] the label to be prepended to the log output
LOG_LABEL = '**Airbrake:'.freeze

##
# @return [Boolean] true if current Ruby is Ruby 1.9.*. The result is used
# for special cases where we need to work around older implementations
RUBY_19 = RUBY_VERSION.start_with?('1.9')

##
# A Hash that holds all notifiers. The keys of the Hash are notifier
# names, the values are Airbrake::Notifier instances.
Expand Down
166 changes: 92 additions & 74 deletions lib/airbrake-ruby/backtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,74 +11,81 @@ module Airbrake
# Backtrace.parse($!, Logger.new(STDOUT))
# end
module Backtrace
##
# @return [Regexp] the pattern that matches standard Ruby stack frames,
# such as ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
RUBY_STACKFRAME_REGEXP = %r{\A
(?<file>.+) # Matches './spec/notice_spec.rb'
:
(?<line>\d+) # Matches '43'
:in\s
`(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
\z}x

##
# @return [Regexp] the template that matches JRuby Java stack frames, such
# as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
JAVA_STACKFRAME_REGEXP = /\A
(?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret
\(
(?<file>[^:]+) # Matches 'NewlineNode.java'
:?
(?<line>\d+)? # Matches '105'
\)
\z/x

##
# @return [Regexp] the template that tries to assume what a generic stack
# frame might look like, when exception's backtrace is set manually.
GENERIC_STACKFRAME_REGEXP = %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

##
# @return [Regexp] the template 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_STACKFRAME_REGEXP = /\A
(?:
ORA-\d{5}
:\sat\s
(?:"(?<function>.+)",\s)?
line\s(?<line>\d+)
|
#{GENERIC_STACKFRAME_REGEXP}
)
\z/x

##
# @return [Regexp] the template that matches CoffeeScript backtraces
# usually coming from Rails & ExecJS
EXECJS_STACKFRAME_REGEXP = /\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_STACKFRAME_REGEXP}
)
\z/x
module Patterns
##
# @return [Regexp] 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

##
# @return [Regexp] the pattern that matches JRuby Java stack frames, such
# as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
JAVA = /\A
(?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret
\(
(?<file>[^:]+) # Matches 'NewlineNode.java'
:?
(?<line>\d+)? # Matches '105'
\)
\z/x

##
# @return [Regexp] 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

##
# @return [Regexp] 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

##
# @return [Regexp] 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

##
# @return [Regexp] +EXECJS+ pattern without named captures and
# uncommon frames
EXECJS_SIMPLIFIED = /\A.+ \(.+:\d+:\d+\)\z/
end

##
# Parses an exception's backtrace.
Expand Down Expand Up @@ -121,27 +128,38 @@ class << self

def best_regexp_for(exception)
if java_exception?(exception)
JAVA_STACKFRAME_REGEXP
Patterns::JAVA
elsif oci_exception?(exception)
OCI_STACKFRAME_REGEXP
Patterns::OCI
elsif execjs_exception?(exception)
EXECJS_STACKFRAME_REGEXP
Patterns::EXECJS
else
RUBY_STACKFRAME_REGEXP
Patterns::RUBY
end
end

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

# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
def execjs_exception?(exception)
return false unless defined?(ExecJS::RuntimeError)
return true if exception.is_a?(ExecJS::RuntimeError)
return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)

if Airbrake::RUBY_19
# Ruby 1.9 doesn't support Exception#cause. We work around this by
# parsing backtraces. It's slow, so we check only a few first frames.
exception.backtrace[0..2].each do |frame|
return true if frame =~ Patterns::EXECJS_SIMPLIFIED
end
elsif exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
return true
end

false
end
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity

def stack_frame(match)
{ file: match[:file],
Expand All @@ -153,7 +171,7 @@ def match_frame(regexp, stackframe)
match = regexp.match(stackframe)
return match if match

GENERIC_STACKFRAME_REGEXP.match(stackframe)
Patterns::GENERIC.match(stackframe)
end
end
end
Expand Down
38 changes: 30 additions & 8 deletions spec/backtrace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
end
end

context "given an ExecJS backtrace" do
context "given an ExecJS exception" do
let(:bt) do
['compile ((execjs):6692:19)',
'eval (<anonymous>:1:10)',
Expand All @@ -203,8 +203,6 @@
"/opt/rubies/ruby-2.3.1/lib/ruby/2.3.0/benchmark.rb:308:in `realtime'"]
end

let(:ex) { ExecJS::RuntimeError.new.tap { |e| e.set_backtrace(bt) } }

let(:parsed_backtrace) do
[{ file: '(execjs)', line: 6692, function: 'compile' },
{ file: '<anonymous>', line: 1, function: 'eval' },
Expand All @@ -218,11 +216,35 @@
function: 'realtime' }]
end

it "returns a properly formatted array of hashes" do
stub_const('ExecJS::RuntimeError', AirbrakeTestError)
expect(
described_class.parse(ex, Logger.new('/dev/null'))
).to eq(parsed_backtrace)
context "when not on Ruby 1.9" do
let(:ex) { ExecJS::RuntimeError.new.tap { |e| e.set_backtrace(bt) } }

it "returns a properly formatted array of hashes" do
stub_const('ExecJS::RuntimeError', AirbrakeTestError)
stub_const('Airbrake::RUBY_19', false)

expect(
described_class.parse(ex, Logger.new('/dev/null'))
).to eq(parsed_backtrace)
end
end

context "when on Ruby 1.9" do
context "and when exception's class isn't ExecJS" do
let(:ex) do
ActionView::Template::Error.new.tap { |e| e.set_backtrace(bt) }
end

it "returns a properly formatted array of hashes" do
stub_const('ActionView::Template::Error', AirbrakeTestError)
stub_const('ExecJS::RuntimeError', NameError)
stub_const('Airbrake::RUBY_19', true)

expect(
described_class.parse(ex, Logger.new('/dev/null'))
).to eq(parsed_backtrace)
end
end
end
end
end
Expand Down

0 comments on commit 622356f

Please sign in to comment.