diff --git a/CHANGELOG.md b/CHANGELOG.md index 368f2701..8bdd01d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/airbrake-ruby.rb b/lib/airbrake-ruby.rb index 56324ff5..3d29ddeb 100644 --- a/lib/airbrake-ruby.rb +++ b/lib/airbrake-ruby.rb @@ -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. diff --git a/lib/airbrake-ruby/backtrace.rb b/lib/airbrake-ruby/backtrace.rb index 4145cec8..bb2a62dc 100644 --- a/lib/airbrake-ruby/backtrace.rb +++ b/lib/airbrake-ruby/backtrace.rb @@ -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 ' - RUBY_STACKFRAME_REGEXP = %r{\A - (?.+) # Matches './spec/notice_spec.rb' - : - (?\d+) # Matches '43' - :in\s - `(?.*)' # Matches "`block (3 levels) in '" - \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 - (?.+) # Matches 'org.jruby.ast.NewlineNode.interpret - \( - (?[^:]+) # Matches 'NewlineNode.java' - :? - (?\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)? - (?.+) # Matches '/foo/bar/baz.ext' - : - (?\d+)? # Matches '43' or nothing - (?: - in\s`(?.+)' # Matches "in `func'" - | - :in\s(?.+) # 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 - (?:"(?.+)",\s)? - line\s(?\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)' - (?.+)\s\((?.+):(?\d+):\d+\) - | - # Matches 'bootstrap_node.js:467:3' - (?.+):(?\d+):\d+(?) - | - # 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 ' + RUBY = %r{\A + (?.+) # Matches './spec/notice_spec.rb' + : + (?\d+) # Matches '43' + :in\s + `(?.*)' # Matches "`block (3 levels) in '" + \z}x + + ## + # @return [Regexp] the pattern that matches JRuby Java stack frames, such + # as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105) + JAVA = /\A + (?.+) # Matches 'org.jruby.ast.NewlineNode.interpret + \( + (?[^:]+) # Matches 'NewlineNode.java' + :? + (?\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)? + (?.+) # Matches '/foo/bar/baz.ext' + : + (?\d+)? # Matches '43' or nothing + (?: + in\s`(?.+)' # Matches "in `func'" + | + :in\s(?.+) # 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 + (?:"(?.+)",\s)? + line\s(?\d+) + | + #{GENERIC} + ) + \z/x + + ## + # @return [Regexp] the pattern that matches CoffeeScript backtraces + # usually coming from Rails & ExecJS + EXECJS = /\A + (?: + # Matches 'compile ((execjs):6692:19)' + (?.+)\s\((?.+):(?\d+):\d+\) + | + # Matches 'bootstrap_node.js:467:3' + (?.+):(?\d+):\d+(?) + | + # 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. @@ -121,13 +128,13 @@ 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 @@ -135,13 +142,24 @@ 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], @@ -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 diff --git a/spec/backtrace_spec.rb b/spec/backtrace_spec.rb index 43a51fbb..2f13edea 100644 --- a/spec/backtrace_spec.rb +++ b/spec/backtrace_spec.rb @@ -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 (:1:10)', @@ -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: '', line: 1, function: 'eval' }, @@ -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