Files
vercel__workflow/.changeset/stream-otel-spans.md
Karthik Kalyan 2c6ee614b5 feat(world-vercel): client-observed stream write/read e2e latency spans (#2857)
* feat(world-vercel): name stream client spans + add stream attributes

Stream write/read requests already share the instrumented HTTP envelope
(a CLIENT span + W3C trace-context injection), but the spans were named
for the bare HTTP verb (`http PUT`/`http GET`) and carried only generic
HTTP attributes — so stream latency couldn't be sliced per run/stream.

Name these spans for their operation (`workflow.stream.write` /
`workflow.stream.read`) and tag them with `workflow.run.id`,
`workflow.stream.name`, `workflow.stream.operation`
(write | write_multi | close | read), and `workflow.stream.start_index`
(read). Implemented via new optional `spanName`/`attributes` fields on
`instrumentedFetch`, so other callers are unaffected.

Additive OTEL only: no behavior change when no OpenTelemetry SDK is
registered (the span is undefined and attributes are dropped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Shorten changeset summary

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add client-observed end-to-end read TTFC span

The read GET can't report a client-measured latency back to the server (the
value only exists after the response starts streaming), so capture it purely
in the SDK's own OTEL: watch response.body for the first non-empty chunk and
emit a workflow.stream.read span back-dated to read dispatch, whose duration is
the end-to-end time-to-first-chunk (incl. the network hop) via
workflow.stream.read.ttfc_ms. Rename the fetch/connect span to
workflow.stream.read.connect. No-op without an OTEL SDK registered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add client-observed e2e write latency attribute

The write PUT is request/response and the server acks only after capturing the
chunk, so the workflow.stream.write span duration already equals the
client->server write latency. Expose it as a named attribute
workflow.stream.write.e2e_ms (via a durationAttribute option on
instrumentedFetch) for direct querying, parallel to the read ttfc_ms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document stream spans and latency attributes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Emit read TTFC span from the core reader instead of a world-vercel transform

Per review: move the client-observed time-to-first-chunk measurement out of a
TransformStream wrapper in world-vercel and into WorkflowServerReadableStream in
core, emitting workflow.stream.read on the first non-empty chunk reaching the
consumer. Removes the passthrough, measures at the reader abstraction, and is
backend-agnostic. world-vercel keeps the workflow.stream.read.connect HTTP span;
the recordElapsedSpan helper now lives in @workflow/core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Apply suggestion from @VaguelySerious

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

* Update docs/content/docs/v5/observability/tracing.mdx

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

* Address review: rename write.e2e_ms -> write.chunk_rtt; docs + changeset wording

Per review, rename the write attribute to workflow.stream.write.chunk_rtt (it's
a per-chunk client<->server round-trip, not a full e2e), update the docs row
wording for both write and read attributes, and shorten the changeset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-07-09 17:28:41 -07:00

98 B

@workflow/core, @workflow/world-vercel
@workflow/core @workflow/world-vercel
patch patch

Add stream latency to OTEL spans