Skip to content

Add improved OpenTelemetry instrumentation for workflow observability#933

Merged
pranaygp merged 5 commits intomainfrom
pranaygp/improve-otel
Feb 5, 2026
Merged

Add improved OpenTelemetry instrumentation for workflow observability#933
pranaygp merged 5 commits intomainfrom
pranaygp/improve-otel

Conversation

@pranaygp
Copy link
Collaborator

@pranaygp pranaygp commented Feb 4, 2026

Summary

  • Add tracing to HTTP requests in world-vercel with method, endpoint, and status attributes
  • Add tracing to storage operations (runs, steps, events, hooks) via instrumentObject()
  • Add tracing to event loading with event count and pages loaded attributes
  • Add queue timing breakdown attributes (deserialize, execution, serialize times) to step handler
  • Add new semantic conventions for world/storage, events, serialization, and queue breakdown

Key Changes

@workflow/core

  • New semantic conventions in telemetry/semantic-conventions.ts
  • getAllWorkflowRunEvents() now wrapped with WORKFLOW.loadEvents span
  • Step handler adds timing breakdown: queue.deserialize_time_ms, queue.execution_time_ms, queue.serialize_time_ms

@workflow/world-vercel

  • makeRequest() wrapped with WORLD.http {method} {endpoint} span (SpanKind.CLIENT)
  • Nested spans for WORLD.parse and WORLD.validate
  • All storage methods instrumented via instrumentObject()
  • New minimal telemetry module to avoid circular dependency with @workflow/core

Test plan

  • pnpm build passes
  • pnpm test passes for @workflow/core (1 pre-existing flaky test)
  • pnpm test passes for @workflow/world-vercel
  • Verify spans appear in OTEL collector with correct attributes

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 4, 2026 17:43
@changeset-bot
Copy link

changeset-bot bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: b142e22

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

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

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

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

@vercel
Copy link
Contributor

vercel bot commented Feb 4, 2026

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.040s (+2.3%) 1.018s (~) 0.977s 10 1.00x
💻 Local Express 0.042s (+0.7%) 1.008s (~) 0.965s 10 1.05x
💻 Local Nitro 0.045s (+2.8%) 1.008s (~) 0.963s 10 1.11x
🐘 Postgres Next.js (Turbopack) 0.147s (-47.4% 🟢) 1.021s (~) 0.874s 10 3.64x
🐘 Postgres Nitro 0.199s (-12.5% 🟢) 1.016s (~) 0.817s 10 4.93x
🐘 Postgres Express 0.239s (-14.0% 🟢) 1.018s (~) 0.779s 10 5.93x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.659s (-11.0% 🟢) 1.542s (-4.1%) 0.883s 10 1.00x
▲ Vercel Nitro 0.670s (~) 1.593s (~) 0.923s 10 1.02x
▲ Vercel Next.js (Turbopack) 0.692s (-21.4% 🟢) 1.576s (-19.5% 🟢) 0.884s 10 1.05x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.094s (-0.7%) 2.013s (~) 0.919s 10 1.00x
💻 Local Express 1.115s (~) 2.007s (~) 0.892s 10 1.02x
💻 Local Nitro 1.119s (~) 2.006s (~) 0.887s 10 1.02x
🐘 Postgres Next.js (Turbopack) 2.112s (+14.3% 🔺) 2.416s (+19.9% 🔺) 0.304s 10 1.93x
🐘 Postgres Express 2.182s (+1.1%) 3.015s (~) 0.833s 10 1.99x
🐘 Postgres Nitro 2.409s (+10.0% 🔺) 3.013s (~) 0.604s 10 2.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.811s (-2.4%) 3.883s (+5.0%) 1.073s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.844s (-2.3%) 3.658s (-4.4%) 0.815s 10 1.01x
▲ Vercel Express 2.950s (~) 3.828s (+7.5% 🔺) 0.878s 10 1.05x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 10.714s (~) 11.019s (~) 0.305s 3 1.00x
💻 Local Express 10.836s (~) 11.009s (~) 0.173s 3 1.01x
💻 Local Nitro 10.853s (~) 11.019s (~) 0.166s 3 1.01x
🐘 Postgres Next.js (Turbopack) 15.357s (+0.8%) 16.034s (~) 0.677s 2 1.43x
🐘 Postgres Nitro 20.405s (~) 21.033s (~) 0.628s 2 1.90x
🐘 Postgres Express 20.503s (~) 21.030s (~) 0.527s 2 1.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 22.832s (+1.0%) 23.287s (~) 0.455s 2 1.00x
▲ Vercel Express 22.872s (-2.0%) 23.848s (-2.7%) 0.976s 2 1.00x
▲ Vercel Next.js (Turbopack) 23.122s (+1.8%) 23.765s (+1.4%) 0.643s 2 1.01x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 27.194s (~) 28.037s (~) 0.843s 3 1.00x
💻 Local Express 27.465s (~) 28.018s (~) 0.553s 3 1.01x
💻 Local Nitro 27.508s (~) 28.026s (~) 0.518s 3 1.01x
🐘 Postgres Next.js (Turbopack) 37.501s (~) 38.067s (~) 0.566s 2 1.38x
🐘 Postgres Nitro 50.285s (~) 51.071s (~) 0.786s 2 1.85x
🐘 Postgres Express 50.430s (~) 51.080s (~) 0.650s 2 1.85x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 57.039s (~) 57.643s (-0.5%) 0.604s 2 1.00x
▲ Vercel Nitro 57.494s (+1.2%) 57.767s (+0.9%) 0.273s 2 1.01x
▲ Vercel Next.js (Turbopack) 57.828s (~) 58.890s (+0.9%) 1.062s 2 1.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 56.691s (~) 57.045s (~) 0.354s 2 1.00x
💻 Local Express 57.087s (~) 58.045s (~) 0.958s 2 1.01x
💻 Local Nitro 57.144s (~) 58.047s (+0.9%) 0.903s 2 1.01x
🐘 Postgres Next.js (Turbopack) 74.897s (-5.5% 🟢) 75.606s (-5.0% 🟢) 0.708s 2 1.32x
🐘 Postgres Express 100.221s (~) 101.153s (~) 0.932s 1 1.77x
🐘 Postgres Nitro 100.579s (~) 101.104s (~) 0.525s 1 1.77x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 121.566s (+0.5%) 123.475s (+0.9%) 1.909s 1 1.00x
▲ Vercel Next.js (Turbopack) 124.169s (-4.5%) 125.527s (-3.8%) 1.358s 1 1.02x
▲ Vercel Express 129.415s (+5.8% 🔺) 130.400s (+5.8% 🔺) 0.985s 1 1.06x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.390s (-0.9%) 2.006s (~) 0.617s 15 1.00x
💻 Local Next.js (Turbopack) 1.403s (+0.8%) 2.011s (~) 0.608s 15 1.01x
💻 Local Express 1.408s (-0.7%) 2.006s (~) 0.598s 15 1.01x
🐘 Postgres Express 2.313s (-3.2%) 3.013s (~) 0.700s 10 1.66x
🐘 Postgres Nitro 2.315s (-1.9%) 3.013s (~) 0.698s 10 1.67x
🐘 Postgres Next.js (Turbopack) 2.506s (+33.4% 🔺) 2.741s (+8.6% 🔺) 0.235s 11 1.80x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.405s (+13.3% 🔺) 4.067s (+4.4%) 0.662s 8 1.00x
▲ Vercel Next.js (Turbopack) 3.408s (+13.0% 🔺) 4.367s (+9.3% 🔺) 0.959s 7 1.00x
▲ Vercel Nitro 3.413s (+20.0% 🔺) 4.033s (+9.1% 🔺) 0.620s 8 1.00x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 2.504s (~) 3.019s (~) 0.515s 10 1.00x
💻 Local Express 2.587s (~) 3.016s (~) 0.429s 10 1.03x
💻 Local Next.js (Turbopack) 2.611s (+1.0%) 3.026s (~) 0.414s 10 1.04x
🐘 Postgres Express 7.458s (-13.0% 🟢) 8.330s (-10.6% 🟢) 0.871s 4 2.98x
🐘 Postgres Nitro 9.694s (+23.3% 🔺) 10.431s (+22.0% 🔺) 0.736s 3 3.87x
🐘 Postgres Next.js (Turbopack) 12.808s (+11.2% 🔺) 13.382s (+11.3% 🔺) 0.574s 3 5.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.550s (+11.7% 🔺) 4.339s (+12.9% 🔺) 0.789s 7 1.00x
▲ Vercel Next.js (Turbopack) 3.812s (-1.5%) 4.486s (-3.2%) 0.674s 7 1.07x
▲ Vercel Nitro 4.414s (+43.2% 🔺) 5.279s (+38.1% 🔺) 0.865s 6 1.24x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 6.679s (-4.1%) 7.599s (+1.3%) 0.920s 4 1.00x
💻 Local Next.js (Turbopack) 6.914s (-6.4% 🟢) 8.298s (-1.9%) 1.384s 4 1.04x
💻 Local Express 7.348s (+1.1%) 8.122s (-1.6%) 0.774s 4 1.10x
🐘 Postgres Nitro 46.078s (+2.0%) 46.143s (~) 0.065s 1 6.90x
🐘 Postgres Express 47.463s (-3.2%) 48.147s (-2.5%) 0.684s 1 7.11x
🐘 Postgres Next.js (Turbopack) 55.389s (+8.4% 🔺) 56.149s (+9.6% 🔺) 0.760s 1 8.29x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.793s (+41.6% 🔺) 6.752s (+46.9% 🔺) 0.960s 5 1.00x
▲ Vercel Next.js (Turbopack) 9.917s (+165.0% 🔺) 10.468s (+135.8% 🔺) 0.551s 3 1.71x
▲ Vercel Express 11.526s (+244.8% 🔺) 11.958s (+216.5% 🔺) 0.432s 3 1.99x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.422s (-1.8%) 2.006s (~) 0.585s 15 1.00x
💻 Local Express 1.434s (~) 2.006s (~) 0.573s 15 1.01x
💻 Local Next.js (Turbopack) 1.436s (~) 2.010s (~) 0.574s 15 1.01x
🐘 Postgres Express 2.053s (-2.2%) 2.610s (+3.6%) 0.557s 12 1.44x
🐘 Postgres Nitro 2.102s (-8.6% 🟢) 2.837s (+6.0% 🔺) 0.735s 11 1.48x
🐘 Postgres Next.js (Turbopack) 2.180s (+21.3% 🔺) 2.846s (+41.4% 🔺) 0.665s 11 1.53x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.955s (+2.9%) 3.740s (-2.1%) 0.785s 9 1.00x
▲ Vercel Express 2.995s (+5.3% 🔺) 3.886s (+3.3%) 0.892s 8 1.01x
▲ Vercel Nitro 3.135s (+13.8% 🔺) 3.925s (+6.4% 🔺) 0.790s 8 1.06x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 2.577s (-1.5%) 3.021s (~) 0.444s 10 1.00x
💻 Local Express 2.616s (-2.7%) 3.010s (~) 0.394s 10 1.01x
💻 Local Next.js (Turbopack) 2.630s (-2.2%) 3.022s (~) 0.392s 10 1.02x
🐘 Postgres Nitro 11.996s (+11.3% 🔺) 12.387s (+12.2% 🔺) 0.391s 3 4.65x
🐘 Postgres Express 12.345s (+13.8% 🔺) 12.707s (+14.6% 🔺) 0.362s 3 4.79x
🐘 Postgres Next.js (Turbopack) 14.005s (+27.6% 🔺) 14.392s (+23.1% 🔺) 0.387s 3 5.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.035s (+2.7%) 3.922s (+7.4% 🔺) 0.887s 8 1.00x
▲ Vercel Next.js (Turbopack) 3.119s (+3.0%) 3.783s (-0.7%) 0.664s 8 1.03x
▲ Vercel Nitro 3.157s (+0.7%) 3.952s (+3.5%) 0.795s 8 1.04x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 7.283s (-4.2%) 7.978s (-7.2% 🟢) 0.695s 4 1.00x
💻 Local Next.js (Turbopack) 7.630s (~) 8.453s (-1.9%) 0.823s 4 1.05x
💻 Local Express 7.907s (+1.1%) 8.914s (+1.2%) 1.007s 4 1.09x
🐘 Postgres Nitro 49.781s (-3.2%) 50.201s (-3.7%) 0.420s 1 6.83x
🐘 Postgres Express 53.042s (+3.0%) 53.131s (+2.0%) 0.089s 1 7.28x
🐘 Postgres Next.js (Turbopack) 58.645s (+7.9% 🔺) 59.197s (+7.3% 🔺) 0.552s 1 8.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.515s (+5.0% 🔺) 4.079s (+3.6%) 0.564s 8 1.00x
▲ Vercel Nitro 3.799s (+22.4% 🔺) 4.538s (+20.3% 🔺) 0.739s 7 1.08x
▲ Vercel Next.js (Turbopack) 4.062s (+14.4% 🔺) 4.697s (+8.3% 🔺) 0.635s 7 1.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.143s (-2.5%) 1.003s (~) 0.016s (-8.0% 🟢) 1.027s (-0.6%) 0.883s 10 1.00x
💻 Local Express 0.181s (-3.3%) 0.992s (~) 0.014s (-1.4%) 1.020s (~) 0.839s 10 1.26x
💻 Local Nitro 0.190s (+7.4% 🔺) 0.991s (~) 0.014s (+0.7%) 1.019s (~) 0.829s 10 1.33x
🐘 Postgres Next.js (Turbopack) 1.182s (+65.2% 🔺) 1.428s (+46.0% 🔺) 0.000s (NaN%) 1.715s (+53.8% 🔺) 0.533s 10 8.25x
🐘 Postgres Nitro 2.337s (+57.1% 🔺) 2.706s (+63.3% 🔺) 0.000s (NaN%) 3.015s (+49.8% 🔺) 0.679s 10 16.32x
🐘 Postgres Express 2.400s (-1.6%) 2.645s (+1.4%) 0.000s (+Infinity% 🔺) 3.017s (~) 0.618s 10 16.76x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.176s (+3.5%) 3.328s (-2.0%) 0.263s (+84.4% 🔺) 4.150s (+3.1%) 0.974s 10 1.00x
▲ Vercel Next.js (Turbopack) 3.182s (+2.9%) 3.485s (+6.2% 🔺) 0.165s (-10.5% 🟢) 4.153s (+4.3%) 0.972s 10 1.00x
▲ Vercel Express 3.358s (+9.6% 🔺) 3.699s (+10.1% 🔺) 0.200s (-6.4% 🟢) 4.468s (+9.7% 🔺) 1.110s 10 1.06x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 6/12
🐘 Postgres Next.js (Turbopack) 6/12
▲ Vercel Express 6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 11/12
Next.js (Turbopack) 💻 Local 11/12
Nitro 💻 Local 10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 479 0 38 517
✅ 💻 Local Development 438 0 32 470
✅ 📦 Local Production 438 0 32 470
✅ 🐘 Local Postgres 438 0 32 470
✅ 🪟 Windows 47 0 0 47
❌ 🌍 Community Worlds 31 169 0 200
✅ 📋 Other 129 0 12 141
Total 2000 169 146 2315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 43 0 4
✅ example 43 0 4
✅ express 43 0 4
✅ fastify 43 0 4
✅ hono 43 0 4
✅ nextjs-turbopack 46 0 1
✅ nextjs-webpack 46 0 1
✅ nitro 43 0 4
✅ nuxt 43 0 4
✅ sveltekit 43 0 4
✅ vite 43 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 47 0 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 5 42 0
✅ redis-dev 3 0 0
❌ redis 5 42 0
✅ starter-dev 3 0 0
❌ starter 4 43 0
✅ turso-dev 3 0 0
❌ turso 5 42 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 43 0 4
✅ e2e-local-postgres-nest-stable 43 0 4
✅ e2e-local-prod-nest-stable 43 0 4

📋 View full workflow run

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive OpenTelemetry instrumentation to improve workflow observability across HTTP requests, storage operations, event loading, and queue processing.

Changes:

  • Added tracing to HTTP requests in world-vercel with detailed attributes for method, endpoint, status, and parse format
  • Instrumented all storage operations (runs, steps, events, hooks) with automatic span wrapping
  • Added queue timing breakdown tracking (deserialize, execution, serialize times) in step handler
  • Added event loading tracing with pagination metrics

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pnpm-lock.yaml Added @opentelemetry/api 1.9.0 as devDependency for world-vercel
packages/world-vercel/package.json Added @opentelemetry/api as optional peer dependency
packages/world-vercel/src/telemetry.ts New minimal telemetry module with trace helpers and semantic conventions
packages/world-vercel/src/utils.ts Wrapped makeRequest with HTTP tracing including nested parse and validate spans
packages/world-vercel/src/storage.ts Added instrumentObject wrapper to all storage methods
packages/world-vercel/src/instrumentObject.ts New utility to automatically wrap object methods with tracing
packages/core/src/telemetry/semantic-conventions.ts Added semantic conventions for world/storage, events, serialization, and queue timing
packages/core/src/telemetry.ts Exported instrumentObject function (no changes in diff, already existed)
packages/core/src/runtime/step-handler.ts Added timing measurements for deserialize, execution, and serialize phases
packages/core/src/runtime/helpers.ts Wrapped getAllWorkflowRunEvents with tracing and pagination metrics
.changeset/improve-otel-tracing.md Added changeset describing the improvements
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 253 to 292
export const WorldParseBytes = SemanticConvention<number>('world.parse.bytes');

// Event loading attributes

/** Number of pagination pages loaded when fetching workflow events */
export const WorkflowEventsPagesLoaded = SemanticConvention<number>(
'workflow.events.pages_loaded'
);

// Serialization attributes

/** Format used for serialization (e.g., 'devalue') */
export const SerializeFormat = SemanticConvention<string>('serialize.format');

/** Size in bytes of the serialized data */
export const SerializeBytes = SemanticConvention<number>('serialize.bytes');

/** Format used for deserialization (e.g., 'devalue') */
export const DeserializeFormat =
SemanticConvention<string>('deserialize.format');

/** Size in bytes of the data being deserialized */
export const DeserializeBytes = SemanticConvention<number>('deserialize.bytes');

// Queue timing breakdown attributes

/** Time spent deserializing the queue message in milliseconds */
export const QueueDeserializeTimeMs = SemanticConvention<number>(
'queue.deserialize_time_ms'
);

/** Time spent executing the handler logic in milliseconds */
export const QueueExecutionTimeMs = SemanticConvention<number>(
'queue.execution_time_ms'
);

/** Time spent serializing the response in milliseconds */
export const QueueSerializeTimeMs = SemanticConvention<number>(
'queue.serialize_time_ms'
);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The semantic conventions WorldParseBytes, SerializeFormat, SerializeBytes, DeserializeFormat, and DeserializeBytes are defined but never used in the codebase. Consider either implementing these attributes where appropriate (e.g., adding WorldParseBytes to the WORLD.parse span in utils.ts, and serialization attributes in the serialization code) or removing them if they're not needed yet. Unused definitions can confuse maintainers about the expected instrumentation coverage.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed the unused semantic conventions (WorldParseBytes, SerializeFormat, SerializeBytes, DeserializeFormat, DeserializeBytes) in commit 161cd67.

Comment on lines 85 to 102
/** HTTP method used in World storage request */
export const WorldHttpMethod = SemanticConvention<string>('world.http.method');

/** API endpoint path for World storage request */
export const WorldHttpEndpoint = SemanticConvention<string>(
'world.http.endpoint'
);

/** HTTP status code from World storage request */
export const WorldHttpStatus = SemanticConvention<number>('world.http.status');

/** Format used for parsing response body (cbor or json) */
export const WorldParseFormat = SemanticConvention<'cbor' | 'json'>(
'world.parse.format'
);

/** Size in bytes of the parsed response body */
export const WorldParseBytes = SemanticConvention<number>('world.parse.bytes');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The semantic conventions WorldHttpMethod, WorldHttpEndpoint, WorldHttpStatus, WorldParseFormat, and WorldParseBytes are duplicated between this file and packages/core/src/telemetry/semantic-conventions.ts (lines 237-253). This duplication can lead to inconsistencies and maintenance issues. Consider importing these from @workflow/core instead, or if avoiding circular dependencies is necessary, document this duplication clearly and ensure the definitions remain synchronized.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added documentation at the top of world-vercel/telemetry.ts explaining the intentional duplication due to circular dependency avoidance. The comment now instructs maintainers to keep the conventions synchronized.

Comment on lines +236 to +249
const deserializeStartTime = Date.now();
const ops: Promise<void>[] = [];
const hydratedInput = hydrateStepArguments(
step.input,
ops,
workflowRunId
);
const deserializeTimeMs = Date.now() - deserializeStartTime;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The deserializeTimeMs measurement (line 243) captures only the synchronous portion of hydrateStepArguments. However, hydrateStepArguments may initiate async operations that are added to the 'ops' array (line 237), and these operations are executed later via Promise.all(ops) at line 294. This means the deserialize timing doesn't fully capture all deserialization work. Consider documenting this limitation or restructuring to measure the complete deserialization time including async operations.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a comment documenting this limitation. The timing intentionally captures only the synchronous hydration - async operations like stream loading happen in background and their timing is included in the overall step execution time tracked separately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we also need separate spans explicitly for serializing and deserializing etc. rather than just including it as an attribute

Comment on lines +50 to +72
return tracer.startActiveSpan(spanName, opts, async (span) => {
try {
const result = await fn(span);
span.setStatus({ code: otel.SpanStatusCode.OK });
return result;
} catch (e) {
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: (e as Error).message,
});
throw e;
} finally {
span.end();
}
});
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The trace function in world-vercel doesn't handle WorkflowSuspension errors like the @workflow/core version does (see packages/core/src/telemetry.ts lines 105, 117-133). WorkflowSuspension is not an actual error but an algebraic effect for suspension, and should be marked with SpanStatusCode.OK. While world-vercel might not encounter WorkflowSuspension errors directly, if any world operations throw this, the span status will incorrectly show ERROR. Consider adding similar handling or documenting why it's not needed.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added documentation explaining why WorkflowSuspension handling isn't needed - world-vercel operates at the HTTP layer and never encounters workflow suspension effects (those only occur in the workflow VM context in @workflow/core).

);
}
return trace(
`WORLD.http ${method} ${endpoint}`,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The span name includes the full endpoint path which contains dynamic segments like run IDs (e.g., /v2/runs/${id}). This creates high cardinality in span names, which can cause performance issues in observability backends and make metrics aggregation difficult. Consider parameterizing the endpoint in the span name (e.g., replacing IDs with placeholders like /v2/runs/{id}) or moving the full endpoint to an attribute only. The endpoint is already captured in the WorldHttpEndpoint attribute.

Suggested change
`WORLD.http ${method} ${endpoint}`,
`WORLD.http ${method}`,

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in commit ba58e13. The span name is now just HTTP {method} without the endpoint path. Per OTEL conventions for HTTP client spans, the recommended span name format is just the method. The full URL is captured in the url.full attribute for filtering/searching.

pranaygp and others added 5 commits February 4, 2026 15:54
- Add tracing to HTTP requests in world-vercel with method, endpoint, and status attributes
- Add tracing to storage operations (runs, steps, events, hooks)
- Add tracing to event loading with event count and pages loaded attributes
- Add queue timing breakdown attributes (deserialize, execution, serialize times)
- Add new semantic conventions for world/storage, events, serialization, and queue breakdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace custom world.http.* attributes with standard OTEL HTTP conventions
  (http.request.method, http.response.status_code, url.full, server.address, server.port)
- Replace custom queue.* attributes with standard OTEL messaging conventions
  (messaging.system, messaging.destination.name, messaging.message.id, messaging.operation.type)
- Add error.type attribute for HTTP error spans
- Add span.recordException() for proper error recording
- Namespace workflow-specific attributes under workflow.* prefix
- Fix span naming: use "HTTP {method}" instead of "WORLD.http {method} {endpoint}"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused semantic conventions (SerializeFormat, SerializeBytes, DeserializeFormat, DeserializeBytes, WorldParseBytes)
- Add documentation explaining intentional duplication in world-vercel/telemetry.ts
- Add documentation explaining why WorkflowSuspension isn't handled in world-vercel
- Add comment about deserialize timing measurement limitation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- world.runs, world.steps, world.events, world.hooks (storage)
- world.parse, world.validate (HTTP parsing)
- http {method} (HTTP client spans)
- workflow.run, workflow.start, workflow.loadEvents
- step {name}, hook.resume

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@pranaygp pranaygp force-pushed the pranaygp/improve-otel branch from f8ce3d7 to b142e22 Compare February 4, 2026 23:54
@pranaygp pranaygp merged commit 79e988f into main Feb 5, 2026
96 checks passed
@pranaygp pranaygp deleted the pranaygp/improve-otel branch February 5, 2026 00:12
Sign up for free to 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