diff --git a/apps/cloud/scripts/start-closure.mjs b/apps/cloud/scripts/start-closure.mjs index 0b45c22f1..a841cb597 100644 --- a/apps/cloud/scripts/start-closure.mjs +++ b/apps/cloud/scripts/start-closure.mjs @@ -12,7 +12,10 @@ // cold isolate before any request is served. // start - the TanStack Start server graph, reached through the lazy // `loadEntries` dynamic imports. Evaluated on the first request -// that enters the app. +// that enters the app - i.e. a page request. +// app - the Effect app plane. `/api/*` dispatches at the Worker entry and +// skips Start entirely, so an API request evaluates this instead of +// `start`; reported separately because the two planes now diverge. // // Anything reachable only through a dynamic import is not counted: making a // heavy dependency lazy is exactly the outcome this rewards. @@ -81,6 +84,9 @@ const startRoots = [...graph.get(ENTRY).dynamic].filter((f) => /(start|router|tanstack)/.test(name(f)), ); const start = closure(startRoots); +const appRoots = [...graph.get(ENTRY).dynamic].filter((f) => /\/app-[A-Za-z0-9_-]+\.js$/.test(f)); +const app = closure([ENTRY, ...appRoots]); +// The budget tracks the worst plane: whichever costs a cold isolate more. const evaluated = new Set([...startup, ...start]); const report = (label, files) => { @@ -93,7 +99,10 @@ const report = (label, files) => { report("startup", startup); report("start", start); -console.log(`\ntotal evaluated on a warm-path request: ${mb(bytes(evaluated))}`); +console.log(`\npage request (startup + start): ${mb(bytes(evaluated))}`); +console.log( + `API request (startup + app): ${mb(bytes(app))}${appRoots.length ? "" : " [no app chunk - /api still routes through Start]"}`, +); const lazyOnly = [...graph.keys()].filter((f) => !evaluated.has(f)); console.log( `deferred behind dynamic import: ${mb(bytes(lazyOnly))} (${lazyOnly.length} chunks)`, diff --git a/apps/cloud/src/app-paths.test.ts b/apps/cloud/src/app-paths.test.ts index 98aeff3bd..dd9ff1af3 100644 --- a/apps/cloud/src/app-paths.test.ts +++ b/apps/cloud/src/app-paths.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; -import { isAppOwnedPath } from "./app-paths"; +import { isAppOwnedPath, servedByAppPlane } from "./app-paths"; // Guards the start.ts dispatch decision: every surface the unified app handler // serves must be classified app-owned (forwarded to `app.handler`), and Start's @@ -62,3 +62,39 @@ describe("isAppOwnedPath", () => { }); } }); + +describe("app-plane dispatch", () => { + // These two are the whole risk of dispatching `/api` before Start: both still + // return a response if routed early, just the wrong one, so nothing else would + // catch a regression here. + it("leaves the Sentry tunnel POST to Start's middleware", () => { + expect(servedByAppPlane("/api/sentry-tunnel", "POST")).toBe(false); + // Only the POST is claimed; anything else under that path is ordinary API. + expect(servedByAppPlane("/api/sentry-tunnel", "GET")).toBe(true); + }); + + it("leaves the OAuth callback to Start, for the signed-out redirect", () => { + expect(servedByAppPlane("/api/oauth/callback", "GET")).toBe(false); + expect(servedByAppPlane("/api/oauth/callback", "POST")).toBe(false); + }); + + const appPlane = [ + "/api/connections", + "/api/tools", + "/api/integrations", + "/api/account/members", + "/api/docs", + "/api/billing/checkout", + ]; + for (const pathname of appPlane) { + it(`serves ${pathname} without entering Start`, () => { + expect(servedByAppPlane(pathname, "GET")).toBe(true); + }); + } + + it("never claims a non-API path, however app-owned", () => { + expect(servedByAppPlane("/mcp", "POST")).toBe(false); + expect(servedByAppPlane("/", "GET")).toBe(false); + expect(servedByAppPlane("/.well-known/oauth-authorization-server", "GET")).toBe(false); + }); +}); diff --git a/apps/cloud/src/app-paths.ts b/apps/cloud/src/app-paths.ts index 6dd6fc56f..48e70a8c3 100644 --- a/apps/cloud/src/app-paths.ts +++ b/apps/cloud/src/app-paths.ts @@ -17,3 +17,31 @@ export const isApiPath = (pathname: string) => pathname === "/api" || pathname.s export const isAppOwnedPath = (pathname: string) => isApiPath(pathname) || classifyMcpPath(pathname) !== null; + +// --------------------------------------------------------------------------- +// Which plane serves an app-owned path: the Effect app directly, or TanStack +// Start's middleware chain. +// +// Everything under `/api` is pure Effect and touches no part of the router, +// React, or SSR — so `server.ts` dispatches it at the Worker entry and skips +// Start's lazy `loadEntries` import entirely. Two paths must NOT take that +// shortcut, because Start's request middleware claims them BEFORE the app +// handler would ever see them: +// +// POST /api/sentry-tunnel - `sentryTunnelMiddleware` forwards the envelope +// to Sentry; the app has no such route. +// /api/oauth/callback - `oauthCallbackSignInMiddleware` redirects a +// signed-out visitor to /login, and start.ts +// rewrites the org-scoped `state` before handing +// off. Routing it early would drop both. +// +// Getting this wrong is silent: the request still gets a response, just the +// wrong one, which is why it is classified here and tested rather than being +// an inline condition at the dispatch site. +// --------------------------------------------------------------------------- + +export const isStartOwnedApiPath = (pathname: string, method: string): boolean => + (pathname === "/api/sentry-tunnel" && method === "POST") || pathname === "/api/oauth/callback"; + +export const servedByAppPlane = (pathname: string, method: string): boolean => + isApiPath(pathname) && !isStartOwnedApiPath(pathname, method); diff --git a/apps/cloud/src/server.ts b/apps/cloud/src/server.ts index 30624f3b4..ec4771e96 100644 --- a/apps/cloud/src/server.ts +++ b/apps/cloud/src/server.ts @@ -11,7 +11,7 @@ import { import * as Sentry from "@sentry/cloudflare"; import handler from "@tanstack/react-start/server-entry"; -import { isAppOwnedPath } from "./app-paths"; +import { isAppOwnedPath, servedByAppPlane } from "./app-paths"; import { marketingProxyRequest } from "./edge/marketing"; import { passthroughResponse } from "./edge/passthrough"; import { makeCloudMcpAgentHandler } from "./mcp/agent-handler"; @@ -244,6 +244,42 @@ const markStartGraphEntered = (): void => { startGraphEntered = true; }; +// --------------------------------------------------------------------------- +// Serving `/api/*` without entering TanStack Start. +// --------------------------------------------------------------------------- +// +// Everything under `/api` is the Effect app (`ExecutorApp.make`'s web handler) +// and uses no part of the router, React, or SSR. But it was dispatched from a +// Start *request middleware*, so reaching it meant paying Start's lazy +// `loadEntries` import of the whole server graph first. Measured on production +// 2026-08-19, splitting `/api/*` by whether the isolate had already loaded that +// graph: warm p50 **186ms**, cold p50 **2129ms**, with 28% of API requests cold. +// The dashboard fires many `/api/*` calls in parallel and waits for the slowest, +// so that cold tail is what the app actually feels like. +// +// So `/api` joins marketing, `/docs`, the PostHog proxy and `/mcp` at the Worker +// entry: classify and dispatch before anything touches Start. The evaluated +// closure for an API request drops from the full Start graph to the Worker's own +// (see `scripts/start-closure.mjs`). +// +// `servedByAppPlane` (./app-paths) decides which paths qualify — two under +// `/api` are claimed by Start's middleware first and must keep their old route. + +// Instantiated on the first request that needs it and memoized per isolate, +// mirroring `start.ts`'s `getApp`. The import stays dynamic so an isolate that +// only serves pages or proxies never evaluates the app graph at all. +let appPlane: ReturnType | undefined; +let appGraphEntered = false; + +const getAppPlane = async (): Promise> => { + if (appPlane === undefined) { + const { cloudApiHandler } = await import("./app"); + appPlane = cloudApiHandler(); + appGraphEntered = true; + } + return appPlane; +}; + const cloudflareHandler: ExportedHandler = { fetch: async (request, env, ctx) => { isolateRequestSeq += 1; @@ -330,13 +366,19 @@ const cloudflareHandler: ExportedHandler = { span.setAttribute("executor.start_graph.entered", startGraphEntered); span.setAttribute("executor.isolate.id", isolate.id); span.setAttribute("executor.isolate.age_ms", isolate.ageMs); + // Which plane served this: "app" skipped the Start graph entirely, so + // `start_graph.entered` says nothing about it. `app_graph.entered` + // is the app-plane analogue - false means this request paid for the + // Effect graph's first evaluation in this isolate. + const appPlaneRequest = servedByAppPlane(url.pathname, request.method); + span.setAttribute("executor.dispatch.plane", appPlaneRequest ? "app" : "start"); + if (appPlaneRequest) span.setAttribute("executor.app_graph.entered", appGraphEntered); // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; observe response/error for span status, keep the flush alive past the response try { - const response = await fetchHandler( - withTraceparent(request, span.spanContext()), - env, - ctx, - ); + const traced = withTraceparent(request, span.spanContext()); + const response = appPlaneRequest + ? await (await getAppPlane()).handler(prepareMcpOrgScope(traced)) + : await fetchHandler(traced, env, ctx); span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status); return response; } catch (err) {