1. See and evaluate
Capture enough that you can read what happened at each step, time it, and grade it.
2. Replay
Decide which steps become mocks: spans that serve their recorded output instead of running again.
Goal 1: capture enough to see and evaluate
Give a step its own span when any of these is true:- It calls a model. Always. This is the span you iterate on, compare across experiments, and attach graders to.
- It reads external mutable state (DB query, HTTP
GET, object storage, vector search, cache). These are the spans you will want to mock on replay. - It writes external state (DB write, queue publish, email, charge, file write). Mark these to mock on replay so a replayed trace does not repeat the side effect.
- It transforms the model output (parsing, validation, ranking, formatting), so a quality regression points at the model or at your post-processing.
- It retries or loops, one span per attempt or iteration, so a trace shows how many attempts it really took.
What one span costs you
Example
Goal 2: decide what gets mocked on replay
Replay re-runs recorded inputs through your current code. The root always runs for real; each descendant either runs for real or serves its recorded output. Spans that serve recordings are mocks, and you choose them per span, at definition time, withmockOnReplay. Only descendants can be mocks: a root always executes.
Mock the world, run your code. The point of a replay is to test a change against production scenarios, so anything that is not the change should be held fixed and cheap:
Mark a span when you define it, and the default
"marked" strategy does the rest:
summarize-doc feeds each historical document straight from its recording, runs your new prompt for real, and writes nothing. Note that this only works because read-doc and persist are spans: goal 2 is only available for steps you captured in goal 1.
Replay also needs a root whose own arguments are JSON-serializable, since that is what it re-runs the trace against. Prefer an id, a request object, or a message list over a live connection, a stream, or a class instance.
Pitfalls
One span around the whole workflow
Calling the Vercel AI SDK without wrapping the model
withSpan around a function that calls a model does not create a span for the model call. See the Vercel AI SDK integration.
Wrapping in an anonymous function
An unserializable root
Mocking the thing you are changing
Mocking the model call, or running withmock: "all", makes a replay return its recorded outputs and prove nothing about your change. Mock the setup around the step you are iterating on, never the step itself.
Marking nothing, then replaying against production
With no span marked, the default strategy mocks nothing and every descendant runs for real. If the workflow writes, a replay repeats the write for every trace in the run. Mark writes before the first replay, not after.Hand-wrapping framework internals
If you use a framework integration, the handler, processor, or middleware already records the graph nodes, tools, and model calls. Wrapping them yourself produces duplicate spans. Add manual spans only for work above, alongside, or below the framework call.Self-check
Open a trace in the web portal:- Is there more than one node?
- Is there a span for the model call, carrying the prompt and the output?
- Is every external read and write its own span, marked to mock on replay?