Skip to content

Add binding_of_caller support for recording local variables #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ group :development do

# DEVELOPMENT
gem 'jeweler'
gem 'binding_of_caller', :platforms => [:mri_19, :mri_20]
end
14 changes: 9 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.2.2)
binding_of_caller (0.7.1)
debug_inspector (>= 0.0.1)
debug_inspector (0.0.2)
diff-lcs (1.2.4)
fakefs (0.4.2)
git (1.2.5)
jeweler (1.8.4)
bundler (~> 1.0)
git (>= 1.2.5)
rake
rdoc
json (1.7.7)
json (1.7.7-java)
json (1.8.0)
json (1.8.0-java)
rake (10.0.4)
rdoc (4.0.1)
json (~> 1.4)
redcarpet (2.2.2)
redcarpet (2.3.0)
rspec (2.13.0)
rspec-core (~> 2.13.0)
rspec-expectations (~> 2.13.0)
Expand All @@ -23,13 +26,14 @@ GEM
rspec-expectations (2.13.0)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.13.1)
yard (0.8.5.2)
yard (0.8.6.1)

PLATFORMS
java
ruby

DEPENDENCIES
binding_of_caller
fakefs
jeweler
json
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ end
If monkey-patching doesn't appeal to you, then don't load
`squash/ruby/exception_additions`; it's not required for the client to work.

#### Recording Local Variables

The Squash Ruby gem can be configured to read and record the values of local
variables at each stack frame, and transmit this information back to the
website. The website can display this information contextually to aid developers
in troubleshooting an exception.

In order to use this functionality, you'll need to do the following:

* Include the [binding_of_caller](https://github.com/banister/binding_of_caller)
gem in your project. (This gem is only compatible with MRI 1.9, MRI 2.0, and
Rubinius.)
* Add `require 'squash/ruby/exception_additions'` in your project if it's not
there already.

Note that using this feature will impact performance when recording and
transmitting exception data to Squash.

#### Ignoring Exceptions

You can ignore certain exceptions within a block of code if those exceptions are
Expand Down
249 changes: 154 additions & 95 deletions lib/squash/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
rescue LoadError
# optional
end
begin
require 'binding_of_caller'
rescue LoadError
# optional
end

# Container for methods relating to notifying Squash of exceptions.

Expand Down Expand Up @@ -442,11 +447,7 @@ def self.exception_info_hash(exception, occurred, user_data, parents)
environment_data.merge(top_level_user_data).merge(
'class_name' => exception.class.to_s,
'message' => exception.to_s,
'backtraces' => [{
'name' => "Active Thread/Fiber",
'faulted' => true,
'backtrace' => prepare_backtrace(exception.backtrace)
}],
'backtraces' => [prepare_backtrace(exception)],
'occurred_at' => occurred,
'revision' => current_revision,

Expand All @@ -460,116 +461,174 @@ def self.exception_info_hash(exception, occurred, user_data, parents)
'parent_exceptions' => parents.nil? ? nil : parents.map do |parent|
{'class_name' => parent.class.to_s,
'message' => parent.to_s,
'backtraces' => [{
'name' => "Active Thread/Fiber",
'faulted' => true,
'backtrace' => prepare_backtrace(parent.backtrace)
}],
'backtraces' => [prepare_backtrace(parent)],
'association' => 'original_exception',
'ivars' => instance_variable_hash(parent)}
end
)
end

def self.prepare_backtrace(bt)
def self.prepare_backtrace(exception, name='Active Thread/Fiber', faulted=true)
if defined?(JRuby)
bt.map(&:strip).map do |element|
if element =~ /^((?:[a-z0-9_$]+\.)*(?:[a-z0-9_$]+))\.(\w+)\((\w+.java):(\d+)\)$/i
# special JRuby backtrace element of the form "org.jruby.RubyHash$27.visit(RubyHash.java:1646)"
{
'type' => 'obfuscated',
'file' => $3,
'line' => $4.to_i,
'symbol' => $2,
'class' => $1
}
elsif element =~ /^(.+?)\.(\w+)\(Native Method\)$/
{
'type' => 'java_native',
'symbol' => $2,
'class' => $1
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\.call\(.+\)$/
{
'type' => 'jruby_block',
'class' => $1,
'symbol' => $2
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?):(\d+)\)$/
{
'file' => $3,
'line' => $4.to_i,
'symbol' => "#{$1}##{$2}"
}
elsif element =~ /^.+\.call\(.+?(\w+)\.gen\)$/
{
'type' => 'asm_invoker',
'file' => $1 + '.gen'
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?)\)$/
{
'file' => $3,
'type' => 'jruby_noline',
'symbol' => "#{$1}##{$2}"
}
prepare_jruby_backtrace exception.backtrace, name, faulted
elsif exception.respond_to?(:_squash_bindings_stack)
prepare_advanced_mri_backtrace exception._squash_bindings_stack, name, faulted
else
prepare_mri_backtrace exception.backtrace, name, faulted
end
end

def self.prepare_mri_backtrace(bt, name, faulted)
trace = bt.map do |element|
file, line, method = decode_basic_backtrace_line(element)

{
'file' => file,
'line' => line,
'symbol' => method
}
end

{
'name' => name,
'faulted' => faulted,
'backtrace' => trace
}
end

def self.decode_basic_backtrace_line(element)
file, line, method = element.split(':')
line = line.to_i
line = nil if line < 1

file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'

if method =~ /^in `(.+)'$/
method = $1
end
method = nil if method && method.empty?
return file, line, method
end

def self.prepare_advanced_mri_backtrace(bindings, name, faulted)
data = {
'name' => name,
'faulted' => faulted,
'backtrace' => []
}

bindings.each do |binding|
if respond_to?(:caller_locations)
file = binding.eval('caller_locations(0, 1)[0].path')
line = binding.eval('caller_locations(0, 1)[0].lineno')
method = binding.eval('caller_locations(0, 1)[0].label')
else
file, line, method = decode_basic_backtrace_line(binding.eval('caller(0, 1)[0]'))
end

if binding.eval('defined?(local_variables)') == 'method'
locals = binding.eval('local_variables')
locals = locals.inject({}) { |hsh, lvar| hsh[lvar] = binding.eval(lvar.to_s); hsh }
else
locals = nil
end

data['backtrace'] << {
'file' => file,
'line' => line,
'symbol' => method,
'locals' => valueify(locals, true)
}
end

data
end

def self.prepare_jruby_backtrace(bt, name, faulted)
trace = bt.map(&:strip).map do |element|
if element =~ /^((?:[a-z0-9_$]+\.)*(?:[a-z0-9_$]+))\.(\w+)\((\w+.java):(\d+)\)$/i
# special JRuby backtrace element of the form "org.jruby.RubyHash$27.visit(RubyHash.java:1646)"
{
'type' => 'obfuscated',
'file' => $3,
'line' => $4.to_i,
'symbol' => $2,
'class' => $1
}
elsif element =~ /^(.+?)\.(\w+)\(Native Method\)$/
{
'type' => 'java_native',
'symbol' => $2,
'class' => $1
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\.call\(.+\)$/
{
'type' => 'jruby_block',
'class' => $1,
'symbol' => $2
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?):(\d+)\)$/
{
'file' => $3,
'line' => $4.to_i,
'symbol' => "#{$1}##{$2}"
}
elsif element =~ /^.+\.call\(.+?(\w+)\.gen\)$/
{
'type' => 'asm_invoker',
'file' => $1 + '.gen'
}
elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?)\)$/
{
'file' => $3,
'type' => 'jruby_noline',
'symbol' => "#{$1}##{$2}"
}
else
if element.include?(' at ')
method, fileline = element.split(' at ')
method.lstrip!
file, line = fileline.split(':')
else
if element.include?(' at ')
method, fileline = element.split(' at ')
method.lstrip!
file, line = fileline.split(':')
else
file, line, method = element.split(':')
if method =~ /^in `(.+)'$/
method = $1
end
method = nil if method && method.empty?
end
line = line.to_i
line = nil if line < 1
file, line, method = element.split(':')
if method =~ /^in `(.+)'$/
method = $1
end
method = nil if method && method.empty?

# it could still be a java backtrace, even if it's not the special format
if file[-5, 5] == '.java'
{
'type' => 'obfuscated',
'file' => file.split('/').last,
'line' => line,
'symbol' => method,
'class' => file.sub(/\.java$/, '').gsub('/', '.')
}
else
# ok now we're sure it's a ruby backtrace
file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'
{
'file' => file,
'line' => line,
'symbol' => method
}
end
end
end
else
bt.map do |element|
file, line, method = element.split(':')
line = line.to_i
line = line.to_i
line = nil if line < 1

file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'

if method =~ /^in `(.+)'$/
method = $1
end
method = nil if method && method.empty?
{
'file' => file,
'line' => line,
'symbol' => method
}

# it could still be a java backtrace, even if it's not the special format
if file[-5, 5] == '.java'
{
'type' => 'obfuscated',
'file' => file.split('/').last,
'line' => line,
'symbol' => method,
'class' => file.sub(/\.java$/, '').gsub('/', '.')
}
else
# ok now we're sure it's a ruby backtrace
file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'
{
'file' => file,
'line' => line,
'symbol' => method
}
end
end
end

{
'name' => name,
'faulted' => faulted,
'backtrace' => trace
}
end

def self.valueify(instance, elements_only=false)
Expand Down
24 changes: 24 additions & 0 deletions lib/squash/ruby/exception_additions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,28 @@ def user_data(data)
end
self
end

# This code courtesy of better_errors (MIT license):
# https://github.com/charliesome/better_errors

if binding.respond_to?(:callers)
original_set_backtrace = instance_method(:set_backtrace)

define_method :set_backtrace do |*args|
unless Thread.current[:_squash_exception_lock]
Thread.current[:_squash_exception_lock] = true
begin
@_squash_bindings_stack = binding.callers[1..-1]
ensure
Thread.current[:_squash_exception_lock] = false
end
end
original_set_backtrace.bind(self).call(*args)
end

def _squash_bindings_stack
@_squash_bindings_stack || []
end
end
end

Loading