fix: register instrumentation in rsc entry and put error handler in global scope#330
fix: register instrumentation in rsc entry and put error handler in global scope#330james-elicx merged 4 commits intomainfrom
Conversation
commit: |
|
1ab76ea to
8964410
Compare
|
/bigbonk review this pr please |
There was a problem hiding this comment.
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.tslines 128 and 152 — tabs instead of spacespackages/vinext/src/global.d.tslines 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.
|
Review posted on PR #330 with "request changes." Here's a summary of the findings: Blocking issue:
Non-blocking issues:
|
|
/bigbonk re-review please |
There was a problem hiding this comment.
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:
-
RSC entry injection (
app-dev-server.ts:384-399) —register()runs as a top-levelawaitbefore route table construction.onRequestErroris stored onglobalThis.__VINEXT_onRequestErrorHandler__. Both are guarded bytypeofchecks. Correct. -
globalThishandler storage (instrumentation.ts) —getOnRequestErrorHandler()reads fromglobalThis,reportRequestError()uses the getter. The unit testafterEachproperly cleans upglobalThis.__VINEXT_onRequestErrorHandler__. Correct. -
Playwright config (
playwright.config.ts) — Thecloudflare-workersproject now picks upapp-router/instrumentation.spec.tsviatestMatch, and bothapp-routerandcloudflare-workersprojects usebaseURLso the spec is portable. Clean approach. -
E2e test design — Startup tests run without reset (to verify
register()fired at startup). Error tests reset viaDELETEinbeforeEach. The test structure prevents state bleed between groups.
Remaining nits (non-blocking)
- The whitespace inconsistencies (tabs vs spaces) in
instrumentation.tsandglobal.d.tsshould ideally be cleaned up in a follow-up since the linter doesn't catch them. - Missing semicolon on
afterEachclosing brace intests/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.
|
Re-review posted and PR approved. Here's the summary: Blocking issue from the first review is fixed. The 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: App Router support + Cloudflare Workers e2e tests
Completes
instrumentation.tssupport for App Router, and adds e2e coverage that runs against the production Cloudflare Workers build.What changed
Core:
register()baked into the RSC entryPreviously
runInstrumentation()was called fromconfigureServer()in the host Node.js process. This broke with@cloudflare/vite-pluginbecause the plugin runs the RSC environment inside a miniflare Worker subprocess — a separate process from the host, with its ownglobalThis. Callingregister()in the host meant it never ran in the same process as request handling.The fix:
generateRscEntry()now accepts aninstrumentationPathparameter and emitsregister()as a top-levelawaitat 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 callsrunInstrumentation()for Pages Router only, where there is no RSC entry and@cloudflare/vite-pluginis never present.onRequestErrorhandler storageThe 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, sharedglobalThis) and inside the Cloudflare Worker (single global scope). The key is declared inglobal.d.tsfor type safety.e2e coverage
tests/e2e/app-router/instrumentation.spec.ts— new spec coveringregister()startup,onRequestError()on route throws, multiple error capture, and no false positives on success. Uses relative URLs + PlaywrightbaseURLso it's portable across projects.cloudflare-workersPlaywright project now picks up this spec viatestMatch, running the same assertions against the production wrangler build on port 4176.app-router-cloudflareexample getsinstrumentation.ts,instrumentation-state.ts,/api/instrumentation-test, and/api/error-routeto support the tests.Unit tests
tests/instrumentation.test.tsgets abeforeEachcleanup ofglobalThis.__VINEXT_onRequestErrorHandler__to prevent handler bleed between tests after the storage change.