Module: RailsTracepointStack::Renderer::Tree

Defined in:
lib/rails_tracepoint_stack/renderer/tree.rb

Overview

Renders a session as an indented call tree.

The format is built for reading in a terminal or pasting into a prompt, so it favours short lines: paths are relative to the working directory, values are compact JSON, and a return sits one level under the call it belongs to.

Constant Summary collapse

INDENT =
"  ".freeze
SINGLETON_CLASS =
/\A#<Class:([A-Z][\w:]*)>\z/
TEMPLATE_FILE =
/\.(erb|haml|slim|builder|jbuilder|rabl)\z/i
TEMPLATE_LOCALS =

Everything a template receives besides the locals belongs to the rendering machinery, not to the developer.

"local_assigns".freeze

Class Method Summary collapse

Class Method Details

.call(session) ⇒ Object



14
15
16
17
18
19
20
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 14

def self.call(session)
  lines = session.traces.map { |record| line_for(record) }
  lines << truncation_notice if session.truncated?
  lines << Summary.line(session)
  lines << empty_notice(session) if session.traces.empty?
  lines.join("\n")
end

.call_line(record) ⇒ Object



46
47
48
49
50
51
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 46

def self.call_line(record)
  return template_line(record) if template?(record)

  "#{indent(record.depth)}#{qualified_name(record)} " \
    "(#{location(record)}) #{compact(record.params)}"
end

.compact(value) ⇒ Object



108
109
110
111
112
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 108

def self.compact(value)
  JSON.generate(value)
rescue SystemStackError, StandardError
  value.to_s
end

.empty_notice(session) ⇒ Object

An empty tree reads like a broken tool. It usually means the block ran entirely inside gems - a bare ActiveRecord query calls no method the developer wrote - so say which of the two happened.



25
26
27
28
29
30
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 25

def self.empty_notice(session)
  return "no code ran inside the block" if session.filtered_count.to_i.zero?

  "no app code ran: #{session.filtered_count} traces from gems, the framework " \
    "and Ruby itself were filtered out"
end

.indent(depth) ⇒ Object



94
95
96
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 94

def self.indent(depth)
  INDENT * depth
end

.line_for(record) ⇒ Object



32
33
34
35
36
37
38
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 32

def self.line_for(record)
  case record.kind
  when :call then call_line(record)
  when :return then return_line(record)
  when :raise then raise_line(record)
  end
end

.location(record) ⇒ Object



98
99
100
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 98

def self.location(record)
  "#{relative_path(record.file_path)}:#{record.line_number}"
end

.qualified_name(record) ⇒ Object

A class method is defined on the singleton class, which prints as #Class:Foo. Ruby writes that call as Foo.bar, so the tree does too. An anonymous singleton prints as an address and is left alone, since turning that into 0x00007f1234.render helps nobody.



57
58
59
60
61
62
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 57

def self.qualified_name(record)
  singleton = SINGLETON_CLASS.match(record.class_name.to_s)
  return "#{singleton[1]}.#{record.method_name}" if singleton

  "#{record.class_name}##{record.method_name}"
end

.raise_line(record) ⇒ Object



86
87
88
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 86

def self.raise_line(record)
  "#{indent(record.depth + 1)}!! #{record.exception_class}: #{record.exception_message}"
end

.relative_path(file_path) ⇒ Object



102
103
104
105
106
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 102

def self.relative_path(file_path)
  return file_path unless file_path.to_s.start_with?("#{Dir.pwd}/")

  file_path[(Dir.pwd.length + 1)..]
end

.return_line(record) ⇒ Object



82
83
84
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 82

def self.return_line(record)
  "#{indent(record.depth + 1)}-> #{compact(record.return_value)}"
end

.template?(record) ⇒ Boolean

A compiled template is a generated method on an anonymous class, named after a hash of the file. None of that is worth showing: the path is.

Returns:

  • (Boolean)


66
67
68
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 66

def self.template?(record)
  TEMPLATE_FILE.match?(record.file_path.to_s)
end

.template_line(record) ⇒ Object



70
71
72
73
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 70

def self.template_line(record)
  "#{indent(record.depth)}render #{relative_path(record.file_path)} " \
    "#{compact(template_locals(record))}"
end

.template_locals(record) ⇒ Object



75
76
77
78
79
80
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 75

def self.template_locals(record)
  params = record.params
  return {} unless params.is_a?(Hash)

  params.fetch(TEMPLATE_LOCALS, {})
end

.truncation_noticeObject



90
91
92
# File 'lib/rails_tracepoint_stack/renderer/tree.rb', line 90

def self.truncation_notice
  "... truncated: the limit was reached before the block finished"
end