Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate
, '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: stream flush span + otel load diagnostics by karthikscale3 · Pull Request #2891 · vercel/workflow · GitHub
Skip to content

telemetry: stream flush span + otel load diagnostics - #2891

Merged
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs
Jul 13, 2026
Merged

telemetry: stream flush span + otel load diagnostics#2891
karthikscale3 merged 4 commits into
mainfrom
kk/stream-client-span-attrs

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stream reads already have a client-observed span (workflow.stream.read + read.ttfc_ms, #2857). This adds the missing write-side piece — the client-side buffering that no server measurement can see — plus a diagnostic for a gap found while validating #2857 in production.

workflow.stream.flush span (core)

Each flushed batch from WorkflowServerWritableStream emits a back-dated CLIENT span whose duration is the app-perceived latency of the batch (first write() → server write settled), with:

  • workflow.stream.flush.buffer_dwell_ms — first write() of the batch → RPC dispatch. Captures the flush-timer wait and the turbo run-ready barrier hold on a stream's first write.
  • workflow.stream.flush.chunks / .bytes — batch shape.
  • workflow.run.id / workflow.stream.name / workflow.stream.operation: flush.

Named workflow.stream.flush (not .write) because #2857 already uses workflow.stream.write for the per-request RPC span (chunk_rtt) — in a trace the flush span sits above the RPC span, decomposing a write into dwell vs network/server. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell; emission is fire-and-forget and no-ops without OTEL; empty closes emit nothing. Documented in the tracing docs alongside #2857's spans.

DEBUG log for world-vercel OTEL load failure (world-vercel)

While validating #2857's spans in production we found its core-emitted span flows but its world-vercel-emitted spans (workflow.stream.write, workflow.stream.read.connect) don't appear from deployed apps, despite the same code emitting correctly in local tests — pointing at an environment-specific @opentelemetry/api load/resolution issue. world-vercel's import('@opentelemetry/api').catch(() => null) silently latches the failure, making that indistinguishable from "app has no OTEL". This PR logs the failure reason under DEBUG=workflow:* so the root cause is diagnosable from a deployment; the actual fix will follow once a deployment surfaces the reason.

Testing

  • writable-stream-telemetry.test.ts (InMemorySpanExporter): span kind/attributes per batch, one span per flush cycle, run-ready-barrier hold counted as dwell, no span on empty close.
  • world-vercel trace-propagation + http-core suites pass (18 tests).
  • pnpm test (packages/core): 1451 passed; pnpm typecheck passes; biome clean on changed files.

🤖 Generated with Claude Code

…batch
Complements the existing workflow.stream.read TTFC span: each flushed
batch emits a back-dated CLIENT span covering the app-perceived write
latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes
attributes so client-side batching cost (flush timer, turbo run-ready
barrier) can be told apart from network/server time. Failed flushes
keep the batch's original t0 so a retried batch reports its full dwell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code ownersJuly 13, 2026 00:15
@changeset-bot

changeset-botBot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff6017b

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

@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 ff6017b · Mon, 13 Jul 2026 03:17:14 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1404 (-6.4%)1804 🔴1846 🔴2018 🔴30
TTFShook + stream1680 (+6.0%)2069 🔴2127 🔴2284 🔴30
STSO1020 steps (1-20)290 (-13%)334 🔴409 🔴591 🔴19
STSO1020 steps (101-120)446 (+2.7%)488 🔴592 🔴653 🔴19
STSO1020 steps (1001-1020)837 (+0.6%)901 🔴907 🔴1118 🔴19
WOstream1404 (-6.4%)18041846201830
WOhook + stream1680 (+6.0%)20692127228430
SLstream4474 (-14%)5228 🔴5811 🔴6285 🔴30
SLhook + stream5072 (+2.2%)5686 🔴5879 🔴5977 🔴30
📜 Previous results (2)

363bb4f

Mon, 13 Jul 2026 02:28:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1171 (-22%)1591 🔴1644 🔴1734 🔴30
TTFShook + stream1452 (-8.4%)1885 🔴1925 🔴1960 🔴30
STSO1020 steps (1-20)296 (-11%)300 🔴554 🔴583 🔴19
STSO1020 steps (101-120)407 (-6.3%)445 🔴531 🔴680 🔴19
STSO1020 steps (1001-1020)862 (+3.7%)918 🔴975 🔴987 🔴19
WOstream1171 (-22%)15911644173430
WOhook + stream1452 (-8.4%)18851925196030
SLstream4568 (-12%)5734 🔴5828 🔴6466 🔴30
SLhook + stream5061 (+2.0%)5616 🔴5717 🔴5906 🔴30

271499a

Mon, 13 Jul 2026 00:38:17 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1372 (-8.5%)1670 🔴1841 🔴2576 🔴30
TTFShook + stream1647 (+3.9%)1945 🔴2117 🔴2687 🔴30
STSO1020 steps (1-20)322 (-3.5%)339 🔴669 🔴685 🔴19
STSO1020 steps (101-120)458 (+5.5%)481 🔴609 🔴1116 🔴19
STSO1020 steps (1001-1020)863 (+3.8%)919 🔴1006 🔴1006 🔴19
WOstream1372 (-8.5%)16701841257630
WOhook + stream1647 (+3.9%)19452117268730
SLstream4493 (-13%)5045 🔴5879 🔴6087 🔴30
SLhook + stream4912 (-1.0%)5593 🔴5639 🔴6233 🔴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.

@github-actions

github-actionsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ 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

📋 View full workflow run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3karthikscale3 changed the title telemetry: client-observed workflow.stream.flush span with buffer-dwell attributestelemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failuresJul 13, 2026
@karthikscale3karthikscale3 changed the title telemetry: add workflow.stream.flush buffer-dwell span; DEBUG-log world-vercel OTEL load failurestelemetry: stream flush span + otel load diagnosticsJul 13, 2026

@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 flush-span mechanics, the failure-path bookkeeping, and the diagnostic; ran both suites. Approving.

The batch-t0 bookkeeping is the subtle part and it's correct. I traced the interleavings:

  • t0 stamps at write() entry before the backpressure wait, so queueing behind an in-flight flush counts toward the next batch's dwell — matching the stated "app-perceived" semantics.
  • A write arriving during an in-flight flush stamps a fresh t0 pre-wait while its chunk lands in the next batch post-wait — so the timestamp and the chunk travel together.
  • The failure path's Math.min restore handles the tricky case (flush fails after a concurrent write already stamped a newer t0): the retried, merged batch correctly reports from the oldest unflushed write. Error propagation is unchanged — the new try/catch only restores state and rethrows.
  • The span emits only after a successful settle, with dispatchAt captured before the RPC, so buffer_dwell_ms cleanly isolates the pre-dispatch share.

Consistency checks: the fire-and-forget void (async ...) shape matches the existing workflow.stream.read call site from #2857 exactly, and the OTEL import is latch-caught so the no-OTEL path is a cheap null check per flush. The flush vs write span naming split (client-perceived batch vs per-request RPC) is the right taxonomy and the docs table makes the decomposition clear. The DEBUG gate on the world-vercel load-failure log matches the existing workflow:/* convention in the HTTP debug logging, and logging the reason while still returning null is the right diagnostic-without-behavior-change move.

Verified locally: core 1451/1451 (including the 4 new InMemorySpanExporter tests: per-batch attributes, one-span-per-cycle, barrier-hold-as-dwell, no-span-on-empty-close), world-vercel 235/235.

CI: the 3 failures are Windows-lane: the unit failure is streamer.test.ts > scopes chunk listing to the stream — a test from the already-merged chunk-sharding PR (#2807) timing out at ~12s on windows-latest, nothing to do with this diff (which doesn't touch world-local). Flagging it separately as a likely Windows fs-timing issue worth a look by whoever owns that test; the E2E Windows lane and the required-check aggregator are the usual baseline.

Two non-blocking notes:

  1. The failure-restore path (Math.min t0 retention across a failed flush → full-dwell reporting on retry) is the trickiest logic in the diff and the one path without a test — a test that fails the first writeMulti and asserts the retry span's back-dated start would pin it.
  2. Both this and the #2857 read-span call sites void an async IIFE without a .catch; a misbehaving third-party tracer that throws from startSpan/end would surface as an unhandled rejection. Vanishingly unlikely, but a shared .catch(() => {}) wrapper for both would close it — fine as a follow-up.

@karthikscale3
karthikscale3 merged commit 4a43e39 into mainJul 13, 2026
234 of 241 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2891-to-stable origin/stable
git cherry-pick -S 4a43e39fec61519a2756f4f5e7bae5ccdac6f662 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2891-to-stable
gh pr create --base stable --head backport/pr-2891-to-stable \
--title "Backport #2891: <original PR title>" \
--body "Manual backport of #2891 (cherry-pick 4a43e39fec61) to \`stable\`."

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.

2 participants

@karthikscale3@TooTallNate