Skip to content

fix: register instrumentation in rsc entry and put error handler in global scope#330

Merged
james-elicx merged 4 commits intomainfrom
j-branch-2
Mar 7, 2026
Merged

fix: register instrumentation in rsc entry and put error handler in global scope#330
james-elicx merged 4 commits intomainfrom
j-branch-2

Conversation

@james-elicx
Copy link
Collaborator

@james-elicx james-elicx commented Mar 7, 2026

Instrumentation.ts: App Router support + Cloudflare Workers e2e tests

Completes instrumentation.ts support for App Router, and adds e2e coverage that runs against the production Cloudflare Workers build.

What changed

Core: register() baked into the RSC entry

Previously runInstrumentation() was called from configureServer() in the host Node.js process. This broke with @cloudflare/vite-plugin because the plugin runs the RSC environment inside a miniflare Worker subprocess — a separate process from the host, with its own globalThis. Calling register() in the host meant it never ran in the same process as request handling.

The fix: generateRscEntry() now accepts an instrumentationPath parameter and emits register() as a top-level await at module evaluation time, before any requests are served. This runs in whatever process/environment evaluates the RSC entry — the Worker subprocess in Cloudflare dev, the RSC Vite environment with @vitejs/plugin-rsc, or the built Worker binary in production.

configureServer() still calls runInstrumentation() for Pages Router only, where there is no RSC entry and @cloudflare/vite-plugin is never present.

onRequestError handler storage

The handler is now stored on globalThis.__VINEXT_onRequestErrorHandler__ instead of a module-level variable. This makes it visible across the RSC and SSR Vite environment module graphs (separate instances, shared globalThis) and inside the Cloudflare Worker (single global scope). The key is declared in global.d.ts for type safety.

e2e coverage

  • tests/e2e/app-router/instrumentation.spec.ts — new spec covering register() startup, onRequestError() on route throws, multiple error capture, and no false positives on success. Uses relative URLs + Playwright baseURL so it's portable across projects.
  • The cloudflare-workers Playwright project now picks up this spec via testMatch, running the same assertions against the production wrangler build on port 4176.
  • app-router-cloudflare example gets instrumentation.ts, instrumentation-state.ts, /api/instrumentation-test, and /api/error-route to support the tests.

Unit tests

tests/instrumentation.test.ts gets a beforeEach cleanup of globalThis.__VINEXT_onRequestErrorHandler__ to prevent handler bleed between tests after the storage change.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 7, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@330

commit: 94c1d0b

@github-actions
Copy link

github-actions bot commented Mar 7, 2026

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

@james-elicx james-elicx force-pushed the j-branch-2 branch 7 times, most recently from 1ab76ea to 8964410 Compare March 7, 2026 22:15
@james-elicx james-elicx changed the title test: instrumentation fix: register instrumentation in rsc entry and put error handler in global scope Mar 7, 2026
@james-elicx
Copy link
Collaborator Author

/bigbonk review this pr please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

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

Review: instrumentation.ts App Router support + Cloudflare Workers e2e

The architectural approach is sound — baking register() into the RSC entry as a top-level await is the right fix for the Cloudflare Worker subprocess problem, and using globalThis for cross-environment handler storage is correct given that RSC/SSR environments share a globalThis within the same process.

However, there's one blocking issue and several things worth addressing.


Blocking: register() called twice for App Router dev

configureServer() in index.ts:2526 still unconditionally calls runInstrumentation() when instrumentationPath is set — there's no hasAppDir guard. Meanwhile, generateRscEntry() now also calls register() at module evaluation time.

For App Router dev without @cloudflare/vite-plugin, both run in the same Node.js process, so register() fires twice. For App Router dev with @cloudflare/vite-plugin, register() runs once in the host (via ssrLoadModule) and once in the Worker subprocess — the host call is wasted since it runs in a different process from request handling.

The PR description says "configureServer() still calls runInstrumentation() for Pages Router only" but the code doesn't enforce that. This needs a guard:

if (instrumentationPath && !hasAppDir) {
  runInstrumentation(server, instrumentationPath).catch((err) => {
    console.error("[vinext] Instrumentation error:", err);
  });
}

Or if you want to support hybrid apps (both pages/ and app/ dirs), you need a more nuanced check. But as-is, App Router users get double register() calls.


Mixed tabs and spaces

Several files in this PR have lines with tab indentation where the rest of the file uses spaces:

  • packages/vinext/src/server/instrumentation.ts lines 128 and 152 — tabs instead of spaces
  • packages/vinext/src/global.d.ts lines 247, 258, 259 — tabs instead of spaces (line 247 was an existing line that got reformatted from spaces to tabs)

The __VINEXT_DEFAULT_LOCALE__ line (global.d.ts:247) was previously indented with spaces and is now indented with a tab — this is an unintentional whitespace change in existing code.

tests/fixtures/app-basic/instrumentation-state.ts — leading space indentation

The entire body of this file (lines 5-39) is indented with a single leading space instead of being flush-left. Compare with examples/app-router-cloudflare/instrumentation-state.ts which is correctly flush-left. Looks like an editor artifact.

pages/index.tsx in app-router-cloudflare — why?

A new pages/index.tsx was added to the App Router Cloudflare example. This seems unrelated to instrumentation. If it's needed for something (maybe a test?), the commit message should explain it. If not, it shouldn't be here — adding a Pages Router index page to an App Router example is confusing and could cause routing conflicts with app/page.tsx.

prod-server.ts doesn't call reportRequestError

dev-server.ts (Pages Router) calls reportRequestError() when SSR throws (line 855). But prod-server.ts has no equivalent — it doesn't import or call reportRequestError at all. This means instrumentation onRequestError() only fires in Pages Router dev, not production.

For App Router this isn't an issue (the RSC entry handles it), but for Pages Router prod, errors go unreported. This isn't a regression from this PR (it was already missing), but since the PR is completing instrumentation support, it's worth noting. If you want to address it separately, a follow-up issue would be appropriate.

E2e test timing — setTimeout fragility

The instrumentation e2e tests use await new Promise((resolve) => setTimeout(resolve, 200)) to wait for reportRequestError to complete. This works but is fragile — under load or slow CI, 200ms may not be enough. Consider polling the /api/instrumentation-test endpoint in a retry loop instead:

await expect.poll(async () => {
  const res = await request.get("/api/instrumentation-test");
  const data = await res.json();
  return data.errors.length;
}, { timeout: 5000 }).toBeGreaterThanOrEqual(1);

Playwright's expect.poll() is designed for exactly this pattern.

Memory growth in instrumentation-state.ts

The capturedErrors array grows unboundedly. For test fixtures this is fine, but the same pattern in the examples/app-router-cloudflare/ directory (which gets deployed to production Workers) means the Worker's memory grows with every error. In practice, Workers have short lifetimes so this is unlikely to matter, but it's worth a comment noting this is test-only infrastructure that shouldn't be used as a pattern for real instrumentation.


Summary

The core design (RSC entry await register() + globalThis handler storage) is correct and well-reasoned. The main issue to fix is the double-registration in configureServer for App Router. The rest are nits/suggestions.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 7, 2026

Review posted on PR #330 with "request changes." Here's a summary of the findings:

Blocking issue:

  • configureServer() in index.ts:2526 still unconditionally calls runInstrumentation() for all projects, not just Pages Router. This means register() gets called twice for App Router dev — once from configureServer via ssrLoadModule() and once from the generated RSC entry's top-level await. Needs a !hasAppDir guard.

Non-blocking issues:

  • Mixed tabs/spaces in instrumentation.ts (lines 128, 152) and global.d.ts (lines 247, 258, 259)
  • tests/fixtures/app-basic/instrumentation-state.ts has a leading space indent on all function bodies
  • Unexplained pages/index.tsx added to the App Router Cloudflare example
  • prod-server.ts still doesn't call reportRequestError (Pages Router prod gap — not a regression but worth tracking)
  • E2e tests use fragile setTimeout(200) instead of Playwright's expect.poll() for async assertions
  • Unbounded capturedErrors array in deployed example code (minor, Workers have short lifetimes)

github run

@james-elicx
Copy link
Collaborator Author

/bigbonk re-review please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

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

Re-review: instrumentation.ts App Router support + Cloudflare Workers e2e

The blocking issue from the first review has been addressed. CI is all green (Lint, Typecheck, Vitest, all 5 Playwright E2E projects pass).

Blocking issue — resolved

Double register() call for App Router dev — Fixed in commit 8d80bb2f. configureServer() now guards with if (instrumentationPath && !hasAppDir) at index.ts:2532, matching the PR description. App Router gets register() only from the RSC entry's top-level await; Pages Router gets it only from configureServer(). Correct.

Previous non-blocking feedback — status

Issue Status
pages/index.tsx in app-router-cloudflare example Not present in the diff — the previous review flagged a phantom file that doesn't exist in the PR. No action needed.
Mixed tabs/spaces in instrumentation.ts and global.d.ts Still present. instrumentation.ts:128 and instrumentation.ts:152 use tab indentation where surrounding lines use spaces. global.d.ts:247,258,259 also have tabs instead of spaces. The linter passes so this is likely a project-level style choice, but it's inconsistent with the rest of these files.
instrumentation-state.ts (app-basic fixture) leading space indent Still present. Lines 5-39 are indented with a single leading space instead of being flush-left. Compare with the examples/ version which is correct. Minor, but looks like an editor artifact.
setTimeout instead of expect.poll() in e2e tests Not addressed. Still uses setTimeout(200) / setTimeout(400). Functional and CI passes, but expect.poll() would be more robust under load.
prod-server.ts missing reportRequestError for Pages Router Not addressed, acknowledged as pre-existing. This is not a regression from this PR.
Unbounded capturedErrors in deployed example Not addressed. Acceptable — Workers have short lifetimes.

Code correctness

The core changes are sound:

  1. RSC entry injection (app-dev-server.ts:384-399) — register() runs as a top-level await before route table construction. onRequestError is stored on globalThis.__VINEXT_onRequestErrorHandler__. Both are guarded by typeof checks. Correct.

  2. globalThis handler storage (instrumentation.ts) — getOnRequestErrorHandler() reads from globalThis, reportRequestError() uses the getter. The unit test afterEach properly cleans up globalThis.__VINEXT_onRequestErrorHandler__. Correct.

  3. Playwright config (playwright.config.ts) — The cloudflare-workers project now picks up app-router/instrumentation.spec.ts via testMatch, and both app-router and cloudflare-workers projects use baseURL so the spec is portable. Clean approach.

  4. E2e test design — Startup tests run without reset (to verify register() fired at startup). Error tests reset via DELETE in beforeEach. The test structure prevents state bleed between groups.

Remaining nits (non-blocking)

  • The whitespace inconsistencies (tabs vs spaces) in instrumentation.ts and global.d.ts should ideally be cleaned up in a follow-up since the linter doesn't catch them.
  • Missing semicolon on afterEach closing brace in tests/instrumentation.test.ts:71 () instead of );). Again, linter passes so this is stylistic.

Overall: the architecture is correct, the blocking issue is fixed, CI is green. Approving.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 7, 2026

Re-review posted and PR approved. Here's the summary:

Blocking issue from the first review is fixed. The !hasAppDir guard was added in commit 8d80bb2f at index.ts:2532, so register() no longer fires twice for App Router dev.

CI is fully green — all checks pass including Lint, Typecheck, Vitest, and all 5 Playwright E2E projects.

Remaining non-blocking nits (tabs vs spaces inconsistency in instrumentation.ts and global.d.ts, leading-space indent in the test fixture's instrumentation-state.ts, setTimeout instead of expect.poll() in e2e tests) were noted but none are worth blocking on. The core architecture — baking register() into the RSC entry as a top-level await and using globalThis for cross-environment handler storage — is correct and well-tested.

github run

@james-elicx james-elicx marked this pull request as ready for review March 7, 2026 22:38
@james-elicx james-elicx merged commit 80acf27 into main Mar 7, 2026
22 of 24 checks passed
@james-elicx james-elicx deleted the j-branch-2 branch March 7, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant