rails_tracepoint_stack 0.3.5 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f67c982828a7ad060513cbb50daf9f88399229a534a43cf12a674571b6ae11ae
4
- data.tar.gz: fd80c178bf9de58408e769742d7640b0ccdbcc99a4722ec9ccbd6e68d115ff51
3
+ metadata.gz: 612ce09f08d0dfebf77cfddc8dfbe9712598aaf324579f688bac6148354c92fb
4
+ data.tar.gz: aec94f6a1a148f16199b6835932a5b1fe91ff458a2915fab60efe937244b7cac
5
5
  SHA512:
6
- metadata.gz: 57b7125a7a242bf59f15096fffeef7de3c8a7543162fa7851566a07256d2ed790d39ff9c9dfcd1f3c3f8ca54714b797ed5f5015f2eb0d6f0adbe57e3ad582412
7
- data.tar.gz: cd33cf3e53fe35fe62ba3867cfe8ff56047cceae59e2139c55898fcb0ed0e44d8dccf57a0b66954f1543797c1cb1070b6c68f479f76ce80f0c15282518c50110
6
+ metadata.gz: de2e858cfad1406ee8becdb5d3be65b956286c802a1f95023d8698eaa80500569ee603be1b28e06404845a41eebb25e364ea936cdd886fcdab02b14113caaf10
7
+ data.tar.gz: 17b4beea42cb83b2058b32652acf0ddeec66a1b2fabe32e61a07fcf446738fd4c92b238dd6ce603d5426d68af4e1bbe397e23c7afd7c739c4385ed3d8cf61177
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # rails_tracepoint_stack
2
+
3
+ **[carlosdanielpohlod.github.io/rails_tracepoint_stack](https://carlosdanielpohlod.github.io/rails_tracepoint_stack/)**
4
+
5
+ See what your Rails code actually did at runtime: which of *your* methods ran,
6
+ what arguments they got, what each one returned, and where an exception was
7
+ first raised — as a call tree, with gems, the framework and stdlib filtered out.
8
+
9
+ ```ruby
10
+ session = RailsTracepointStack.capture(max_depth: 4) do
11
+ Order.find(42).recalculate!
12
+ end
13
+
14
+ puts session.to_tree
15
+ ```
16
+
17
+ ```
18
+ Order#recalculate! (app/models/order.rb:88) {}
19
+ Order#apply_discount (app/models/order.rb:102) {"total":200.0}
20
+ Discount#rate_for (app/models/discount.rb:12) {"order":"#<Order id: 42>"}
21
+ -> null
22
+ -> 200.0
23
+ -> 200.0
24
+ 3 calls, 3 returns, 0 raises, 2 classes
25
+ ```
26
+
27
+ `rate_for` returned nil. No `puts`, no log file, no restart.
28
+
29
+ A whole Rails request comes out about this long, because everything the
30
+ framework runs underneath is filtered away:
31
+
32
+ ```
33
+ RealEstateAgenciesController#index (app/controllers/real_estate_agencies_controller.rb:14) {}
34
+ IpLocation.guess_state_uf_and_city_name (app/services/ip_location.rb:15) {"ip":"127.0.0.1"}
35
+ -> [null,null]
36
+ -> null
37
+ render app/views/real_estate_agencies/index.html.erb {}
38
+ -> "<!-- BEGIN app/views/real_estate_agencies/index.html.erb\n-->… (2507 chars)"
39
+ 4 calls, 4 returns, 0 raises, 3 classes
40
+ ```
41
+
42
+ That is 4 kept traces out of 13869 the tracer saw.
43
+
44
+ ## Install
45
+
46
+ ```ruby
47
+ # Gemfile
48
+ gem "rails_tracepoint_stack"
49
+ ```
50
+
51
+ ## Debugging with an AI agent
52
+
53
+ Agents reach for `puts` and log lines because they do not know this exists.
54
+ Install the packaged skill into your app and yours will use the gem instead:
55
+
56
+ ```bash
57
+ bin/rails generate rails_tracepoint_stack:install
58
+ ```
59
+
60
+ That writes `.claude/skills/debug-with-tracepoint/SKILL.md`, which tells the
61
+ agent when tracing beats reading code, how to keep the output inside its
62
+ context window, and how to read the tree. Pass `--force` to overwrite an
63
+ existing copy.
64
+
65
+ ## Capturing
66
+
67
+ `capture` traces the block, returns a session and re-raises anything the block
68
+ raised. It watches only the calling thread, so it stays clean under Puma.
69
+
70
+ ```ruby
71
+ session = RailsTracepointStack.capture { Order.find(42).recalculate! }
72
+
73
+ session.to_tree # the indented call tree above
74
+ session.as_json # the same data, structured, one entry per trace
75
+ session.summary # {calls:, returns:, raises:, classes:, truncated:}
76
+ session.result # what the block returned
77
+ session.error # the exception that escaped, if any
78
+ session.traces # the raw TraceRecord list
79
+ ```
80
+
81
+ To keep the traces when the block blows up, take the session from the block
82
+ argument:
83
+
84
+ ```ruby
85
+ session = nil
86
+ begin
87
+ RailsTracepointStack.capture { |s| session = s; thing_that_blows_up }
88
+ rescue => error
89
+ puts session.to_tree
90
+ end
91
+ ```
92
+
93
+ ### Keeping the output small
94
+
95
+ A real request produces tens of thousands of traces, so captures are bounded.
96
+
97
+ | Option | Default | What it does |
98
+ |---|---|---|
99
+ | `max_depth` | none | Drops traces nested deeper than this |
100
+ | `max_traces` | 5000 | Stops collecting; `session.truncated?` becomes true |
101
+ | `max_string_length` | 200 | Shortens long strings |
102
+ | `max_collection_size` | 20 | Shortens long arrays and hashes |
103
+ | `capture_params` | `true` | Set false to show only the flow |
104
+ | `capture_return` | `true` | Set false to show only the calls |
105
+ | `threads` | `:current` | `:all` also records background threads |
106
+
107
+ ```ruby
108
+ RailsTracepointStack.capture(max_depth: 3, capture_return: false) { ... }
109
+ ```
110
+
111
+ ## Tracing the whole process
112
+
113
+ For code you cannot wrap in a block — boot, a rake task, a request handled by a
114
+ running server — enable the tracer globally instead. Output goes to
115
+ `log/rails_tracepoint_stack.log`.
116
+
117
+ ```bash
118
+ RAILS_TRACEPOINT_STACK_ENABLED=true bin/rails server
119
+ ```
120
+
121
+ ```
122
+ called: Bar#perform in /path/to/app/services/bar.rb:12 with params: {}
123
+ ```
124
+
125
+ `RailsTracepointStack.enable_trace { ... }` does the same for one block, also
126
+ writing to the log rather than returning a session.
127
+
128
+ ## Configuration
129
+
130
+ ```ruby
131
+ # config/initializers/rails_tracepoint_stack.rb
132
+
133
+ RailsTracepointStack.configure do |config|
134
+ config.file_path_to_filter_patterns << %r{app/services/}
135
+ config.ignore_patterns << %r{app/models/concerns/}
136
+ config.log_format = :json
137
+ config.log_external_sources = false
138
+ config.logger = Rails.logger
139
+ end
140
+ ```
141
+
142
+ | Configuration | Description |
143
+ |---|---|
144
+ | `file_path_to_filter_patterns` | Trace **only** files whose path matches one of these patterns |
145
+ | `ignore_patterns` | Skip traces whose file path matches one of these patterns |
146
+ | `log_format` | `:text` (default) or `:json`. Applies to the log, not to `capture` |
147
+ | `log_external_sources` | Include gems, bundler and stdlib. Default `false` |
148
+ | `logger` | Your own logger. Defaults to `log/rails_tracepoint_stack.log` |
149
+
150
+ A method missing from a trace is usually one the filters dropped as external —
151
+ add its path to `file_path_to_filter_patterns` to force it in.
152
+
153
+ ## Worth knowing
154
+
155
+ - TracePoint makes the traced code noticeably slower — around 8x on a real
156
+ Rails request. Fine for an investigation, not for something left running in
157
+ production.
158
+ - Ruby >= 3.0.
159
+ - A `-> null` line directly under a `!!` line is the frame unwinding, not a
160
+ method that returned nil.
161
+ - An empty tree says `no app code ran` along with how much was filtered. That
162
+ usually means the block ran entirely inside gems — `Order.where(...).map(&:name)`
163
+ calls no method you wrote, since `name` is generated by ActiveRecord.
164
+
165
+ ## Links
166
+
167
+ - [Website](https://carlosdanielpohlod.github.io/rails_tracepoint_stack/) — the
168
+ same story with real captured output, including a whole Rails request
169
+ - [RubyGems](https://rubygems.org/gems/rails_tracepoint_stack)
170
+ - [Changelog](changelog.md)
171
+
172
+ ## License
173
+
174
+ MIT
data/changelog.md ADDED
@@ -0,0 +1,132 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ **Features**
6
+
7
+ - `RailsTracepointStack.capture` traces a block and returns the traces in
8
+ memory, with no env var, log file or restart involved
9
+ - Traces now carry return values and raised exceptions, not just calls and params
10
+ - Each trace knows its call depth, so a session renders as a call tree
11
+ - `session.to_tree`, `session.as_json` and `session.summary` render a capture
12
+ - Captures are bounded by `max_depth`, `max_traces`, `max_string_length` and
13
+ `max_collection_size`, and report `truncated?` when a limit cut them short
14
+ - Captures watch only the calling thread by default; `threads: :all` opts out
15
+ - `rails g rails_tracepoint_stack:install` writes an agent skill into the app,
16
+ so AI coding agents know when and how to trace it
17
+
18
+ **BugFix**
19
+
20
+ - Params reported every local the method body declared, not just the arguments
21
+ it received, so unassigned locals showed up as arguments passed as nil.
22
+ `TracePoint#parameters` now decides what is read from the binding
23
+ - `GemPath` referenced `Bundler` without requiring it, raising `NameError` on
24
+ the first trace outside a bundle
25
+ - A raise coming out of a C method landed one level too deep in the tree
26
+
27
+ **Changes**
28
+
29
+ - The tracer writes to a sink; the default one keeps the previous logging
30
+ behaviour, so global tracing is unchanged
31
+ - Class methods render as `Foo.bar` rather than `#<Class:Foo>#bar`
32
+ - Compiled templates render as `render app/views/…` with their locals, instead
33
+ of the generated method name and the rendering internals
34
+ - An empty capture reports how many traces the filters dropped, so "nothing
35
+ ran" is distinguishable from "nothing of yours ran"
36
+ - Serialized string values are frozen copies, so a snapshot no longer follows
37
+ later mutations of the traced object
38
+
39
+ ## 0.3.5
40
+
41
+ **BugFix**
42
+
43
+ - Sanitize traced params before formatting so JSON logging does not overflow on recursive or problematic objects [Issue #37](https://github.com/carlosdanielpohlod/rails_tracepoint_stack/issues/37)
44
+ - Stabilize text log param rendering across Ruby versions
45
+
46
+ ## 0.3.4
47
+
48
+ **Changes**
49
+
50
+ - Fix flaky tests by @danielmbrasil [PR #27](https://github.com/carlosdanielpohlod/rails_tracepoint_stack/pull/27)
51
+
52
+ - Huge refactor of filters class, separating in modules by filter type [PR #18](https://github.com/carlosdanielpohlod/rails_tracepoint_stack/pull/18)
53
+
54
+ - Some other code refactors
55
+
56
+ ## 0.3.3
57
+
58
+ **Changes**
59
+
60
+ - Add autoload of all lib files on test_helper
61
+ - Fix some tests
62
+
63
+ **BugFix**
64
+
65
+ - Fixed the Configuration module not loading the default value for the configuration attributes
66
+
67
+ ## 0.3.1
68
+
69
+ **Changes:**
70
+
71
+ - Add the ability to include the external sources to the log using `config.log_external_sources = true`
72
+
73
+ ## 0.3.0
74
+
75
+ **Changes:**
76
+
77
+ - Refactor classes, formatting a trace using a value object class `RailsTracepointStack::Trace`
78
+ - include configuration `log_format` option, allowing choose an output as `text` or `json`
79
+ - Include configuration `file_path_to_filter_patterns`, allowing filter traces only when the origin file path matches a pattern
80
+ - Improve test coverage
81
+
82
+ ## 0.2.1
83
+
84
+ **Changes:**
85
+
86
+ - Update the ENV enable to be more semantic
87
+ - Add the VERSION constant module
88
+ - Sorted the files inside of gemspec
89
+ - Fix the depencies on the gemspec
90
+ - Add the "log_format" configuration support for text and json formats
91
+
92
+ ## 0.2.0
93
+
94
+ **Changes:**
95
+
96
+ - Refactor by separating Logger and Filter into their own classes.
97
+
98
+ - Introduce `RailsTracepointStack.configure`, which allows ignoring traces with a custom pattern and customizing the logs output. Example:
99
+
100
+ ```ruby
101
+ RailsTracepointStack.configure do |config|
102
+ config.ignore_patterns << /services\/foo.rb/
103
+ config.logger = YourLogger
104
+ end
105
+ ```
106
+
107
+ The default log destination is a file located on `log/rails_tracepoint_stack.log`
108
+
109
+ - Add The possibility of enable the tracer locally, by calling:
110
+
111
+ ```ruby
112
+ class Foo
113
+ def bar
114
+ RailsTracepointStack.enable_trace do
115
+ p "your code"
116
+ end
117
+ end
118
+ end
119
+ ```
120
+
121
+ - Add Rspec and Rake development dependencies, and add partial test coverage.
122
+
123
+ ## 0.1.4
124
+
125
+ **Changes:**
126
+
127
+ - Ignore logs containing `gems/bundler`.
128
+ - Require ruby >= 3.0.
129
+
130
+ **Breaking Changes:**
131
+
132
+ - To enable logs catch, it is necessary to set `RAILS_TRACEPOINT_STACK` as `true`.
@@ -0,0 +1,29 @@
1
+ require "rails/generators/base"
2
+ require "rails_tracepoint_stack/skill_installer"
3
+
4
+ module RailsTracepointStack
5
+ module Generators
6
+ # Thin wrapper over SkillInstaller so the behaviour stays testable without
7
+ # booting Rails.
8
+ class InstallGenerator < Rails::Generators::Base
9
+ desc "Installs the debug-with-tracepoint skill so agents working in " \
10
+ "this app know how to trace it"
11
+
12
+ def install_agent_skill
13
+ installer = RailsTracepointStack::SkillInstaller.new(
14
+ destination: destination_root,
15
+ force: options[:force]
16
+ )
17
+ written = installer.install
18
+
19
+ if written
20
+ say_status :create, relative_to_original_destination_root(written), :green
21
+ else
22
+ say_status :skip,
23
+ "#{relative_to_original_destination_root(installer.target_path)} already exists (--force to replace)",
24
+ :yellow
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ module RailsTracepointStack
2
+ # Turns raw stack positions into app-level nesting.
3
+ #
4
+ # Counting :call and :return events would not work here, for two reasons.
5
+ # Most frames between two traced methods belong to gems and get filtered out,
6
+ # so an event counter would indent app code by the depth of the framework
7
+ # underneath it. And the tracer does not always watch :return at all - the
8
+ # global tracer only subscribes to :call - so nothing would ever pop.
9
+ #
10
+ # Reading the real stack position of each kept trace avoids both. A frame
11
+ # recorded at an equal or deeper position has necessarily finished, whether
12
+ # it returned, raised, or was abandoned, so it gets dropped on the next
13
+ # event rather than waiting for one that may never arrive.
14
+ class DepthTracker
15
+ def initialize
16
+ @stack = []
17
+ end
18
+
19
+ def enter(raw_position)
20
+ drop_finished_frames(raw_position, inclusive: true)
21
+ depth = @stack.size
22
+ @stack.push(raw_position)
23
+ depth
24
+ end
25
+
26
+ def leave(raw_position)
27
+ drop_finished_frames(raw_position, inclusive: false)
28
+ return @stack.size unless @stack.last == raw_position
29
+
30
+ @stack.pop
31
+ @stack.size
32
+ end
33
+
34
+ # A raise belongs to the innermost frame being tracked. It may report a
35
+ # position deeper than that frame when the exception comes out of a C
36
+ # method the app called, such as an arithmetic coercion, and there is no
37
+ # tracked frame down there to attribute it to.
38
+ def raised(raw_position)
39
+ drop_finished_frames(raw_position, inclusive: false)
40
+
41
+ [@stack.size - 1, 0].max
42
+ end
43
+
44
+ private
45
+
46
+ def drop_finished_frames(raw_position, inclusive:)
47
+ if inclusive
48
+ @stack.pop while @stack.last && @stack.last >= raw_position
49
+ else
50
+ @stack.pop while @stack.last && @stack.last > raw_position
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,8 +1,27 @@
1
1
  module RailsTracepointStack
2
2
  module Filter
3
3
  class GemPath
4
+ # Bundler gives the tightest answer, but it is not always there: a plain
5
+ # `ruby -e` or an irb session against an installed gem has no bundle, and
6
+ # asking Bundler then raises instead of filtering. Falling back to every
7
+ # installed gem is wider than needed and still filters out the code the
8
+ # developer did not write.
4
9
  def self.full_gem_path
5
- @full_gem_path ||= Bundler.load.specs.map(&:full_gem_path)
10
+ @full_gem_path ||= bundled_gem_paths || gem_install_roots
11
+ end
12
+
13
+ def self.bundled_gem_paths
14
+ return nil unless defined?(::Bundler)
15
+
16
+ ::Bundler.load.specs.map(&:full_gem_path)
17
+ rescue
18
+ nil
19
+ end
20
+
21
+ # Without a bundle there is no resolved list to ask for, but everything
22
+ # installed lives under these roots, which is all the filter needs.
23
+ def self.gem_install_roots
24
+ Gem.path.map { |dir| File.join(dir, "gems") }
6
25
  end
7
26
  end
8
27
  end
@@ -0,0 +1,41 @@
1
+ module RailsTracepointStack
2
+ # How much a capture is allowed to keep. The defaults aim at an output a
3
+ # person or an agent can read in one go rather than at completeness: a
4
+ # single Rails request can easily produce tens of thousands of traces.
5
+ class Limits
6
+ DEFAULT_MAX_TRACES = 5_000
7
+ DEFAULT_MAX_STRING_LENGTH = 200
8
+ DEFAULT_MAX_COLLECTION_SIZE = 20
9
+
10
+ attr_reader :max_depth,
11
+ :max_traces,
12
+ :max_string_length,
13
+ :max_collection_size,
14
+ :capture_params,
15
+ :capture_return
16
+
17
+ def initialize(
18
+ max_depth: nil,
19
+ max_traces: DEFAULT_MAX_TRACES,
20
+ max_string_length: DEFAULT_MAX_STRING_LENGTH,
21
+ max_collection_size: DEFAULT_MAX_COLLECTION_SIZE,
22
+ capture_params: true,
23
+ capture_return: true
24
+ )
25
+ @max_depth = max_depth
26
+ @max_traces = max_traces
27
+ @max_string_length = max_string_length
28
+ @max_collection_size = max_collection_size
29
+ @capture_params = capture_params
30
+ @capture_return = capture_return
31
+ end
32
+
33
+ def too_deep?(depth)
34
+ !max_depth.nil? && !depth.nil? && depth > max_depth
35
+ end
36
+
37
+ def room_for?(count)
38
+ max_traces.nil? || count < max_traces
39
+ end
40
+ end
41
+ end
@@ -35,8 +35,12 @@ module RailsTracepointStack
35
35
 
36
36
  def self.safe_value(value, ancestry = {})
37
37
  case value
38
- when nil, true, false, Numeric, String
38
+ when nil, true, false, Numeric
39
39
  value
40
+ when String
41
+ # Callers may keep the result around after the traced code moved on,
42
+ # so a live reference to a mutable string would drift.
43
+ value.frozen? ? value : value.dup.freeze
40
44
  when Symbol
41
45
  value.to_s
42
46
  when Array
@@ -0,0 +1,37 @@
1
+ module RailsTracepointStack
2
+ module Renderer
3
+ # Counts what a session holds, so a reader can tell at a glance whether
4
+ # the capture saw what they expected before reading the tree itself.
5
+ module Summary
6
+ def self.call(session)
7
+ {
8
+ calls: count(session, :call),
9
+ returns: count(session, :return),
10
+ raises: count(session, :raise),
11
+ classes: session.traces.map(&:class_name).uniq.size,
12
+ filtered: session.filtered_count,
13
+ truncated: session.truncated?
14
+ }
15
+ end
16
+
17
+ def self.line(session)
18
+ counts = call(session)
19
+
20
+ [
21
+ pluralize(counts[:calls], "call"),
22
+ pluralize(counts[:returns], "return"),
23
+ pluralize(counts[:raises], "raise"),
24
+ pluralize(counts[:classes], "class", "classes")
25
+ ].join(", ")
26
+ end
27
+
28
+ def self.count(session, kind)
29
+ session.traces.count { |record| record.kind == kind }
30
+ end
31
+
32
+ def self.pluralize(count, singular, plural = "#{singular}s")
33
+ "#{count} #{(count == 1) ? singular : plural}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,115 @@
1
+ require "json"
2
+
3
+ module RailsTracepointStack
4
+ module Renderer
5
+ # Renders a session as an indented call tree.
6
+ #
7
+ # The format is built for reading in a terminal or pasting into a prompt,
8
+ # so it favours short lines: paths are relative to the working directory,
9
+ # values are compact JSON, and a return sits one level under the call it
10
+ # belongs to.
11
+ module Tree
12
+ INDENT = " ".freeze
13
+
14
+ def self.call(session)
15
+ lines = session.traces.map { |record| line_for(record) }
16
+ lines << truncation_notice if session.truncated?
17
+ lines << Summary.line(session)
18
+ lines << empty_notice(session) if session.traces.empty?
19
+ lines.join("\n")
20
+ end
21
+
22
+ # An empty tree reads like a broken tool. It usually means the block ran
23
+ # entirely inside gems - a bare ActiveRecord query calls no method the
24
+ # developer wrote - so say which of the two happened.
25
+ def self.empty_notice(session)
26
+ return "no code ran inside the block" if session.filtered_count.to_i.zero?
27
+
28
+ "no app code ran: #{session.filtered_count} traces from gems, the framework " \
29
+ "and Ruby itself were filtered out"
30
+ end
31
+
32
+ def self.line_for(record)
33
+ case record.kind
34
+ when :call then call_line(record)
35
+ when :return then return_line(record)
36
+ when :raise then raise_line(record)
37
+ end
38
+ end
39
+
40
+ SINGLETON_CLASS = /\A#<Class:([A-Z][\w:]*)>\z/
41
+ TEMPLATE_FILE = /\.(erb|haml|slim|builder|jbuilder|rabl)\z/i
42
+ # Everything a template receives besides the locals belongs to the
43
+ # rendering machinery, not to the developer.
44
+ TEMPLATE_LOCALS = "local_assigns".freeze
45
+
46
+ def self.call_line(record)
47
+ return template_line(record) if template?(record)
48
+
49
+ "#{indent(record.depth)}#{qualified_name(record)} " \
50
+ "(#{location(record)}) #{compact(record.params)}"
51
+ end
52
+
53
+ # A class method is defined on the singleton class, which prints as
54
+ # #<Class:Foo>. Ruby writes that call as Foo.bar, so the tree does too.
55
+ # An anonymous singleton prints as an address and is left alone, since
56
+ # turning that into `0x00007f1234.render` helps nobody.
57
+ def self.qualified_name(record)
58
+ singleton = SINGLETON_CLASS.match(record.class_name.to_s)
59
+ return "#{singleton[1]}.#{record.method_name}" if singleton
60
+
61
+ "#{record.class_name}##{record.method_name}"
62
+ end
63
+
64
+ # A compiled template is a generated method on an anonymous class, named
65
+ # after a hash of the file. None of that is worth showing: the path is.
66
+ def self.template?(record)
67
+ TEMPLATE_FILE.match?(record.file_path.to_s)
68
+ end
69
+
70
+ def self.template_line(record)
71
+ "#{indent(record.depth)}render #{relative_path(record.file_path)} " \
72
+ "#{compact(template_locals(record))}"
73
+ end
74
+
75
+ def self.template_locals(record)
76
+ params = record.params
77
+ return {} unless params.is_a?(Hash)
78
+
79
+ params.fetch(TEMPLATE_LOCALS, {})
80
+ end
81
+
82
+ def self.return_line(record)
83
+ "#{indent(record.depth + 1)}-> #{compact(record.return_value)}"
84
+ end
85
+
86
+ def self.raise_line(record)
87
+ "#{indent(record.depth + 1)}!! #{record.exception_class}: #{record.exception_message}"
88
+ end
89
+
90
+ def self.truncation_notice
91
+ "... truncated: the limit was reached before the block finished"
92
+ end
93
+
94
+ def self.indent(depth)
95
+ INDENT * depth
96
+ end
97
+
98
+ def self.location(record)
99
+ "#{relative_path(record.file_path)}:#{record.line_number}"
100
+ end
101
+
102
+ def self.relative_path(file_path)
103
+ return file_path unless file_path.to_s.start_with?("#{Dir.pwd}/")
104
+
105
+ file_path[(Dir.pwd.length + 1)..]
106
+ end
107
+
108
+ def self.compact(value)
109
+ JSON.generate(value)
110
+ rescue SystemStackError, StandardError
111
+ value.to_s
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,69 @@
1
+ require "rails_tracepoint_stack/limits"
2
+ require "rails_tracepoint_stack/log_formatter"
3
+ require "rails_tracepoint_stack/trace_record"
4
+ require "rails_tracepoint_stack/trace_session"
5
+ require "rails_tracepoint_stack/truncator"
6
+
7
+ module RailsTracepointStack
8
+ module Sink
9
+ # Turns live traces into immutable records held in memory, so a caller can
10
+ # inspect them once the traced block is done.
11
+ class Collector
12
+ attr_reader :session, :limits
13
+
14
+ def initialize(session: RailsTracepointStack::TraceSession.new, limits: RailsTracepointStack::Limits.new)
15
+ @session = session
16
+ @limits = limits
17
+ end
18
+
19
+ def record(trace)
20
+ return if limits.too_deep?(trace.depth)
21
+
22
+ unless limits.room_for?(session.traces.size)
23
+ session.truncated = true
24
+ return
25
+ end
26
+
27
+ session.add(build_record(trace))
28
+ end
29
+
30
+ private
31
+
32
+ def build_record(trace)
33
+ exception = trace.exception
34
+
35
+ RailsTracepointStack::TraceRecord.new(
36
+ kind: trace.kind,
37
+ class_name: RailsTracepointStack::LogFormatter.stringify(trace.class_name),
38
+ method_name: trace.method_name,
39
+ file_path: trace.file_path,
40
+ line_number: trace.line_number,
41
+ params: params_for(trace),
42
+ return_value: return_value_for(trace),
43
+ exception_class: exception && exception.class.to_s,
44
+ exception_message: exception && RailsTracepointStack::LogFormatter.stringify(exception.message),
45
+ depth: trace.depth || 0
46
+ )
47
+ end
48
+
49
+ def params_for(trace)
50
+ return {} unless limits.capture_params
51
+
52
+ snapshot(trace.params)
53
+ end
54
+
55
+ def return_value_for(trace)
56
+ return nil unless limits.capture_return
57
+
58
+ snapshot(trace.return_value)
59
+ end
60
+
61
+ def snapshot(value)
62
+ RailsTracepointStack::Truncator.call(
63
+ RailsTracepointStack::LogFormatter.safe_value(value),
64
+ limits
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,17 @@
1
+ require "rails_tracepoint_stack/logger"
2
+ require "rails_tracepoint_stack/log_formatter"
3
+
4
+ module RailsTracepointStack
5
+ module Sink
6
+ # Formats each trace and writes it out, which is what the gem has always
7
+ # done. Kept as the default so enabling the tracer globally behaves the
8
+ # same as before sinks existed.
9
+ class Log
10
+ def record(trace)
11
+ RailsTracepointStack::Logger.log(
12
+ RailsTracepointStack::LogFormatter.message(trace)
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ require "fileutils"
2
+
3
+ module RailsTracepointStack
4
+ # Copies the packaged agent skill into the host app.
5
+ #
6
+ # A skill living in this gem's repository only helps someone working on the
7
+ # gem. Agents look for skills inside the project they are working on, so the
8
+ # file has to land there for the gem to ever get picked up on its own.
9
+ class SkillInstaller
10
+ SKILL_PATH = ".claude/skills/debug-with-tracepoint/SKILL.md".freeze
11
+ TEMPLATE_PATH = File.expand_path("templates/skill.md", __dir__).freeze
12
+
13
+ attr_reader :destination, :force
14
+
15
+ def initialize(destination: Dir.pwd, force: false)
16
+ @destination = destination
17
+ @force = force
18
+ end
19
+
20
+ # Returns the path written, or nil when a skill was already there.
21
+ def install
22
+ return nil if File.exist?(target_path) && !force
23
+
24
+ FileUtils.mkdir_p(File.dirname(target_path))
25
+ File.write(target_path, template)
26
+ target_path
27
+ end
28
+
29
+ def target_path
30
+ File.join(destination, SKILL_PATH)
31
+ end
32
+
33
+ private
34
+
35
+ def template
36
+ File.read(TEMPLATE_PATH)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: debug-with-tracepoint
3
+ description: Use when you need to know what a Ruby/Rails code path actually did at runtime - which methods ran, what arguments they got, what each one returned, and where an exception was first raised. Reach for this before adding puts/Rails.logger lines or reading call sites by hand, and whenever a value is wrong but you cannot tell which method produced it.
4
+ ---
5
+
6
+ # Debugging with rails_tracepoint_stack
7
+
8
+ Traces one block of code and hands back the call tree: every method your app
9
+ defined that ran, with its arguments, its return value, and any exception,
10
+ nested by call depth. Gems, the framework and stdlib are filtered out.
11
+
12
+ ## When this beats the alternatives
13
+
14
+ | Question | Use this |
15
+ |---|---|
16
+ | "Which of these methods is returning nil?" | Yes - return values are the point |
17
+ | "Where did this exception actually start?" | Yes - the raise is tagged in the tree |
18
+ | "What is the real order of calls here?" | Yes - the tree shows nesting |
19
+ | "What is the value of one variable right here?" | No - a `binding.irb` is cheaper |
20
+ | "Is this endpoint slow?" | No - this measures nothing |
21
+
22
+ ## Run it
23
+
24
+ One shot, no config file, no server restart, output bounded:
25
+
26
+ ```bash
27
+ bin/rails runner '
28
+ session = RailsTracepointStack.capture(max_depth: 4) do
29
+ Order.find(42).recalculate!
30
+ end
31
+ puts session.to_tree
32
+ '
33
+ ```
34
+
35
+ Reading the output:
36
+
37
+ ```
38
+ Order#recalculate! (app/models/order.rb:88) {}
39
+ Order#apply_discount (app/models/order.rb:102) {"total":200.0}
40
+ Discount#rate_for (app/models/discount.rb:12) {"order":"#<Order id: 42>"}
41
+ -> null <- rate_for returned nil, which is the bug
42
+ -> 200.0
43
+ -> 200.0
44
+ 3 calls, 3 returns, 0 raises, 2 classes
45
+ ```
46
+
47
+ - indentation is call depth
48
+ - `-> value` is what the method on the line above returned
49
+ - `!! ArgumentError: message` marks where an exception was raised
50
+ - a `-> null` right after a `!!` is the frame unwinding, not a real return
51
+ - `render app/views/...` is a template; the `{...}` after it are its locals
52
+ - the last line is the summary; `... truncated` means a limit cut it short
53
+
54
+ ### An empty tree is an answer, not a failure
55
+
56
+ ```
57
+ 0 calls, 0 returns, 0 raises, 0 classes
58
+ no app code ran: 34025 traces from gems, the framework and Ruby itself were filtered out
59
+ ```
60
+
61
+ This means the block ran entirely inside gems. A bare `Order.where(...).map(&:name)`
62
+ calls no method anyone in this app wrote — `name` is generated by ActiveRecord.
63
+ Do not conclude the tool is broken and fall back to `puts`. Either widen the
64
+ capture to include the code that calls into this, or accept that the bug is not
65
+ in app code. `session.filtered_count` is that number if you want it directly.
66
+
67
+ ## Keep the output small
68
+
69
+ `capture` is scoped to the calling thread and takes limits. Use them - an
70
+ unbounded capture of a real request is tens of thousands of lines.
71
+
72
+ | Option | Default | Use it to |
73
+ |---|---|---|
74
+ | `max_depth:` | none | Stay near the top of the tree |
75
+ | `max_traces:` | 5000 | Cap total lines |
76
+ | `max_string_length:` | 200 | Keep big payloads readable |
77
+ | `max_collection_size:` | 20 | Keep loaded associations readable |
78
+ | `capture_params: false` | on | Show only the flow |
79
+ | `capture_return: false` | on | Show only the calls |
80
+ | `threads: :all` | current only | Include background threads |
81
+
82
+ To narrow by file instead, set patterns before capturing:
83
+
84
+ ```ruby
85
+ RailsTracepointStack.configure do |config|
86
+ config.file_path_to_filter_patterns << %r{app/services/}
87
+ config.ignore_patterns << %r{app/models/concerns/}
88
+ end
89
+ ```
90
+
91
+ ## Other shapes of the same data
92
+
93
+ ```ruby
94
+ session.as_json # structured: one entry per trace
95
+ session.summary # counts only: {calls:, returns:, raises:, classes:, truncated:}
96
+ session.result # what the block itself returned
97
+ session.error # the exception that escaped, if any
98
+ ```
99
+
100
+ `capture` re-raises whatever the block raised. To keep the traces in that
101
+ case, take the session from the block argument:
102
+
103
+ ```ruby
104
+ session = nil
105
+ begin
106
+ RailsTracepointStack.capture { |s| session = s; thing_that_blows_up }
107
+ rescue => error
108
+ puts session.to_tree
109
+ end
110
+ ```
111
+
112
+ ## Worth knowing
113
+
114
+ - Requires the gem in the app: `gem "rails_tracepoint_stack"`.
115
+ - TracePoint slows the traced block down noticeably. Fine for a one-off
116
+ investigation, not for anything left running.
117
+ - Only methods defined in the app appear. If a method you expected is
118
+ missing, it is probably in a gem - add its path to
119
+ `file_path_to_filter_patterns` to force it in.
120
+ - `RAILS_TRACEPOINT_STACK_ENABLED=true` traces the whole process to
121
+ `log/rails_tracepoint_stack.log` instead. Prefer `capture` unless you need
122
+ to trace something you cannot wrap in a block, such as boot.
@@ -5,6 +5,7 @@ module RailsTracepointStack
5
5
  extend Forwardable
6
6
 
7
7
  attr_reader :params, :trace_point
8
+ attr_accessor :depth
8
9
 
9
10
  def_delegator :@trace_point, :defined_class, :class_name
10
11
  def_delegator :@trace_point, :method_id, :method_name
@@ -15,16 +16,42 @@ module RailsTracepointStack
15
16
  @trace_point = trace_point
16
17
  end
17
18
 
19
+ def kind
20
+ trace_point.event
21
+ end
22
+
18
23
  def params
24
+ return {} unless kind == :call
25
+
19
26
  @params ||= fetch_params(trace_point)
20
27
  end
21
28
 
29
+ def return_value
30
+ return nil unless kind == :return
31
+
32
+ trace_point.return_value
33
+ end
34
+
35
+ def exception
36
+ return nil unless kind == :raise
37
+
38
+ trace_point.raised_exception
39
+ end
40
+
22
41
  private
23
42
 
43
+ # Every local the method body declares is already in scope at :call time,
44
+ # holding nil. Reading the whole binding would report those as arguments
45
+ # the caller passed as nil, so the parameter list decides what to read.
24
46
  def fetch_params(trace_point)
25
- trace_point.binding.local_variables.map { |var|
26
- [var, trace_point.binding.local_variable_get(var)]
27
- }.to_h
47
+ binding = trace_point.binding
48
+ declared = binding.local_variables
49
+
50
+ trace_point.parameters.each_with_object({}) do |(_type, name), params|
51
+ next if name.nil? || !declared.include?(name)
52
+
53
+ params[name] = binding.local_variable_get(name)
54
+ end
28
55
  end
29
56
  end
30
57
  end
@@ -0,0 +1,30 @@
1
+ module RailsTracepointStack
2
+ # An immutable snapshot of a single trace. The TracePoint object is only
3
+ # valid while its event is being handled, so anything worth keeping has to
4
+ # be copied out before the handler returns.
5
+ TraceRecord = Struct.new(
6
+ :kind,
7
+ :class_name,
8
+ :method_name,
9
+ :file_path,
10
+ :line_number,
11
+ :params,
12
+ :return_value,
13
+ :exception_class,
14
+ :exception_message,
15
+ :depth,
16
+ keyword_init: true
17
+ ) do
18
+ def call?
19
+ kind == :call
20
+ end
21
+
22
+ def return?
23
+ kind == :return
24
+ end
25
+
26
+ def raise?
27
+ kind == :raise
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ require "json"
2
+ require "rails_tracepoint_stack/renderer/summary"
3
+ require "rails_tracepoint_stack/renderer/tree"
4
+
5
+ module RailsTracepointStack
6
+ # The traces gathered by one capture, plus whatever the traced block itself
7
+ # produced.
8
+ class TraceSession
9
+ attr_reader :traces
10
+ attr_accessor :result, :error, :filtered_count
11
+ attr_writer :truncated
12
+
13
+ def initialize
14
+ @traces = []
15
+ @truncated = false
16
+ @filtered_count = 0
17
+ end
18
+
19
+ def add(record)
20
+ @traces << record
21
+ end
22
+
23
+ def empty?
24
+ @traces.empty?
25
+ end
26
+
27
+ # True when a limit stopped the collection, so the traces are a prefix of
28
+ # what actually ran rather than the whole story.
29
+ def truncated?
30
+ @truncated
31
+ end
32
+
33
+ def to_tree
34
+ RailsTracepointStack::Renderer::Tree.call(self)
35
+ end
36
+ alias_method :to_s, :to_tree
37
+
38
+ def summary
39
+ RailsTracepointStack::Renderer::Summary.call(self)
40
+ end
41
+
42
+ def as_json
43
+ {
44
+ summary: summary,
45
+ traces: traces.map { |record| entry_for(record) }
46
+ }
47
+ end
48
+
49
+ def to_json(*args)
50
+ JSON.generate(as_json, *args)
51
+ end
52
+
53
+ private
54
+
55
+ def entry_for(record)
56
+ entry = {
57
+ kind: record.kind,
58
+ depth: record.depth,
59
+ class_name: record.class_name,
60
+ method_name: record.method_name,
61
+ file_path: "#{RailsTracepointStack::Renderer::Tree.relative_path(record.file_path)}:#{record.line_number}",
62
+ params: record.params
63
+ }
64
+
65
+ # A method that returned nil is often the answer being looked for, so
66
+ # the key stays even when there is nothing in it.
67
+ entry[:return_value] = record.return_value if record.return?
68
+
69
+ if record.raise?
70
+ entry[:exception_class] = record.exception_class
71
+ entry[:exception_message] = record.exception_message
72
+ end
73
+
74
+ entry
75
+ end
76
+ end
77
+ end
@@ -3,30 +3,80 @@ 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
+ def initialize(sink: RailsTracepointStack::Sink::Log.new, events: DEFAULT_EVENTS, thread: nil)
27
+ @sink = sink
28
+ @events = events
29
+ @thread = thread
30
+ @filtered_count = 0
15
31
  generate_tracer
16
32
  end
17
33
 
18
34
  private
19
35
 
20
36
  def generate_tracer
21
- @tracer ||= TracePoint.new(:call) do |tracepoint|
37
+ @tracer ||= TracePoint.new(*@events) do |tracepoint|
38
+ next if out_of_scope_thread?
39
+
22
40
  trace = RailsTracepointStack::Trace.new(trace_point: tracepoint)
23
41
 
24
- next if ignore_trace?(trace: trace)
42
+ if ignore_trace?(trace: trace)
43
+ @filtered_count += 1
44
+ next
45
+ end
46
+
47
+ trace.depth = depth_for(trace)
48
+ @sink.record(trace)
49
+ end
50
+ end
51
+
52
+ def out_of_scope_thread?
53
+ !@thread.nil? && Thread.current != @thread
54
+ end
55
+
56
+ # Only kept traces pay for reading the stack position, so the cost stays
57
+ # proportional to the app code being traced rather than to everything the
58
+ # VM runs underneath it.
59
+ def depth_for(trace)
60
+ tracker = depth_tracker
61
+ raw_position = raw_stack_position
25
62
 
26
- # TODO: Use proper OO
27
- message = RailsTracepointStack::LogFormatter.message trace
28
- RailsTracepointStack::Logger.log message
63
+ case trace.kind
64
+ when :call then tracker.enter(raw_position)
65
+ when :return then tracker.leave(raw_position)
66
+ when :raise then tracker.raised(raw_position)
29
67
  end
30
68
  end
69
+
70
+ def raw_stack_position
71
+ caller_locations(1)&.length || 0
72
+ end
73
+
74
+ def depth_tracker
75
+ Thread.current[depth_tracker_key] ||= RailsTracepointStack::DepthTracker.new
76
+ end
77
+
78
+ def depth_tracker_key
79
+ @depth_tracker_key ||= :"rails_tracepoint_stack_depth_#{object_id}"
80
+ end
31
81
  end
32
82
  end
@@ -0,0 +1,50 @@
1
+ module RailsTracepointStack
2
+ # Shrinks an already-serialized value so one fat argument (a big payload, a
3
+ # long SQL string, a loaded association) cannot dominate the output. Runs
4
+ # over the plain structures LogFormatter.safe_value produces, so it only
5
+ # ever sees strings, numbers, booleans, nil, arrays and hashes.
6
+ module Truncator
7
+ ELLIPSIS = "…".freeze
8
+
9
+ def self.call(value, limits)
10
+ case value
11
+ when String then truncate_string(value, limits)
12
+ when Array then truncate_array(value, limits)
13
+ when Hash then truncate_hash(value, limits)
14
+ else value
15
+ end
16
+ end
17
+
18
+ def self.truncate_string(value, limits)
19
+ max = limits.max_string_length
20
+ return value if max.nil? || value.length <= max
21
+
22
+ "#{value[0, max]}#{ELLIPSIS} (#{value.length} chars)"
23
+ end
24
+
25
+ def self.truncate_array(value, limits)
26
+ max = limits.max_collection_size
27
+ kept = (max.nil? || value.length <= max) ? value : value.first(max)
28
+ result = kept.map { |item| call(item, limits) }
29
+
30
+ return result if kept.length == value.length
31
+
32
+ result << "(+#{value.length - kept.length} more)"
33
+ end
34
+
35
+ def self.truncate_hash(value, limits)
36
+ max = limits.max_collection_size
37
+ dropped = (max.nil? || value.size <= max) ? 0 : value.size - max
38
+ kept = dropped.zero? ? value : value.first(max).to_h
39
+
40
+ result = kept.each_with_object({}) do |(key, item), memo|
41
+ memo[key] = call(item, limits)
42
+ end
43
+
44
+ return result if dropped.zero?
45
+
46
+ result[ELLIPSIS] = "(+#{dropped} more)"
47
+ result
48
+ end
49
+ end
50
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsTracepointStack
2
- VERSION = "0.3.5"
2
+ VERSION = "0.4.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,37 @@ module RailsTracepointStack
17
21
  yield(configuration)
18
22
  end
19
23
 
24
+ CAPTURE_EVENTS = [:call, :return, :raise].freeze
25
+
26
+ # Traces a block and hands back everything that happened inside it, instead
27
+ # of writing to a log. Meant to be run as a one-off: capture, read, done.
28
+ def self.capture(threads: :current, **limit_options)
29
+ raise ArgumentError, "Block not given to #capture" unless block_given?
30
+
31
+ collector = RailsTracepointStack::Sink::Collector.new(
32
+ limits: RailsTracepointStack::Limits.new(**limit_options)
33
+ )
34
+ session = collector.session
35
+ tracer = RailsTracepointStack::Tracer.new(
36
+ sink: collector,
37
+ events: CAPTURE_EVENTS,
38
+ thread: (threads == :all) ? nil : Thread.current
39
+ )
40
+
41
+ tracer.enable
42
+ begin
43
+ session.result = yield(session)
44
+ rescue Exception => error
45
+ session.error = error
46
+ raise
47
+ ensure
48
+ tracer.disable
49
+ session.filtered_count = tracer.filtered_count
50
+ end
51
+
52
+ session
53
+ end
54
+
20
55
  def self.enable_trace
21
56
  raise ArgumentError, "Block not given to #enable_trace" unless block_given?
22
57
 
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.4.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-24 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: []