rails_tracepoint_stack 0.3.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,30 +3,123 @@ require "rails_tracepoint_stack/logger"
3
3
  require "rails_tracepoint_stack/trace_filter"
4
4
  require "rails_tracepoint_stack/trace"
5
5
  require "rails_tracepoint_stack/log_formatter"
6
+ require "rails_tracepoint_stack/sink/log"
7
+ require "rails_tracepoint_stack/depth_tracker"
6
8
 
7
9
  module RailsTracepointStack
8
10
  class Tracer
9
11
  include RailsTracepointStack::TraceFilter
10
12
  extend Forwardable
11
-
13
+
14
+ DEFAULT_EVENTS = [:call].freeze
15
+
12
16
  def_delegators :@tracer, :enable, :disable
13
17
 
14
- def initialize
18
+ # How many traces the filters dropped. A capture that keeps nothing is
19
+ # ambiguous on its own: this tells the difference between code that never
20
+ # ran and code that ran entirely inside gems.
21
+ attr_reader :filtered_count
22
+
23
+ # A nil thread watches every thread in the process, which is what the
24
+ # global tracer wants. Passing one confines tracing to it, so a capture
25
+ # running inside a threaded server does not pick up other requests.
26
+ # own_block is the block the caller wrapped around the code under
27
+ # inspection. It is the caller's own code, so it is skipped before a depth
28
+ # is assigned - otherwise it would indent the whole trace by a level it
29
+ # never shows.
30
+ def initialize(
31
+ sink: RailsTracepointStack::Sink::Log.new,
32
+ events: DEFAULT_EVENTS,
33
+ thread: nil,
34
+ own_block: nil
35
+ )
36
+ @sink = sink
37
+ @events = events
38
+ @thread = thread
39
+ # Ruby has grown extra entries on source_location over time (columns, end
40
+ # positions), so only the file and the line are compared.
41
+ @own_block_location = own_block&.source_location&.first(2)
42
+ @filtered_count = 0
15
43
  generate_tracer
16
44
  end
17
45
 
18
46
  private
19
47
 
20
48
  def generate_tracer
21
- @tracer ||= TracePoint.new(:call) do |tracepoint|
49
+ @tracer ||= TracePoint.new(*@events) do |tracepoint|
50
+ next if out_of_scope_thread?
51
+
22
52
  trace = RailsTracepointStack::Trace.new(trace_point: tracepoint)
23
53
 
24
- next if ignore_trace?(trace: trace)
54
+ next if own_block?(trace)
55
+
56
+ if ignore_trace?(trace: trace)
57
+ @filtered_count += 1
58
+ next
59
+ end
60
+
61
+ trace.depth = depth_for(trace)
62
+ @sink.record(trace)
63
+ end
64
+ end
65
+
66
+ # Matching on location alone would also drop a block written on the same
67
+ # line as the capture call. The caller's block is the first to arrive from
68
+ # that location, so only that one is skipped, along with its return.
69
+ def own_block?(trace)
70
+ return false if @own_block_location.nil?
25
71
 
26
- # TODO: Use proper OO
27
- message = RailsTracepointStack::LogFormatter.message trace
28
- RailsTracepointStack::Logger.log message
72
+ case trace.kind
73
+ when :b_call then claim_own_block(trace)
74
+ when :b_return then release_own_block(trace)
75
+ else false
29
76
  end
30
77
  end
78
+
79
+ def claim_own_block(trace)
80
+ return false if @own_block_seen
81
+ return false unless [trace.file_path, trace.line_number] == @own_block_location
82
+
83
+ @own_block_seen = true
84
+ @own_block_position = raw_stack_position
85
+ true
86
+ end
87
+
88
+ def release_own_block(trace)
89
+ return false unless @own_block_position && raw_stack_position == @own_block_position
90
+
91
+ @own_block_position = nil
92
+ true
93
+ end
94
+
95
+ def out_of_scope_thread?
96
+ !@thread.nil? && Thread.current != @thread
97
+ end
98
+
99
+ # Only kept traces pay for reading the stack position, so the cost stays
100
+ # proportional to the app code being traced rather than to everything the
101
+ # VM runs underneath it.
102
+ def depth_for(trace)
103
+ tracker = depth_tracker
104
+ raw_position = raw_stack_position
105
+
106
+ case trace.kind
107
+ when :call, :b_call then tracker.enter(raw_position)
108
+ when :return, :b_return then tracker.leave(raw_position)
109
+ when :raise then tracker.raised(raw_position)
110
+ end
111
+ end
112
+
113
+ def raw_stack_position
114
+ caller_locations(1)&.length || 0
115
+ end
116
+
117
+ def depth_tracker
118
+ Thread.current[depth_tracker_key] ||= RailsTracepointStack::DepthTracker.new
119
+ end
120
+
121
+ def depth_tracker_key
122
+ @depth_tracker_key ||= :"rails_tracepoint_stack_depth_#{object_id}"
123
+ end
31
124
  end
32
125
  end
@@ -0,0 +1,85 @@
1
+ require "json"
2
+
3
+ module RailsTracepointStack
4
+ # Shrinks an already-serialized value so one fat argument (a big payload, a
5
+ # long SQL string, a loaded association) cannot dominate the output. Runs
6
+ # over the plain structures LogFormatter.safe_value produces, so it only
7
+ # ever sees strings, numbers, booleans, nil, arrays and hashes.
8
+ module Truncator
9
+ ELLIPSIS = "…".freeze
10
+
11
+ # Per-string and per-collection limits still leave room for one value to
12
+ # run to thousands of characters: twenty items each truncated to two
13
+ # hundred is still four thousand. This caps the value as a whole, and does
14
+ # it before the per-item truncation so the summary can report the real
15
+ # size rather than the already-shrunk one.
16
+ def self.bounded(value, limits)
17
+ capped = cap(value, limits)
18
+ return capped unless capped.equal?(value)
19
+
20
+ call(value, limits)
21
+ end
22
+
23
+ def self.cap(value, limits)
24
+ max = limits.max_value_length
25
+ return value if max.nil?
26
+ return value unless value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash)
27
+
28
+ size = JSON.generate(value).length
29
+ return value if size <= max
30
+
31
+ summarize(value, size)
32
+ rescue SystemStackError, StandardError
33
+ value
34
+ end
35
+
36
+ def self.summarize(value, size)
37
+ case value
38
+ when Array then "[#{value.size} items, #{size} chars — over max_value_length]"
39
+ when Hash then "{#{value.size} keys, #{size} chars — over max_value_length}"
40
+ else "[#{size} chars — over max_value_length]"
41
+ end
42
+ end
43
+
44
+ def self.call(value, limits)
45
+ case value
46
+ when String then truncate_string(value, limits)
47
+ when Array then truncate_array(value, limits)
48
+ when Hash then truncate_hash(value, limits)
49
+ else value
50
+ end
51
+ end
52
+
53
+ def self.truncate_string(value, limits)
54
+ max = limits.max_string_length
55
+ return value if max.nil? || value.length <= max
56
+
57
+ "#{value[0, max]}#{ELLIPSIS} (#{value.length} chars)"
58
+ end
59
+
60
+ def self.truncate_array(value, limits)
61
+ max = limits.max_collection_size
62
+ kept = (max.nil? || value.length <= max) ? value : value.first(max)
63
+ result = kept.map { |item| call(item, limits) }
64
+
65
+ return result if kept.length == value.length
66
+
67
+ result << "(+#{value.length - kept.length} more)"
68
+ end
69
+
70
+ def self.truncate_hash(value, limits)
71
+ max = limits.max_collection_size
72
+ dropped = (max.nil? || value.size <= max) ? 0 : value.size - max
73
+ kept = dropped.zero? ? value : value.first(max).to_h
74
+
75
+ result = kept.each_with_object({}) do |(key, item), memo|
76
+ memo[key] = call(item, limits)
77
+ end
78
+
79
+ return result if dropped.zero?
80
+
81
+ result[ELLIPSIS] = "(+#{dropped} more)"
82
+ result
83
+ end
84
+ end
85
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsTracepointStack
2
- VERSION = "0.3.5"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -1,6 +1,10 @@
1
1
  require 'rails_tracepoint_stack/configuration'
2
2
  require 'rails_tracepoint_stack/log_formatter'
3
3
  require 'rails_tracepoint_stack/tracer'
4
+ require 'rails_tracepoint_stack/limits'
5
+ require 'rails_tracepoint_stack/skill_installer'
6
+ require 'rails_tracepoint_stack/sink/collector'
7
+ require 'rails_tracepoint_stack/trace_session'
4
8
 
5
9
  $rails_tracer_rtps = nil
6
10
 
@@ -17,6 +21,43 @@ module RailsTracepointStack
17
21
  yield(configuration)
18
22
  end
19
23
 
24
+ CAPTURE_EVENTS = [:call, :return, :raise].freeze
25
+ BLOCK_EVENTS = [:b_call, :b_return].freeze
26
+
27
+ # Traces a block and hands back everything that happened inside it, instead
28
+ # of writing to a log. Meant to be run as a one-off: capture, read, done.
29
+ # Blocks are off by default: a block written inside a loop fires once per
30
+ # element, so watching them costs more traces than watching methods does.
31
+ # Turn them on to see scopes, lambdas and anything else whose logic lives in
32
+ # a block rather than a method.
33
+ def self.capture(threads: :current, blocks: false, **limit_options, &traced)
34
+ raise ArgumentError, "Block not given to #capture" unless traced
35
+
36
+ collector = RailsTracepointStack::Sink::Collector.new(
37
+ limits: RailsTracepointStack::Limits.new(**limit_options)
38
+ )
39
+ session = collector.session
40
+ tracer = RailsTracepointStack::Tracer.new(
41
+ sink: collector,
42
+ events: blocks ? CAPTURE_EVENTS + BLOCK_EVENTS : CAPTURE_EVENTS,
43
+ thread: (threads == :all) ? nil : Thread.current,
44
+ own_block: traced
45
+ )
46
+
47
+ tracer.enable
48
+ begin
49
+ session.result = traced.call(session)
50
+ rescue Exception => error
51
+ session.error = error
52
+ raise
53
+ ensure
54
+ tracer.disable
55
+ session.filtered_count = tracer.filtered_count
56
+ end
57
+
58
+ session
59
+ end
60
+
20
61
  def self.enable_trace
21
62
  raise ArgumentError, "Block not given to #enable_trace" unless block_given?
22
63
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_tracepoint_stack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Daniel Pohlod
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-17 00:00:00.000000000 Z
11
+ date: 2026-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -70,33 +70,50 @@ dependencies:
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
72
  version: 1.39.1
73
- description: A formatted output of all methods called in your rails application of
74
- code created by the developer, with the complete path to the class/module, including
75
- passed params.
73
+ description: Traces a block of your Rails app and returns the call tree of your own
74
+ code - arguments, return values, raised exceptions and call depth - with gems, the
75
+ framework and stdlib filtered out. Bounded output meant to be read directly, by
76
+ a developer or by an AI coding agent debugging the app.
76
77
  email: carlospohlod@gmail.com
77
78
  executables: []
78
79
  extensions: []
79
80
  extra_rdoc_files: []
80
81
  files:
82
+ - README.md
83
+ - changelog.md
84
+ - lib/generators/rails_tracepoint_stack/install/install_generator.rb
81
85
  - lib/rails_tracepoint_stack.rb
82
86
  - lib/rails_tracepoint_stack/configuration.rb
87
+ - lib/rails_tracepoint_stack/depth_tracker.rb
83
88
  - lib/rails_tracepoint_stack/filter/custom_trace_selector_filter.rb
84
89
  - lib/rails_tracepoint_stack/filter/gem_path.rb
85
90
  - lib/rails_tracepoint_stack/filter/rb_config.rb
86
91
  - lib/rails_tracepoint_stack/filter/trace_from_dependencies_filter.rb
87
92
  - lib/rails_tracepoint_stack/filter/trace_from_ruby_code_filter.rb
88
93
  - lib/rails_tracepoint_stack/filter/trace_to_ignore_filter.rb
94
+ - lib/rails_tracepoint_stack/limits.rb
89
95
  - lib/rails_tracepoint_stack/log_formatter.rb
90
96
  - lib/rails_tracepoint_stack/logger.rb
97
+ - lib/rails_tracepoint_stack/renderer/summary.rb
98
+ - lib/rails_tracepoint_stack/renderer/tree.rb
99
+ - lib/rails_tracepoint_stack/sink/collector.rb
100
+ - lib/rails_tracepoint_stack/sink/log.rb
101
+ - lib/rails_tracepoint_stack/skill_installer.rb
102
+ - lib/rails_tracepoint_stack/templates/skill.md
91
103
  - lib/rails_tracepoint_stack/trace.rb
92
104
  - lib/rails_tracepoint_stack/trace_filter.rb
105
+ - lib/rails_tracepoint_stack/trace_record.rb
106
+ - lib/rails_tracepoint_stack/trace_session.rb
93
107
  - lib/rails_tracepoint_stack/tracer.rb
108
+ - lib/rails_tracepoint_stack/truncator.rb
94
109
  - lib/rails_tracepoint_stack/version.rb
95
- homepage: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/
110
+ homepage: https://carlosdanielpohlod.github.io/rails_tracepoint_stack/
96
111
  licenses:
97
112
  - MIT
98
113
  metadata:
99
- documentation_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/
114
+ source_code_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack
115
+ documentation_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack#readme
116
+ bug_tracker_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/issues
100
117
  changelog_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/blob/main/changelog.md
101
118
  post_install_message:
102
119
  rdoc_options: []
@@ -116,5 +133,6 @@ requirements: []
116
133
  rubygems_version: 3.4.19
117
134
  signing_key:
118
135
  specification_version: 4
119
- summary: Get a complete stack trace for your code on a Rails application.
136
+ summary: 'Runtime call tree for a Rails app: which methods ran, with what params,
137
+ and what they returned.'
120
138
  test_files: []