Summary#
Answers the question you actually have when something goes wrong: what happened when it ran? A run that threw is retained with the stack and the values as they were at the moment of the throw — so instead of adding logging and running the failure a second time, you open the failure you already have.
Signature#
osy inspect [traceId] [--list] [--fault] [--step N] [--correlation-id <id>] [--json]Description#
Something failed — a test went red, a page action broke, a tool call came back with an error. The message tells you that it threw. It does not tell you why, because the message does not carry the values.
osy inspect does. Run it with no arguments and it lists the runs it has kept, the faulted ones marked with the
exception that ended them. Give it a run and it opens that run.
Every step is named and points at your source. A step is not an opaque position — it shows the function it ran, the
file and line it was on, and the statement itself. A step lands where you would put your cursor, in the .osy you wrote.
A run that crosses client and server reads as one story. A page action that awaits a server function — or a
server function that calls back to the client — interleaves both sides in one sequence, and shows the values that
crossed between them: the arguments handed over (→ handoff) and the value handed back (← resume). "Why did the
server receive the wrong argument?" and "why did the page get the wrong result?" become answerable without guessing at
the boundary.
You did not have to predict the failure. A fault is captured whether or not you had recording on: it costs nothing until something actually throws, so the moment you most need the state is never the moment it is discarded. Turning recording on (see Recording a run) additionally records the steps that led there — but you never need it just to see where and why a run died.
The values are the callee's. A throw is almost never in the function you invoked; it is several calls down, and the
state that explains it belongs to the function that threw — the arguments it was called with, and the local that made
the guard trip. --fault shows that stack, innermost frames included.
For a wrong result rather than a crash, record with --full. A fault shows the values at the throw; a run that
finished with the wrong answer never threw. Record it with osy trace start --full (Recording a run) and
each step carries the locals in scope at that point — so you watch the value that ends up wrong take shape, step by
step, on both sides of the run.
Nothing is resolved for you. A reference to a record is shown as its type and id, never fetched — reading it would
mean a database query under somebody's permissions, and a post-mortem view must not do that. A string is shown quoted,
so "3" is visibly not 3.
It is local only. Traces are kept in the local platform you run while developing (Running a local platform) and never leave your machine. A deployed server does not record them and will tell you so if asked.
Traces are scoped to this project's app, so a workspace of several apps does not mix their runs. Only the most recent runs are kept; a faulted run is kept in preference to a successful one, so a failure is not pushed out by the green runs behind it. If a run was long enough to be truncated, it says so rather than presenting a partial story as a whole one.
You do not need the run's own id to find it. When something failed through your app — an error dialog, a log line —
you usually hold a correlation id, the id of the interaction, not of one run. --correlation-id <id> opens the set
of runs that one interaction caused (a single click can trigger several), so you go from "this failed" to "these are
the runs it produced" without hunting for a trace id.
--json writes the trace as JSON — the form to hand to a coding agent, with the names, source spans, crossed-wire
values and locals it needs to act against your source.
This is a read of a finished run. To pause a live one and step it, use the debugger instead.
Examples#
osy inspect # what runs are retained? which one broke?
osy inspect <traceId> --fault # where it threw, and the values that were in scope there
osy inspect <traceId> # the whole run
osy inspect <traceId> --step 12 # one recorded step
osy inspect --correlation-id c-9f2a # the runs one interaction (a click) caused
osy inspect <traceId> --json # the same, as JSONA test fails. The message says only ValidationException: discount must be under 100%. The fault says why — named, at
the source line, with the values in scope:
$ osy inspect 232a6a2c --fault
FullDiscount_IsRejected Faulted 232a6a2c-ec59-402a-ab53-7d6747687c4b
ValidationException: discount must be under 100%
ApplyDiscount at pricing.osy:14
total = 200
percent = 100
factor = 0percent came in as 100, so factor computed to 0 and the guard tripped. Nobody was recording.
A checkout returns the wrong total — no crash, just a wrong number. Recorded with --full, the whole run reads as one
story: each step at its source line, the values crossing between the page and the server, and the locals taking shape:
$ osy trace start --full
$ osy inspect 7c1d0a4e
Cart.CheckOut Completed 7c1d0a4e-...
0 client stmt CheckOut at checkout.osy:8 total = Price(cart);
1 → handoff Price(cart: [Item#a1, Item#b2])
2 server stmt Price at pricing.osy:4 var sum = 0;
sum = 0
3 server return Price at pricing.osy:9 return sum;
sum = 240
4 ← resume Price returned 240The arguments that crossed to the server, the value that came back, and sum at each step — enough to see exactly
where 240 should have been something else.
See also#
The inner loop — where osy inspect sits in the develop-and-debug loop.
Recording a run — also record the steps that led to a failure.
Log.* — what your app writes with Log.*; a failure's log line names the run to inspect.
Understanding your app — what the app is, as opposed to what it did.