Skip to content
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

Send code and context frame data #523

Merged
merged 2 commits into from
Sep 15, 2016
Merged
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This is the Ruby library for Rollbar. It will instrument many kinds of Ruby appl
- [Before process hook](#before-process-hook)
- [Transform hook](#transform-hook)
- [The Scope](#the-scope)
- [Code and context](#code-and-context)
- [Silencing exceptions at runtime](#silencing-exceptions-at-runtime)
- [Sending backtrace without rescued exceptions](#sending-backtrace-without-rescued-exceptions)
- [ActiveJob integration](#activejob-integration)
Expand Down Expand Up @@ -607,6 +608,18 @@ your_handler = proc do |options|
end
```

## Code and context

By default we send the following values for each backtrace frame: `filename`, `lineno` and `method`. You can configure Rollbar to additionally send `code` (the actual line of code) and `context` (lines before and after) for each frame.

Since the backtrace can be very long, you can configure to send this data for all the frames or only your in-project frames. There are three levels: `:none` (default), `:app` (only your project files) and `all`. Example:

```ruby
Rollbar.configure do |config|
config.send_extra_frame_data = :app
end
```

## Silencing exceptions at runtime

If you just want to disable exception reporting for a single block, use ```Rollbar.silenced```:
Expand Down
14 changes: 14 additions & 0 deletions lib/rollbar/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module Rollbar
class Configuration
SEND_EXTRA_FRAME_DATA_OPTIONS = [:none, :app, :all].freeze

attr_accessor :access_token
attr_accessor :async_handler
Expand Down Expand Up @@ -52,6 +53,7 @@ class Configuration
attr_accessor :use_eventmachine
attr_accessor :web_base
attr_accessor :write_to_file
attr_reader :send_extra_frame_data

attr_reader :project_gem_paths

Expand Down Expand Up @@ -111,6 +113,8 @@ def initialize
@verify_ssl_peer = true
@web_base = DEFAULT_WEB_BASE
@write_to_file = false
@send_extra_frame_data = :none
@project_gem_paths = []
end

def initialize_copy(orig)
Expand Down Expand Up @@ -192,6 +196,16 @@ def transform=(*handler)
@transform = Array(handler)
end

def send_extra_frame_data=(value)
unless SEND_EXTRA_FRAME_DATA_OPTIONS.include?(value)
logger.warning("Wrong 'send_extra_frame_data' value, :none, :app or :full is expected")

return
end

@send_extra_frame_data = value
end

# allow params to be read like a hash
def [](option)
send(option)
Expand Down
4 changes: 2 additions & 2 deletions lib/rollbar/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def build_data
},
:body => build_body
}
data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths
data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths.any?
data[:code_version] = configuration.code_version if configuration.code_version
data[:uuid] = SecureRandom.uuid if defined?(SecureRandom) && SecureRandom.respond_to?(:uuid)

Expand Down Expand Up @@ -148,7 +148,7 @@ def build_backtrace_body
:configuration => configuration
)

backtrace.build
backtrace.to_h
end

def build_extra
Expand Down
43 changes: 26 additions & 17 deletions lib/rollbar/item/backtrace.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
require 'rollbar/item/frame'

module Rollbar
class Item
class Backtrace
attr_reader :exception
attr_reader :message
attr_reader :extra
attr_reader :configuration
attr_reader :files

private :files

def initialize(exception, options = {})
@exception = exception
@message = options[:message]
@extra = options[:extra]
@configuration = options[:configuration]
@files = {}
end

def build
def to_h
traces = trace_chain

traces[0][:exception][:description] = message if message
Expand All @@ -26,10 +32,23 @@ def build
end
end

alias_method :build, :to_h

def get_file_lines(filename)
files[filename] ||= read_file(filename)
end

private

def read_file(filename)
return unless File.exist?(filename)

File.read(filename).split("\n")
rescue
nil
end

def trace_chain
exception
traces = [trace_data(exception)]
visited = [exception]

Expand All @@ -45,29 +64,19 @@ def trace_chain
end

def trace_data(current_exception)
frames = reduce_frames(current_exception)
# reverse so that the order is as rollbar expects
frames.reverse!

{
:frames => frames,
:frames => map_frames(current_exception),
:exception => {
:class => current_exception.class.name,
:message => current_exception.message
}
}
end

def reduce_frames(current_exception)
exception_backtrace(current_exception).map do |frame|
# parse the line
match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)

if match
{ :filename => match[1], :lineno => match[2].to_i, :method => match[3] }
else
{ :filename => '<unknown>', :lineno => 0, :method => frame }
end
def map_frames(current_exception)
exception_backtrace(current_exception).reverse.map do |frame|
Rollbar::Item::Frame.new(self, frame,
:configuration => configuration).to_h
end
end

Expand Down
112 changes: 112 additions & 0 deletions lib/rollbar/item/frame.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# We want to use Gem.path
require 'rubygems'

module Rollbar
class Item
# Representation of the trace data per frame in the payload
class Frame
attr_reader :backtrace
attr_reader :frame
attr_reader :configuration

MAX_CONTEXT_LENGTH = 4

def initialize(backtrace, frame, options = {})
@backtrace = backtrace
@frame = frame
@configuration = options[:configuration]
end

def to_h
# parse the line
match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)

return unknown_frame unless match

filename = match[1]
lineno = match[2].to_i
frame_data = {
:filename => filename,
:lineno => lineno,
:method => match[3]
}

frame_data.merge(extra_frame_data(filename, lineno))
end

private

def unknown_frame
{ :filename => '<unknown>', :lineno => 0, :method => frame }
end

def extra_frame_data(filename, lineno)
file_lines = backtrace.get_file_lines(filename)

return {} if skip_extra_frame_data?(filename, file_lines)

{
:code => code_data(file_lines, lineno),
:context => context_data(file_lines, lineno)
}
end

def skip_extra_frame_data?(filename, file_lines)
config = configuration.send_extra_frame_data
missing_file_lines = !file_lines || file_lines.empty?

return false if !missing_file_lines && config == :all

missing_file_lines ||
config == :none ||
config == :app && outside_project?(filename)
end

def outside_project?(filename)
project_gem_paths = configuration.project_gem_paths
inside_project_gem_paths = project_gem_paths.any? do |path|
filename.start_with?(path)
end

# The file is inside the configuration.project_gem_paths,
return false if inside_project_gem_paths

root = configuration.root
inside_root = root && filename.start_with?(root.to_s)

# The file is outside the configuration.root
return true unless inside_root

# At this point, the file is inside the configuration.root.
# Since it's common to have gems installed in {root}/vendor/bundle,
# let's check it's in any of the Gem.path paths
Gem.path.any? { |path| filename.start_with?(path) }
end

def code_data(file_lines, lineno)
file_lines[lineno - 1]
end

def context_data(file_lines, lineno)
{
:pre => pre_data(file_lines, lineno),
:post => post_data(file_lines, lineno)
}
end

def post_data(file_lines, lineno)
from_line = lineno
number_of_lines = [from_line + MAX_CONTEXT_LENGTH, file_lines.size].min - from_line

file_lines[from_line, number_of_lines]
end

def pre_data(file_lines, lineno)
to_line = lineno - 2
from_line = [to_line - MAX_CONTEXT_LENGTH + 1, 0].max

file_lines[from_line, (to_line - from_line + 1)].select(&:present?)
end
end
end
end
26 changes: 26 additions & 0 deletions spec/rollbar/item/backtrace_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'spec_helper'
require 'tempfile'
require 'rollbar/item/backtrace'

describe Rollbar::Item::Backtrace do
describe '#get_file_lines' do
subject { described_class.new(exception) }

let(:exception) { Exception.new }
let(:file) { Tempfile.new('foo') }

before do
File.open(file.path, 'w') do |f|
f << "foo\nbar"
end
end

it 'returns the lines of the file' do
lines = subject.get_file_lines(file.path)

expect(lines.size).to be_eql(2)
expect(lines[0]).to be_eql('foo')
expect(lines[1]).to be_eql('bar')
end
end
end
Loading