Skip to content

feat(world-vercel): synthesize per-event client spans on the WS transport - #3452

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/ws-synthetic-trace-spans
Aug 13, 2026
Merged

feat(world-vercel): synthesize per-event client spans on the WS transport#3452
shalabhc merged 4 commits into
mainfrom
shalabhc/ws-synthetic-trace-spans

Conversation

@shalabhc

@shalabhc shalabhc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

Websocket transport (#3084) does not produce per-message spans. In HTTP we got per request spans. In the WS transport writes become multiplexed frames on one long-lived socket, so the per-event spans dont exist.

This restores per-event spans by synthesizing a request-shaped span around each frame.

AI Details

Approach

The guiding constraint was comparability: a trace collected with the flag on should line up with one collected with it off, so dashboards and per-event traces survive flipping WORKFLOW_EVENTS_TRANSPORT. The guiding counter-constraint was honesty: a synthetic span must never be mistaken for a real HTTP request.

  • Shared envelope. Extracted withHttpClientSpan / recordClientSpanStatus out of instrumentedFetch in http-core.ts, so the synthetic span is emitted by the same code as the real one and cannot drift from it. InstrumentedFetchOptions now extends HttpClientSpanOptions.
  • Per-write span. postEventFrameOverWs opens http POST with url.full pointing at the v4 REST endpoint the frame is forwarded into — that's what keeps traces and latency dashboards comparable across the flag. Extracted eventsV4Url so this URL cannot drift from the one the HTTP path actually requests.
  • Honesty attributes. Both transports now set workflow.events.transport (http | ws) and workflow.event.type. The WS path additionally sets network.protocol.name=websocket, workflow.events.ws.url (the real wire destination) and workflow.events.ws.req_id (the join key to the server's log line for that frame). Setting workflow.events.transport on both paths is deliberate — it's the only way to slice two otherwise-identical spans against each other.
  • Connection span. Added workflow.events.ws.connect around the upgrade — the one genuinely-HTTP request in this path — carrying workflow.events.ws.reconnect_attempt. Named for the operation so it doesn't bucket with the event writes. This also puts resolveUpgradeHeaders' trace-context injection inside a client span, which AGENTS.md requires of every outgoing world-vercel request.
  • Bug fix. parseServer treated wss: as plaintext and defaulted server.port to 80; it's now TLS-aware (443).

Deliberately out of scope

  • Per-frame traceparent. Each frame can't carry its own trace context without a frame-meta field plus a coordinated server change. Only the upgrade propagates context today. Happy to spec it separately.
  • Vercel's "outgoing requests" view. That instruments global fetch. A WS frame is structurally not a fetch, so it cannot appear there no matter what spans we emit. OTEL traces are the recoverable surface; this closes that one.

Behaviour changes worth flagging

Two assertions in trace-propagation.test.ts changed, both intended:

  1. The upgrade's traceparent now names the connect span rather than the invocation span (it's a child, as every HTTP path already does).
  2. A traceparent is now injected even when no span is active — parity with every other world-vercel request path, where previously the WS upgrade injected nothing.

Tests

New ws-transport-spans.test.ts (12 tests). Unlike events-v4-ws.test.ts it does not mock resolveWsTransport, so spans come from the real selection + transport + adapter stack over a fake socket. Covers span shape and kind, url.full, the honesty attributes, reqId sequencing across writes, parenting to the caller rather than the connection, absence of a span when falling back to HTTP, HTTP 409 and TRANSPORT failure reporting, one span per retry attempt through withEventPostRetry, connect-span attributes, a refused upgrade, and HTTP/WS parity.

Verified: tsc --noEmit clean; vitest run src487/487 passing across 24 files.

Docs updated in docs/content/worlds/v5/vercel.mdx (attribute table comparing the two transports), plus an AGENTS.md note that non-fetch request paths must still open their client span via withHttpClientSpan. Changeset included (@workflow/world-vercel: patch).

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: daa7259

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@workflow/world-vercel Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 13, 2026 4:08am
example-nextjs-workflow-webpack Ready Ready Preview Aug 13, 2026 4:08am
example-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-astro-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-express-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-fastify-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-hono-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-nestjs-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-nitro-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-nuxt-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-python-workflow Error Error Aug 13, 2026 4:08am
workbench-sveltekit-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-tanstack-start-workflow Ready Ready Preview Aug 13, 2026 4:08am
workbench-vite-workflow Ready Ready Preview Aug 13, 2026 4:08am
workflow-docs Ready Ready Preview, v0 Aug 13, 2026 4:08am
workflow-swc-playground Ready Ready Preview Aug 13, 2026 4:08am
workflow-tarballs Ready Ready Preview Aug 13, 2026 4:08am
workflow-web Ready Ready Preview Aug 13, 2026 4:08am

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3466 0 590 4056
✅ 💻 Local Development 3517 0 539 4056
✅ 📦 Local Production 3810 0 558 4368
✅ 🐘 Local Postgres 3810 0 558 4368
✅ vercel-multi-region 27 0 0 27
✅ vercel-ws-transport 537 0 87 624
Total 15167 0 2332 17499
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 128 0 28
✅ astro-quickjs 128 0 28
✅ example-node 128 0 28
✅ example-quickjs 128 0 28
✅ express-node 128 0 28
✅ express-quickjs 128 0 28
✅ fastify-node 128 0 28
✅ fastify-quickjs 128 0 28
✅ hono-node 128 0 28
✅ hono-quickjs 128 0 28
✅ nest-node 128 0 28
✅ nest-quickjs 128 0 28
✅ nextjs-turbopack-node 153 0 3
✅ nextjs-turbopack-quickjs 153 0 3
✅ nextjs-webpack-node 153 0 3
✅ nextjs-webpack-quickjs 153 0 3
✅ nitro-node 128 0 28
✅ nitro-quickjs 128 0 28
✅ nuxt-node 128 0 28
✅ nuxt-quickjs 128 0 28
✅ sveltekit-node 147 0 9
✅ sveltekit-quickjs 147 0 9
✅ tanstack-start-node 128 0 28
✅ tanstack-start-quickjs 128 0 28
✅ vite-node 128 0 28
✅ vite-quickjs 128 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

✅ vercel-ws-transport

App Passed Failed Skipped
✅ example 128 0 28
✅ express 128 0 28
✅ nextjs-turbopack 153 0 3
✅ vite 128 0 28

📋 View full workflow run

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

Benchmarks are running for daa7259... (run logs)

Results below are from a previous run.

commit c2ea12b · Thu, 13 Aug 2026 01:29:34 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 225 (-6.6%) 1368 🔴 (+20%) 🔻 1511 🔴 (+29%) 🔻 1618 🔴 (+28%) 🔻 30
TTFS stream 254 (-7.3%) 1516 🔴 (+30%) 🔻 1535 🔴 (+27%) 🔻 1563 🔴 (±0%) 30
TTFS hook + stream 396 (-1.0%) 1734 🔴 (+25%) 🔻 1799 🔴 (+27%) 🔻 1940 🔴 (+30%) 🔻 30
STSO 1020 steps (inline) 134 (+19%) 🔻 196 (-7.1%) 232 (-7.9%) 392 (-5.5%) 1019
WO 1020 steps 193381 (-7.3%) 193381 (-7.3%) 193381 (-7.3%) 193381 (-7.3%) 1
SL stream latency 100 (+3.1%) 175 🔴 (+8.7%) 352 🔴 (+63%) 🔻 816 🔴 (+118%) 🔻 30
SO stream overhead (text) 128 (-15%) 259 🔴 (-8.8%) 673 🔴 (+10%) 840 (+8.5%) 30
SO stream overhead (structured) 124 (-6.1%) 208 (-22%) 💚 298 (-18%) 💚 980 (-96%) 💚 30
📜 Previous results (4)

eea5ff1

Wed, 12 Aug 2026 15:51:27 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1264 (+79%) 🔻 1462 🔴 (+45%) 🔻 1481 🔴 (+46%) 🔻 1672 🔴 (+11%) 30
TTFS stream 1364 (+43%) 🔻 1469 🔴 (+49%) 🔻 1497 🔴 (+48%) 🔻 1605 🔴 (+53%) 🔻 30
TTFS hook + stream 1659 (+37%) 🔻 1763 🔴 (+37%) 🔻 1817 🔴 (+36%) 🔻 1897 🔴 (+18%) 🔻 30
STSO 1020 steps (inline) 114 163 191 344 1019
WO 1020 steps 165277 (-57%) 💚 165277 (-57%) 💚 165277 (-57%) 💚 165277 (-57%) 💚 1
SL stream latency 85 (+4.9%) 121 🔴 (-8.3%) 161 🔴 (+15%) 216 🔴 (+21%) 🔻 30
SO stream overhead (text) 100 (-1.0%) 163 (-9.4%) 190 (-5.9%) 515 (+106%) 🔻 30
SO stream overhead (structured) 105 (+6.1%) 167 (+3.1%) 196 (+0.5%) 883 (+307%) 🔻 30

ae04a56

Tue, 11 Aug 2026 16:38:40 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 265 (-70%) 💚 1523 🔴 (+24%) 🔻 1548 🔴 (+19%) 🔻 2035 🔴 (+13%) 30
TTFS stream 246 (-77%) 💚 1456 🔴 (+29%) 🔻 1555 🔴 (+33%) 🔻 1830 🔴 (+47%) 🔻 30
TTFS hook + stream 474 (-66%) 💚 1757 🔴 (+13%) 1835 🔴 (+16%) 🔻 1960 🔴 (+23%) 🔻 30
STSO 1020 steps (inline) 97 (-38%) 💚 164 (-34%) 💚 195 (-38%) 💚 425 (-40%) 💚 1019
WO 1020 steps 162208 (-33%) 💚 162208 (-33%) 💚 162208 (-33%) 💚 162208 (-33%) 💚 1
SL stream latency 99 (-21%) 💚 182 🔴 (-1.1%) 200 🔴 (-15%) 💚 276 🔴 (-12%) 30
SO stream overhead (text) 120 (-19%) 💚 218 (-12%) 267 (-23%) 💚 915 (+54%) 🔻 30
SO stream overhead (structured) 121 (-25%) 💚 215 (-29%) 💚 298 (-15%) 💚 842 (-20%) 💚 30

080e4b2

Tue, 11 Aug 2026 15:28:43 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 248 (-77%) 💚 1449 🔴 (+23%) 🔻 1568 🔴 (+30%) 🔻 1588 🔴 (-7.1%) 30
TTFS stream 210 (-80%) 💚 1433 🔴 (+25%) 🔻 1506 🔴 (+29%) 🔻 1600 🔴 (+32%) 🔻 30
TTFS hook + stream 410 (-67%) 💚 1662 🔴 (+21%) 🔻 1727 🔴 (+24%) 🔻 1813 🔴 (+20%) 🔻 30
STSO 1020 steps (inline) 89 (-7.3%) 152 (-7.3%) 174 (-14%) 274 (-65%) 💚 1019
WO 1020 steps 149431 (-13%) 149431 (-13%) 149431 (-13%) 149431 (-13%) 1
SL stream latency 99 (+8.8%) 201 🔴 (+31%) 🔻 252 🔴 (+47%) 🔻 425 🔴 (-24%) 💚 30
SO stream overhead (text) 128 (-3.0%) 225 (-18%) 💚 343 (+3.9%) 974 (+24%) 🔻 30
SO stream overhead (structured) 124 (+1.6%) 313 🔴 (+28%) 🔻 412 (+32%) 🔻 621 (+38%) 🔻 30

847a1c3

Tue, 11 Aug 2026 06:37:30 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 218 (-79%) 💚 1233 🔴 (+4.8%) 1266 🔴 (+5.1%) 1430 🔴 (-16%) 💚 30
TTFS stream 1151 (+10%) 1211 🔴 (+5.9%) 1228 🔴 (+5.3%) 1303 🔴 (+7.8%) 30
TTFS hook + stream 1397 (+12%) 1516 🔴 (+11%) 1587 🔴 (+14%) 1629 🔴 (+8.0%) 30
STSO 1020 steps (inline) 86 (-10%) 121 (-26%) 💚 139 (-32%) 💚 208 (-73%) 💚 1019
WO 1020 steps 119887 (-30%) 💚 119887 (-30%) 💚 119887 (-30%) 💚 119887 (-30%) 💚 1
SL stream latency 74 (-19%) 💚 105 🔴 (-32%) 💚 121 🔴 (-30%) 💚 330 🔴 (-41%) 💚 30
SO stream overhead (text) 96 (-27%) 💚 136 (-51%) 💚 143 (-57%) 💚 151 (-81%) 💚 30
SO stream overhead (structured) 96 (-21%) 💚 141 (-42%) 💚 160 (-49%) 💚 290 (-36%) 💚 30
ℹ️ Metric definitions & methodology

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@shalabhc shalabhc changed the title feat(world-vercel): synthesize per-event client spans on the WS transport WIP feat(world-vercel): synthesize per-event client spans on the WS transport Aug 11, 2026
@shalabhc shalabhc added the ws-transport-test Run the e2e-vercel-ws-transport lanes (WORKFLOW_EVENTS_TRANSPORT=ws) on this PR label Aug 11, 2026
@vercel
vercel Bot temporarily deployed to Preview – workflow-docs August 11, 2026 15:11 Inactive
Comment thread packages/world-vercel/src/events-v4.ts Outdated
// Named on both transports so a trace or a latency dashboard can tell which
// one served a write — they are otherwise deliberately indistinguishable,
// right down to the span name and `url.full`. See `postEventFrameOverWs`.
attributes: { ...WorkflowEventsTransport('http'), ...attributes },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the GET call sites will also be tagged with this attribute - is that intentional?

@shalabhc shalabhc Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, no was a mistake. fixed.

@karthikscale3 karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just left one comment

Comment thread packages/world-vercel/src/events-v4.ts Outdated
@shalabhc

Copy link
Copy Markdown
Collaborator Author

Rebased cleanly onto current main with only the intended WS-span commits. The branch was updated with force-with-lease after verifying the 9-file diff and git diff --check.

vercel Bot and others added 4 commits August 13, 2026 01:04
…port

PR #3084 added the opt-in `WORKFLOW_EVENTS_TRANSPORT=ws` path and listed
"no client-side span on the WS path" as a known limitation. Because event
writes become multiplexed frames on one long-lived socket rather than
individual `fetch` calls, the per-event `http POST` CLIENT span that the
HTTP transport produced simply disappeared — traces went from one span
per event to nothing between the invocation and the server.

Restore it by synthesizing a request-shaped span around each frame, and
give the upgrade its own span:

- Extract `withHttpClientSpan` / `recordClientSpanStatus` from
  `instrumentedFetch` in `http-core.ts` so the synthetic span is emitted
  by the same envelope as the real one and cannot drift from it.
  `InstrumentedFetchOptions` now extends `HttpClientSpanOptions`.
- `postEventFrameOverWs` opens `http POST` with `url.full` pointing at the
  v4 REST endpoint the frame is forwarded into, so per-event traces and
  latency dashboards keep working across the flag. Extract `eventsV4Url`
  so that URL cannot drift from the one the HTTP path actually requests.
- Tag both transports with `workflow.events.transport` (`http` | `ws`) and
  `workflow.event.type`; the WS path additionally sets
  `network.protocol.name=websocket`, `workflow.events.ws.url` (the real
  wire destination) and `workflow.events.ws.req_id` (join key to the
  server's log line for the frame), so the span is never mistaken for a
  real HTTP request.
- Add a `workflow.events.ws.connect` span around the upgrade — the one
  genuinely-HTTP request here, previously the invisible half of every WS
  write's latency — carrying `workflow.events.ws.reconnect_attempt`. This
  also puts `resolveUpgradeHeaders`' trace-context injection inside a
  client span, as AGENTS.md requires.
- Fix `parseServer` to treat `wss:` as TLS (port 443, not 80).

Out of scope, deliberately: per-frame `traceparent` (needs a frame-meta
field plus a server change) and Vercel's outgoing-requests view (that
instruments global `fetch`, so a frame structurally cannot appear there).

Covered by `ws-transport-spans.test.ts`, which drives the real selection +
transport + adapter stack over a fake socket and asserts span shape,
failure reporting, retry behaviour and HTTP/WS parity.

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

Co-Authored-By: shalabhchaturvedi-7802 <shalabh.chaturvedi@vercel.com>

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Co-Authored-By: shalabhchaturvedi-7802 <shalabh.chaturvedi@vercel.com>

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Signed-off-by: Shalabh Chaturvedi <shalabh.chaturvedi@vercel.com>

Co-Authored-By: shalabhchaturvedi-7802 <shalabh.chaturvedi@vercel.com>
Signed-off-by: Shalabh Chaturvedi <shalabh.chaturvedi@vercel.com>

Co-Authored-By: Shalabh Chaturvedi <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 01991ed (AI decision).

This is observability feature work: it adds new synthetic per-event client spans, a workflow.events.ws.connect span, and several new span attributes for the opt-in WebSocket events transport. That transport is main-only — packages/world-vercel/src/ws-transport.ts and docs/content/worlds/v5/vercel.mdx do not exist on stable — so the change builds on APIs absent from the maintenance line. The one genuine fix bundled in is parseServer treating wss: as TLS (port 443 instead of 80) in http-core.ts, which a human could split out and force through if desired, though it only matters for the main-only WS path.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

01991edeebe15b3b5c38801a5ac1cf2962805e33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ws-transport-test Run the e2e-vercel-ws-transport lanes (WORKFLOW_EVENTS_TRANSPORT=ws) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants