void! can
be followed back to the root. In this tree void! is reached twice, once through
Refund and once directly from Cancellation.
The problem
A support ticket says an invoice was voided twice. Invoice#void! is meant to run once, from the
admin screen, and you need to find out what else ran it.
def void!
accounts.credit(account, total, memo: "void #{number}")
update!(state: "void")
end
A stack trace would answer that, but you get a stack trace when something raises, and nothing
raised. Reading the code shows that void! is called from Admin::InvoicesController, from
Refund#process and from a Cancellation service, and each of those has its own callers. That
gives you a list of plausible routes without telling you which of them run.
The usual next step is to add puts caller to void!, run again and read through the log.
The fix
The recording already has the stack for every call to void! that ran:
$ ra how_it_got_here Invoice#void! --examples
Invoice#void!
· 5 distinct routes · 18 calls
SubscriptionsController#cancel
└ Cancellation#call
└ Refund#process
└ Invoice#void! 6 calls
└ Invoice#void! 6 calls
Admin::InvoicesController#destroy
└ Invoice#void! 4 calls
Invoice#void! 1 call
Refund#process
└ Invoice#void! 1 call
called directly: spec/models/invoice_spec.rb:29
via Admin::InvoicesController#destroy: spec/requests/admin_invoices_spec.rb:4
via SubscriptionsController#cancel → Cancellation#call → Refund#process: spec/requests/subscriptions_spec.rb:4
via SubscriptionsController#cancel → Cancellation#call: spec/requests/subscriptions_spec.rb:4
via Refund#process: spec/services/refund_spec.rb:4
The first route contains the bug. Cancellation#call refunds the last invoice, which voids it, and
then voids it again itself, so every cancellation with a refund due voids twice: six cancellations
made twelve calls to void!. The last five lines name a spec example that takes each route, so the
one to run while you fix this is spec/requests/subscriptions_spec.rb:4. The fix is to leave
voiding to Refund:
def call
invoice = @subscription.last_invoice
Refund.new(invoice).process if invoice
@subscription.update!(state: "cancelled")
@subscription.student.current_enrollment&.withdraw
end
Where it shows up
- Over MCP,
how_it_got_herereturns the merged routes as text and as structured blocks, so your agent can see which route has no spec before it changes the method. - On the command line, run
ra how_it_got_here Klass#method, and add--examplesto name the spec example behind each route.
How it works
Each record in the recording stores the call tree it belongs to and its step within that tree,
along with the method that made the call and the method that received it. Starting from every
recorded call to Invoice#void!, how_it_got_here walks back up its own tree, one caller at a
time, to the root.
Identical chains are merged and counted, so a route that ran a thousand times appears once with its count, and a route that ran once has a line of its own beside it. Each chain is in call order, the same order as a stack trace read from the bottom.
Each tree is stamped with the spec example or request that produced it, so --examples can name a
spec that takes each route, or say that none does. A route whose root is Invoice#void! itself was
called directly from a spec body. Spec bodies are not traced, so the chain starts at the method.
Run it on your app
The output on this page comes from our example Rails app, and it is regenerated whenever a tool
changes. These are the commands behind it. On your own app, run ra record first, then put your
own class, method or file in their place.
ra how_it_got_here 'Invoice#void!' --examples
The double void is app/services/cancellation.rb:8-9. The method is app/models/invoice.rb:57.
Reading the result
- Frames inside gems and the framework are not recorded, so a chain starts at the first method your
app defines: the controller action, the job’s
perform, or the method a spec called.
Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.
Related tools
what_calls
The callers of a method. Every caller of a method in the recorded runs, with the file and line of each call and how often it was made, including calls made through public_send. what_it_calls lists the methods it calls.
See more →specs_that_reach
Run only the specs that reach your change. The spec examples whose recorded runs reach a method. After a change you rerun those examples alone, and the red-green loop takes seconds.
See more →how_to_reach
The setup a method needs. The collaborators and the call path that put an object into the state a method needs, taken from a recorded run. Use it to write setup for tests that use real objects in place of doubles.
See more →