Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); telemetry: move client stream spans from world-vercel to core by karthikscale3 · Pull Request #2901 · vercel/workflow · GitHub
Skip to content

telemetry: move client stream spans from world-vercel to core - #2901

Merged
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api
Jul 14, 2026
Merged

telemetry: move client stream spans from world-vercel to core#2901
karthikscale3 merged 8 commits into
mainfrom
kk/dedupe-otel-api

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

world-vercel's client spans don't export from deployed apps (root cause under investigation — evidence in comments; core's spans are proven to export), so the client-observed stream latency signals move to @workflow/core:

  • workflow.stream.write.chunk_rtt — write RPC duration incl. network, on the workflow.stream.flush span
  • workflow.stream.read.connect_ms — read dispatch → stream handle, on the workflow.stream.read span
  • workflow.stream.close — new span; duration = close RPC round trip
  • workflow.stream.read.complete — new span; duration = total read, with .chunks / .bytes for throughput (and .reconnects on the framed path)

Measured at the World interface, so world-local/world-postgres are covered too. Also: @opentelemetry/api deduped to one workspace instance (hygiene) and DEBUG-gated OTEL diagnostics retained from the investigation.

Verified live on this PR's preview: chunk_rtt 73–85ms, close.rpc_ms 72–96ms. connect_ms/read.complete are unit-tested on both read paths (WorkflowServerReadableStream and createReconnectingFramedStream) — the latter was initially missed, which is why preview reads produced no spans; covered as of 4d08c87.

Testing

packages/core: 1454 tests pass; pnpm build / typecheck / biome clean.

🤖 Generated with Claude Code

The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers
the tracer provider (via @vercel/otel in the app) and the copy a package
imports could differ. The API's global-registration version check rejects
a consumer newer than the registered copy and silently hands back a noop
tracer — which is why world-vercel's spans (workflow.stream.write/
chunk_rtt, read.connect, its http spans) never reached Datadog from
deployed apps while core's spans flowed in the same process. Root-caused
via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans
dropped.
Pin a single version via a workspace override so every bundle shares one
API instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 18:03
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ced2a8

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production145212301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7377110648442

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro125127
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@vercel

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8ced2a8 · Tue, 14 Jul 2026 01:15:33 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1343 (+15%)1683 🔴1737 🔴2212 🔴30
TTFShook + stream1785 (+34%)1965 🔴2034 🔴3978 🔴30
STSO1020 steps (1-20)275 (-0.7%)322 🔴373 🔴377 🔴19
STSO1020 steps (101-120)414 (-4.0%)452 🔴477 🔴582 🔴19
STSO1020 steps (1001-1020)840 (+1.2%)890 🔴937 🔴967 🔴19
WOstream1343 (+15%)16831737221230
WOhook + stream1785 (+34%)19652034397830
SLstream4266 (-17%)4895 🔴5662 🔴6557 🔴30
SLhook + stream4757 (-2.0%)4700 🔴5596 🔴6386 🔴30
📜 Previous results (4)

4d08c87

Tue, 14 Jul 2026 00:51:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1262 (+7.9%)1646 🔴1712 🔴2046 🔴30
TTFShook + stream1424 (+6.7%)1924 🔴2004 🔴2154 🔴30
STSO1020 steps (1-20)280 (+1.2%)321 🔴362 🔴366 🔴19
STSO1020 steps (101-120)461 (+7.0%)574 🔴693 🔴723 🔴19
STSO1020 steps (1001-1020)866 (+4.3%)906 🔴1034 🔴1051 🔴19
WOstream1262 (+7.9%)16461712204630
WOhook + stream1424 (+6.7%)19242004215430
SLstream4404 (-15%)5411 🔴5552 🔴5763 🔴30
SLhook + stream5047 (+4.0%)5551 🔴5656 🔴5975 🔴30

63eba7b

Tue, 14 Jul 2026 00:18:56 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1555 (+50%)1815 🔴1849 🔴2981 🔴30
TTFShook + stream1690 (+18%)2099 🔴2136 🔴2284 🔴30
STSO1020 steps (1-20)314 (+4.5%)365 🔴495 🔴734 🔴19
STSO1020 steps (101-120)463 (+4.5%)502 🔴605 🔴754 🔴19
STSO1020 steps (1001-1020)842 (-1.5%)893 🔴933 🔴1035 🔴19
WOstream1555 (+50%)18151849298130
WOhook + stream1690 (+18%)20992136228430
SLstream4834 (+5.0%)5248 🔴5680 🔴6089 🔴30
SLhook + stream4930 (-0.5%)5594 🔴5722 🔴5853 🔴30

cfd543a

Mon, 13 Jul 2026 19:21:32 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1407 (+31%)1686 🔴1697 🔴2194 🔴30
TTFShook + stream1360 (-1.6%)1912 🔴1944 🔴2329 🔴30
STSO1020 steps (1-20)295 (+13%)367 🔴438 🔴534 🔴19
STSO1020 steps (101-120)426 (+7.3%)449 🔴658 🔴715 🔴19
STSO1020 steps (1001-1020)892 (-5.9%)940 🔴1022 🔴1234 🔴19
WOstream1407 (+31%)16861697219430
WOhook + stream1360 (-1.6%)19121944232930
SLstream4502 (-7.6%)4860 🔴5695 🔴5819 🔴30
SLhook + stream5010 (+1.6%)5471 🔴5599 🔴5712 🔴30

4b037cb

Mon, 13 Jul 2026 18:26:14 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1211 (±0%)1772 🔴1802 🔴1841 🔴30
TTFShook + stream1839 (+22%)2043 🔴2145 🔴2297 🔴30
STSO1020 steps (1-20)282 (+3.9%)356 🔴497 🔴507 🔴19
STSO1020 steps (101-120)435 (+7.6%)451 🔴636 🔴677 🔴19
STSO1020 steps (1001-1020)955 (+5.5%)1014 🔴1117 🔴1163 🔴19
WOstream1211 (±0%)17721802184130
WOhook + stream1839 (+22%)20432145229730
SLstream4607 (-1.3%)5547 🔴5647 🔴5869 🔴30
SLhook + stream4389 (-13%)4575 🔴5148 🔴5761 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

… on turbopack workbench
The dedupe alone did not restore world-vercel span emission (verified on
this PR's own preview: stream traffic flowed, zero workflow.stream.write
spans). Under DEBUG=workflow:*, both packages now log once how their
module instance of @opentelemetry/api sees the world — global
registration version, provider/delegate/tracer/probe constructor names,
and whether a probe span is recording. Diffing the core line (spans work)
against the world-vercel line (spans dropped) in one deployment's logs
pinpoints the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, close span)
world-vercel's instrumentedFetch spans never export from deployed apps
(root cause still open — see PR discussion), so the operationally
needed client-side latency signals move one layer up to core, whose
spans are proven to export:
- workflow.stream.write.chunk_rtt on the workflow.stream.flush span:
the World write RPC duration, network included (same attribute key as
world-vercel's per-request span so queries are layer-agnostic).
- workflow.stream.read.connect_ms on the workflow.stream.read span:
the world.streams.get await (read dispatch -> stream handle).
- new workflow.stream.close span: the close RPC round trip.
Bonus: measured at the World interface, these cover world-local and
world-postgres too, not just Vercel deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title fix: world-vercel OTEL span emission (dedupe + runtime diagnostics)telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationJul 13, 2026
Completes the read-side picture: workflow.stream.read.complete is
back-dated to the read dispatch so its duration is the total read, with
chunk/byte counts for throughput. Cancelled reads emit nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: emit stream RPC latencies from core; world-vercel OTEL investigationtelemetry: move client stream spans from world-vercel to coreJul 13, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the root-cause work, the span relocation, and the dedupe; ran both suites. Approving with one pre-merge ask.

The root-cause analysis is the best part of this PR. The override comment nails the mechanism (two resolved @opentelemetry/api copies → the global-registration version check hands the non-registering copy a noop tracer → world-vercel's spans silently dropped while core's flowed), and keeping the DEBUG-gated one-shot diagnostics that found it — with core and world-vercel emitting the same shape side by side (registration version, provider/delegate/tracer/probe constructors, recording status) — turns a one-off investigation into a permanent triage tool for exactly this failure class.

Relocation details verified:

  • connect_ms is measured around the single world.streams.get acquisition (transparent v3 reconnects happen inside the world stream, so it's genuinely connect-once) and stamped onto the existing production-proven read span.
  • read.complete emits only on drain — cancelled reads emit nothing, matching the docs.
  • The close span is correctly unconditional (the close RPC happens even for an empty stream), and the pre-existing "empty close" test correctly scopes its assertion to flush spans, so there's no inconsistency.
  • Keeping workflow.stream.write.chunk_rtt as the attribute key on the flush span preserves query/dashboard continuity with world-vercel's per-RPC span attribute — and the resulting double-emission in environments where world-vercel's OTEL does work is coherent layering (per-batch above per-RPC), not duplication.
  • 1454 core + 235 world-vercel tests pass locally; the new InMemorySpanExporter tests cover the read (ttfc+connect), read-complete, and close paths.

Pre-merge ask: merge main and let the Multi-Region lane actually run. The current E2E Vercel Multi-Region Tests failure is "No test files found" — e2e-region.test.ts landed on main (#1981) after this branch's base, so the lane exits 1 without running anything. That's a staleness artifact, not a defect — but this PR changes workspace-wide dependency resolution (the @opentelemetry/api override + 470-line lockfile churn), which is precisely the kind of change every lane should validate, and the multi-region suite has never run against it. A main merge + fresh lockfile + green run closes that gap.

Two questions/notes, non-blocking:

  1. world-vercel's own remaining spans are still exposed to the noop-tracer failure in user apps. The workspace override fixes the monorepo/workbenches, and the relocation rescues the client stream signals — but world-vercel still emits its per-RPC stream spans and HTTP client spans through its own @opentelemetry/api import, whose resolution in a user's app is whatever their package manager decides. core avoids this via its optional peer "@opentelemetry/api": "1"; world-vercel's manifest is untouched here. Is aligning world-vercel's dependency declaration the follow-up tracked in #2900? Worth stating in the PR description either way, since this PR reads like the fix but is really the workaround plus hygiene.
  2. Docs nit: the chunk_rtt table row leads with the old per-chunk ack semantics ("emissions of a chunk to the wire… ack for that chunk") — on the flush span it's the whole batch's write RPC duration, which only the second sentence conveys. Leading with the batch semantics would better match what's actually emitted now.

Ordinary serialized streams read through createReconnectingFramedStream
(which calls world.streams.get directly), so connect_ms / ttfc /
read.complete never fired for that path — only WorkflowServerReadableStream
was instrumented. Wire the same helpers into the framed reader: first-
connect duration, first-frame TTFC, and completion totals — plus
workflow.stream.read.reconnects, which only this path can know.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3

Copy link
Copy Markdown
ContributorAuthor

Addressed the review finding: createReconnectingFramedStream (the path ordinary serialized streams read through — it calls world.streams.get directly) now emits the same read telemetry as WorkflowServerReadableStream: connect_ms (first connect), first-frame TTFC, and workflow.stream.read.complete with totals — plus workflow.stream.read.reconnects, which only this path can know. New test covers the framed path (4d08c87; 1455 core tests pass).

This also retroactively explains why preview replays never produced read spans during verification — those reads went through the framed path, which was uninstrumented.

@github-actionsgithub-actionsBot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9242ddb (AI decision).

The substance of this commit (new chunk_rtt/connect_ms attributes and the workflow.stream.close / workflow.stream.read.complete spans) extends client-side stream telemetry helpers like recordStreamWriteFlush, recordReadTimeToFirstChunk, and the associated workflow.stream.flush/read spans that exist only on main — none of them, nor the two new telemetry test files, are present on stable's serialization.ts. The v5 docs edits and the @opentelemetry/api dedupe are in service of this main-only feature, so the change as a whole cannot meaningfully or cleanly land on stable.

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

9242ddb02c5df6046bf0d93cc5e520eedcfd7471

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@karthikscale3@TooTallNate@VaguelySerious