class StackTracer

Public Class Methods

new(base_path) click to toggle source
# File lib/stack_tracer.rb, line 11
def initialize(base_path)
  @git_client = GitClient.new({"git_dir" => File.join(base_path, ".git")})
  @cache = ProjectCache.new(base_path)
end

Public Instance Methods

trace(error_id, deployed_commit_id=nil, last_deployed_commit_id=nil) click to toggle source
# File lib/stack_tracer.rb, line 16
def trace(error_id, deployed_commit_id=nil, last_deployed_commit_id=nil)
  error = @cache.read("errors")[error_id.to_s].deep_symbolize_keys
  commits = @cache.read("commits")
  deploys = @cache.read("deploys")

  deployed_commits = deploys
    .map { |deploy| commits[deploy["revision"]] }
    .reject(&:nil?)
    .sort_by { |commit| commit["date"] }

  deployed_commit_id ||= after_deployed_commit(error[:first_time], deployed_commits)
  last_deployed_commit_id ||= prior_deployed_commit(error[:first_time], deployed_commits)

  git_files = git_files(deployed_commit_id.to_s)
  stacktrace = stacktrace(error, git_files)
  trace_files = trace_files(stacktrace, git_files)
  trace_lines = trace_lines(stacktrace, trace_files, commits, last_deployed_commit_id.to_s)

  detail(error, trace_lines)
end

Private Instance Methods

after_deployed_commit(timestamp, commits) click to toggle source
# File lib/stack_tracer.rb, line 190
def after_deployed_commit(timestamp, commits)
  commit = commits
    .reverse()
    .detect { |commit| commit["date"] > timestamp }
  commit ||= commits.last
  commit["commit_id"]
end
describe_line(line, commits, cutoff_time) click to toggle source
# File lib/stack_tracer.rb, line 163
def describe_line(line, commits, cutoff_time)
  line = line.dup
  line[:updated_at] = DateTime.parse(commits[line[:commit]]["date"])
  line[:after_cutoff] = line[:updated_at] > cutoff_time
  line[:author] = commits[line[:commit]]["author"]
  line[:score] = Line.score(line)
  line
end
detail(error, trace_lines) click to toggle source
# File lib/stack_tracer.rb, line 97
def detail(error, trace_lines)
  functions = trace_lines.reduce({}) do |acc, trace|
    function_id = trace[:function].nil? ?
      trace[:file] :
      "#{trace[:file]}:#{trace[:function][:name]}"

    acc[function_id] ||= trace
    depth = trace[:line][:depth]
    trace[:line][:trace_title] = trace[:title]

    if acc[function_id].has_key?(:lines)
      acc[function_id][:lines] << trace[:line]
    else
      acc[function_id][:lines] = [trace[:line]]
      acc[function_id].delete(:line)
    end

    acc[function_id][:function_lines].each_with_index do |line, index|
      function_line_num = trace[:line_num] - trace[:function][:start]
      line[:depth] = depth if index == function_line_num
    end

    if not acc.has_key?(function_id)
      acc[function_id] = trace
    end
    acc
  end

  details = functions
    .reduce(detail_context()) do |acc, (file_path, function)|
      function[:function_lines].each do |line|
        score_line(acc[:experts], line[:author]["email"], line)
        if line[:after_cutoff]
          score_line(acc[:suspects], line[:author]["email"], line)
          score_line(acc[:suspect_commits], line[:commit], line)
        end
        line[:revisions].each do |revision|
          score_line(acc[:experts], revision[:author]["email"], line)
        end
      end
      acc
    end

  details.merge({
    message: error[:message],
    first_time: error[:first_time],
    last_time: error[:last_time],
    total_occurrences: error[:total_occurrences],
    functions: functions,
  })
end
detail_context() click to toggle source
# File lib/stack_tracer.rb, line 155
def detail_context()
  {
    experts: {},
    suspects: {},
    suspect_commits: {}
  }
end
function_lines(file, function) click to toggle source
# File lib/stack_tracer.rb, line 181
def function_lines(file, function)
  function.nil? ? [] : file[:lines][(function[:start] - 1)..function[:end]]
end
git_files(commit_id) click to toggle source
# File lib/stack_tracer.rb, line 39
def git_files(commit_id)
  @git_client
    .file_tree(commit_id)
    .reduce({}) do |acc, blob|
      blob = blob.split(" ")
      acc[blob.last] = blob[2]
      acc
    end
end
prior_deployed_commit(timestamp, commits) click to toggle source
# File lib/stack_tracer.rb, line 185
def prior_deployed_commit(timestamp, commits)
  commits
    .detect { |commit| commit["date"] < timestamp }["commit_id"]
end
score_line(hash, key, line) click to toggle source
# File lib/stack_tracer.rb, line 149
def score_line(hash, key, line)
  hash[key] ||= 0
  hash[key] += 1
  hash[key] += 2.0 / (line[:depth] + 1) if line.has_key?(:depth)
end
stacktrace(error, git_files) click to toggle source
# File lib/stack_tracer.rb, line 172
def stacktrace(error, git_files)
  error[:stack_trace]
    .reduce([]) do |acc, trace|
      trace[:file] = git_files.keys.detect { |app_file| trace[:file].include?(app_file) }
      acc << trace if not trace[:file].nil?
      acc
    end
end
trace_files(stacktrace, git_files) click to toggle source
# File lib/stack_tracer.rb, line 49
def trace_files(stacktrace, git_files)
  stacktrace
    .map { |trace_file| trace_file[:file] }
    .uniq
    .reduce({}) do |acc, file|
      acc[file] = @cache.read_object(git_files[file]).deep_symbolize_keys
      acc
    end
end
trace_lines(stacktrace, trace_files, commits, last_deployed_commit_id) click to toggle source
# File lib/stack_tracer.rb, line 59
def trace_lines(stacktrace, trace_files, commits, last_deployed_commit_id)
  last_deployed_commit = commits[last_deployed_commit_id]
  cutoff_time = DateTime.parse(last_deployed_commit["date"])

  stacktrace
    .each_with_index
    .map do |trace, depth|
      file = trace_files[trace[:file]]
      line_num = trace[:line] - 1
      line = describe_line(file[:lines][line_num], commits, cutoff_time)
      line[:depth] = depth

      function = file[:functions].detect do |function|
        line_num >= function[:start] && line_num <= function[:end]
      end

      function_lines = function_lines(file, function)
        .each_with_index
        .map do |line, index|
          line = describe_line(line, commits, cutoff_time)
          line[:revisions] = line[:revisions].map do |revision|
            revision[:author] = commits[revision[:commit]]["author"]
            revision
          end
          line
        end

      {
        file: trace[:file],
        line_num: trace[:line],
        line: line,
        title: "#{trace[:file]}:#{trace[:line]} - #{trace[:function]}",
        function: function,
        function_lines: function_lines
      }
    end
end