Debugging is a team activity we still do alone. Python Tutor is single user and Python only. Over a call, one person shares a screen while everyone else watches, and nobody can point at the exact line where a variable went wrong.

I spent a week building Trace to fix that. You open a room with a six character code, no accounts, no sign up. A friend joins with the same code. You both edit the same snippet live, hit Run, and watch the code execute together. A timeline scrubber lets you drag back through the run, with the variables and call stack updating step by step. It is live at trace-swwy.vercel.app.

A line event fires before the line runs

Python tracing starts with sys.settrace, and the first thing it taught me is that a line event fires before the line executes. If you snapshot the locals on every line event, you are recording the state before the assignment, not after it. The last line of a function never shows its final values at all, because the function returns without another line event firing.

I found this on day one, writing fixtures by hand, because the final value of a variable kept coming back one step stale. The fix was to listen for return events too and treat them as the moment a frame's state is complete. It took an afternoon to find, because the wrong output looked almost right.

The JavaScript side had its own version of the same lesson. There is no settrace in Node, so I parse with acorn, instrument statement boundaries with step callbacks, and run the result in a vm context with no require, no filesystem, and no network. Async code returns a clean "not supported in v1" error instead of a half trace.

User code never runs in the API process

One rule before writing any tracer code: user code never runs in the API process. Each run spawns a subprocess with a scrubbed environment, no network, a read only working directory, CPU and memory caps, a ten second wall timeout, and a 2,000 step cap. Anything past the step cap gets flagged as truncated in the UI, never dropped quietly.

That sounds like paranoia until you remember what the endpoint does. It takes code from a stranger and executes it. The sandbox is the product boundary, not a hardening pass at the end. I wrote the limits as named constants in one config module so tuning is one edit, and I tested the kill paths the same way I tested the happy path.

A loop will blow up your trace payload

The third thing that broke was scale, and it broke in the most boring way possible. A 60 iteration loop produced 124 steps. That is fine. A real program with a few thousand iterations is not, because every step carries a snapshot of the locals, the stack, and stdout so far.

The answer was two rules. First, cap steps and say so plainly. Trace stops at 2,000 steps and tells you, with the option to raise the cap in config. Second, snapshots are JSON safe values with an explicit truncation marker. Objects get a type, a length, and a shallow preview. Nothing is dropped silently. The 60 iteration loop traced correctly, all 124 steps, and the total came out 1770, which I checked by hand.

The co-pilot only knows what the trace contains

The AI co-pilot was the feature I was most nervous about. A debugger that invents an explanation is worse than one that stays quiet. So the co-pilot gets one input, the trace JSON, and its explanations have to cite the step index and line they are talking about. If no Groq key is configured, it answers 503 and everything else keeps working.

I wrote a seeded bug fixture for this: a function that returns None, then code that indexes into it. Seven steps. The variable is None at step 4, the TypeError lands at step 5. The explanation has to name step 4. That test exists because the failure mode is so quiet. A confident wrong explanation sends a student hunting in the wrong file for an hour.

What I would do differently

Two things. First, I would have written the sandbox limits and the truncation policy on day one instead of day two. They shaped every later decision, and the tracer code I wrote before them had to be revisited once the limits existed. Second, I would have dogfooded with a real second person sooner. The two browser room test passed locally: collab edit, presence, shared run, zero console errors. The live version, two real people debugging together, is still the next step.

The backend runs on Render's free tier, and the first visit can take half a minute to wake up. The landing page warns first visitors, and the frontend rides out the wake-up. Free tier trade-offs are fine as long as you name them.

Trace shipped with 91 backend tests green and the frontend build green. The part I am proudest of is not a feature. It is that the values on screen are honest: what the code actually did, with the gaps labeled instead of hidden. That is the whole job of a debugger.

Try it live: https://trace-swwy.vercel.app