Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

@TooTallNate@pranaygp@VaguelySerious@adriandlam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

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

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

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

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

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

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

@TooTallNate@pranaygp@VaguelySerious@adriandlam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

@TooTallNate@pranaygp@VaguelySerious@adriandlam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

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

Invoke the endpoints health check in web o11y and render result in toolbar - #615

Closed
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar
Closed

Invoke the endpoints health check in web o11y and render result in toolbar#615
TooTallNate wants to merge 3 commits into
mainfrom
12-15-invoke_the_endpoints_health_check_in_web_o11y_and_render_result_in_toolbar

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 15, 2025

Copy link
Copy Markdown
Member

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot

changeset-botBot commented Dec 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
NameType
@workflow/webPatch
@workflow/cliPatch
workflowPatch
@workflow/world-testingPatch
@workflow/aiPatch

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 Dec 15, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
✅ 💻 Local Development26208270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
❌ 🪟 Windows027027
❌ 🌍 Community Worlds87330120
Total115960351254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • 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 endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
❌ 🪟 Windows
AppPassedFailedSkipped
❌ nextjs-turbopack0270
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
❌ starter-dev030
❌ starter0270
✅ turso-dev300
❌ turso2610

📋 View full workflow run

@github-actions

github-actionsBot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.032s (-28.4% 🟢)1.007s (~)0.975s101.00x
💻 LocalNext.js (Turbopack)0.039s (-4.2%)1.018s (~)0.979s101.22x
🌐 RedisNext.js (Turbopack)0.039s (-7.6% 🟢)1.017s (~)0.978s101.23x
🌐 StarterNext.js (Turbopack)0.039s (-3.2%)1.015s (~)0.975s101.24x
💻 LocalExpress0.046s (+7.3% 🔺)1.007s (~)0.961s101.43x
🌐 TursoNext.js (Turbopack)0.094s (-12.6% 🟢)1.013s (~)0.919s102.96x
🌐 MongoDBNext.js (Turbopack)0.121s (+102.9% 🔺)1.015s (~)0.894s103.80x
🐘 PostgresNext.js (Turbopack)0.303s (+24.0% 🔺)1.021s (~)0.717s109.54x
🐘 PostgresNitro0.306s (+2.6%)1.011s (~)0.705s109.62x
🐘 PostgresExpress0.336s (-11.5% 🟢)1.014s (~)0.678s1010.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.517s (-22.1% 🟢)1.581s (+2.5%)1.065s101.00x
▲ VercelNext.js (Turbopack)0.518s (-29.5% 🟢)1.638s (+1.9%)1.120s101.00x
▲ VercelNitro0.526s (-10.4% 🟢)1.408s (-0.6%)0.882s101.02x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.080s (-3.0%)2.006s (~)0.926s101.00x
💻 LocalNext.js (Turbopack)1.094s (~)2.011s (~)0.917s101.01x
🌐 StarterNext.js (Turbopack)1.097s (+1.0%)2.010s (~)0.913s101.02x
🌐 RedisNext.js (Turbopack)1.097s (~)2.012s (~)0.914s101.02x
💻 LocalExpress1.117s (~)2.008s (~)0.891s101.03x
🌐 MongoDBNext.js (Turbopack)1.279s (-2.4%)2.014s (~)0.735s101.18x
🌐 TursoNext.js (Turbopack)1.296s (-1.8%)2.012s (~)0.715s101.20x
🐘 PostgresNext.js (Turbopack)1.864s (+0.9%)2.015s (~)0.151s101.73x
🐘 PostgresNitro2.163s (+0.7%)3.014s (~)0.852s102.00x
🐘 PostgresExpress2.188s (-1.9%)3.017s (~)0.829s102.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.603s (+3.3%)3.567s (-0.9%)0.964s101.00x
▲ VercelExpress2.739s (+10.8% 🔺)3.653s (+6.6% 🔺)0.915s101.05x
▲ VercelNext.js (Turbopack)2.780s (+6.5% 🔺)3.788s (+3.0%)1.008s101.07x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.533s (-2.4%)11.012s (~)0.479s51.00x
🌐 StarterNext.js (Turbopack)10.598s (~)11.011s (~)0.413s51.01x
💻 LocalNext.js (Turbopack)10.643s (~)11.017s (~)0.373s51.01x
🌐 RedisNext.js (Turbopack)10.689s (~)11.019s (~)0.329s51.01x
💻 LocalExpress10.810s (~)11.014s (~)0.204s51.03x
🌐 TursoNext.js (Turbopack)12.194s (~)13.026s (~)0.832s51.16x
🌐 MongoDBNext.js (Turbopack)12.233s (~)13.028s (~)0.795s51.16x
🐘 PostgresNext.js (Turbopack)15.310s (+0.6%)16.031s (~)0.721s51.45x
🐘 PostgresNitro20.325s (-0.8%)21.029s (~)0.703s51.93x
🐘 PostgresExpress20.387s (+0.9%)21.036s (~)0.649s51.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.169s (+0.7%)22.161s (+2.1%)0.992s51.00x
▲ VercelNext.js (Turbopack)21.322s (+0.6%)22.296s (+1.2%)0.975s51.01x
▲ VercelNitro21.362s (+0.9%)21.864s (+0.6%)0.503s51.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.347s (+0.9%)2.008s (~)0.661s151.00x
💻 LocalNitro1.355s (-4.4%)2.006s (~)0.651s151.01x
🌐 RedisNext.js (Turbopack)1.356s (~)2.010s (~)0.654s151.01x
💻 LocalNext.js (Turbopack)1.380s (-1.0%)2.012s (~)0.633s151.02x
💻 LocalExpress1.421s (+0.7%)2.006s (~)0.586s151.05x
🐘 PostgresNext.js (Turbopack)1.890s (-3.7%)2.022s (-6.4% 🟢)0.132s151.40x
🌐 MongoDBNext.js (Turbopack)2.114s (-0.7%)3.017s (~)0.903s101.57x
🐘 PostgresNitro2.114s (-13.3% 🟢)2.510s (-16.6% 🟢)0.396s121.57x
🌐 TursoNext.js (Turbopack)2.204s (-0.9%)3.013s (~)0.809s101.64x
🐘 PostgresExpress2.373s (-4.2%)3.012s (~)0.639s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (+8.8% 🔺)3.766s (+7.0% 🔺)0.963s81.00x
▲ VercelNitro2.845s (+8.0% 🔺)3.681s (+0.8%)0.836s91.02x
▲ VercelNext.js (Turbopack)2.926s (+4.3%)3.840s (+6.3% 🔺)0.914s81.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.009s (-9.5% 🟢)2.410s (-23.9% 🟢)0.400s131.00x
💻 LocalNext.js (Turbopack)2.179s (-2.2%)3.131s (-1.4%)0.951s101.08x
💻 LocalExpress2.242s (~)3.191s (~)0.949s101.12x
🌐 RedisNext.js (Turbopack)2.470s (~)3.012s (~)0.542s101.23x
🌐 StarterNext.js (Turbopack)2.488s (+1.1%)3.009s (~)0.520s101.24x
🐘 PostgresNext.js (Turbopack)2.751s (+4.5%)3.032s (~)0.281s101.37x
🐘 PostgresNitro2.876s (-1.8%)3.110s (-3.1%)0.235s101.43x
🐘 PostgresExpress3.095s (+6.1% 🔺)3.781s (+21.5% 🔺)0.686s81.54x
🌐 MongoDBNext.js (Turbopack)4.697s (+1.3%)5.180s (~)0.483s62.34x
🌐 TursoNext.js (Turbopack)4.723s (+1.1%)5.182s (~)0.459s62.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.977s (-9.0% 🟢)3.736s (-4.2%)0.759s91.00x
▲ VercelNitro3.248s (-22.6% 🟢)4.029s (-17.2% 🟢)0.781s81.09x
▲ VercelNext.js (Turbopack)3.319s (-11.8% 🟢)3.983s (-11.0% 🟢)0.664s81.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.355s (-3.7%)2.006s (~)0.651s151.00x
🌐 RedisNext.js (Turbopack)1.364s (+1.0%)2.010s (~)0.646s151.01x
🌐 StarterNext.js (Turbopack)1.393s (+2.4%)2.007s (~)0.615s151.03x
💻 LocalNext.js (Turbopack)1.405s (~)2.014s (~)0.609s151.04x
💻 LocalExpress1.416s (-0.7%)2.006s (~)0.590s151.04x
🐘 PostgresNext.js (Turbopack)1.658s (-1.3%)2.012s (~)0.354s151.22x
🐘 PostgresNitro1.664s (-18.2% 🟢)2.009s (-29.1% 🟢)0.345s151.23x
🐘 PostgresExpress1.913s (+18.4% 🔺)2.011s (~)0.098s151.41x
🌐 MongoDBNext.js (Turbopack)2.115s (-2.0%)3.014s (~)0.899s101.56x
🌐 TursoNext.js (Turbopack)2.223s (+0.7%)3.012s (~)0.790s101.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.632s (+1.1%)3.781s (+7.7% 🔺)1.148s81.00x
▲ VercelNext.js (Turbopack)2.685s (-3.0%)3.775s (+3.7%)1.091s81.02x
▲ VercelNitro2.709s (+5.0%)3.618s (+3.4%)0.909s91.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.107s (-6.2% 🟢)3.081s (-3.4%)0.974s101.00x
💻 LocalExpress2.207s (-2.5%)3.159s (-1.8%)0.952s101.05x
💻 LocalNext.js (Turbopack)2.286s (+0.8%)3.201s (+1.1%)0.915s101.09x
🌐 RedisNext.js (Turbopack)2.451s (-0.6%)3.011s (~)0.560s101.16x
🐘 PostgresNext.js (Turbopack)2.466s (-6.4% 🟢)3.018s (~)0.552s101.17x
🌐 StarterNext.js (Turbopack)2.471s (-1.5%)3.009s (~)0.537s101.17x
🐘 PostgresNitro2.639s (-10.7% 🟢)3.037s (-2.6%)0.398s101.25x
🐘 PostgresExpress2.772s (-7.1% 🟢)3.015s (-6.3% 🟢)0.243s101.32x
🌐 TursoNext.js (Turbopack)4.645s (-1.9%)5.180s (~)0.535s62.20x
🌐 MongoDBNext.js (Turbopack)4.683s (+0.8%)5.181s (~)0.497s62.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.004s (+0.9%)3.805s (+7.8% 🔺)0.801s81.00x
▲ VercelNitro3.063s (-0.6%)3.546s (-3.2%)0.483s91.02x
▲ VercelNext.js (Turbopack)3.143s (+2.3%)3.847s (+4.9%)0.704s81.05x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.108s (-38.6% 🟢)0.999s (+0.7%)0.014s (-13.9% 🟢)1.020s (~)0.912s101.00x
🌐 StarterNext.js (Turbopack)0.127s (+1.8%)1.005s (~)0.000s (NaN%)1.011s (~)0.884s101.17x
💻 LocalNext.js (Turbopack)0.137s (-1.6%)1.003s (~)0.017s (-22.7% 🟢)1.027s (-0.5%)0.891s101.27x
🌐 RedisNext.js (Turbopack)0.139s (-7.5% 🟢)1.004s (~)0.000s (+Infinity% 🔺)1.013s (~)0.875s101.28x
💻 LocalExpress0.182s (+1.3%)0.994s (~)0.018s (+1.7%)1.026s (~)0.844s101.68x
🌐 TursoNext.js (Turbopack)0.493s (-6.4% 🟢)0.956s (+3.3%)0.000s (-100.0% 🟢)1.013s (~)0.521s104.56x
🌐 MongoDBNext.js (Turbopack)0.500s (-5.3% 🟢)0.945s (+2.5%)0.000s (-100.0% 🟢)1.014s (~)0.514s104.62x
🐘 PostgresNext.js (Turbopack)1.272s (+9.1% 🔺)1.771s (+6.3% 🔺)0.000s (NaN%)2.018s (+11.0% 🔺)0.746s1011.77x
🐘 PostgresNitro1.399s (-40.0% 🟢)1.695s (-37.4% 🟢)0.000s (+Infinity% 🔺)2.012s (-33.2% 🟢)0.612s1012.94x
🐘 PostgresExpress2.295s (-3.9%)2.747s (+3.5%)0.000s (+Infinity% 🔺)3.016s (~)0.721s1021.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.531s (-1.6%)3.079s (+2.1%)0.539s (+107.2% 🔺)4.103s (+11.7% 🔺)1.572s101.00x
▲ VercelNext.js (Turbopack)2.623s (+4.0%)3.247s (+3.8%)0.456s (+72.8% 🔺)4.147s (+9.6% 🔺)1.524s101.04x
▲ VercelNitro2.713s (+12.2% 🔺)3.265s (+11.4% 🔺)0.360s (-9.7% 🟢)4.123s (+11.1% 🔺)1.410s101.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro8/8
🐘 PostgresNext.js (Turbopack)8/8
▲ VercelExpress7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)💻 Local4/8
Nitro💻 Local8/8
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

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
async function checkEndpointHealth(
baseUrl: string,
- endpoint: 'flow' | 'step'+ endpoint: 'flow' | 'step',+ signal?: AbortSignal
): Promise<{ success: boolean; message: string }> {
try {
const url = new URL(
`/.well-known/workflow/v1/${endpoint}?__health`,
baseUrl
);
+ // Combine provided signal with timeout signal+ const timeoutSignal = AbortSignal.timeout(5000);+ const combinedSignal = signal+ ? AbortSignal.any([signal, timeoutSignal])+ : timeoutSignal;+
const response = await fetch(url.toString(), {
method: 'POST',
- // Short timeout for health checks- signal: AbortSignal.timeout(5000),+ signal: combinedSignal,
});
if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
return;
}
+ // Track whether the component is still mounted+ let isMounted = true;+ const abortController = new AbortController();+
// Otherwise, perform the health check
const performHealthCheck = async () => {
+ if (!isMounted || abortController.signal.aborted) return;
setIsChecking(true);
// Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
const baseUrl = `http://localhost:${port}`;
const [flowResult, stepResult] = await Promise.all([
- checkEndpointHealth(baseUrl, 'flow'),- checkEndpointHealth(baseUrl, 'step'),+ checkEndpointHealth(baseUrl, 'flow', abortController.signal),+ checkEndpointHealth(baseUrl, 'step', abortController.signal),
]);
+ // Only update state if the component is still mounted+ if (!isMounted || abortController.signal.aborted) return;+
const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
};
performHealthCheck();
++ // Cleanup function: cancel pending requests and mark component as unmounted+ return () => {+ isMounted = false;+ abortController.abort();+ };
}, [config]);
const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
FileDescription
packages/web/src/components/display-utils/endpoints-health-status.tsxNew component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsxIntegrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.tsAdds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.mdDocuments CORS headers addition to core package
.changeset/empty-yaks-follow.mdDocuments endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/runtime.ts Outdated
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return`${config.backend||'local'}-${config.port||'3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
constkeyObj: Record<string,unknown>={
backend: config.backend||'local',
port: config.port||'3000',
};
// Add backend-specific fields
if('env'inconfig&&config.env)keyObj.env=config.env;
if('project'inconfig&&config.project)keyObj.project=config.project;
if('team'inconfig&&config.team)keyObj.team=config.team;
if('dataDir'inconfig&&config.dataDir)keyObj.dataDir=config.dataDir;
if('postgresUrl'inconfig&&config.postgresUrl)keyObj.postgresUrl=config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
returnJSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();

CopilotAIDec 15, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

CopilotAI commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp

Copy link
Copy Markdown
Contributor

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

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.

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

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.

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

@VaguelySeriousVaguelySerious 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.

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

@VaguelySeriousVaguelySerious 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.

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious

Copy link
Copy Markdown
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
constport=config.port||'3000';
constbaseUrl=`http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
letbaseUrl: string;
constbackend=config.backend||'local';
if(backend==='local'){
constport=config.port||'3000';
baseUrl=`http://localhost:${port}`;
}else{
// For deployed backends, use the current window origin
baseUrl=typeofwindow!=='undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails:EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Closing. Will take another stab at this using the queue based health check in #743 lands.

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.

6 participants

@TooTallNate@pranaygp@VaguelySerious@adriandlam