Skip to content

Commit 51835e1

Browse files
authored
One http.server span per request: the worker defers to Effect's tracer (#985)
Every API/MCP request produced TWO identical sibling http.server spans: the worker-boundary span server.ts opens (scope executor-cloud-worker) AND the one Effect's HttpMiddleware.tracer opens for app-owned paths (scope executor-cloud) — both joined the caller's traceparent, so the waterfall showed a childless twin next to the real envelope, and server span ingest was doubled (visible in Axiom: 39.7k executor-cloud vs 24.9k executor-cloud-worker spans on /mcp alone). The worker now skips its span for app-owned paths (app-paths.ts: /api/*, /mcp, /.well-known/*) — Effect's middleware already parses traceparent and parents the workos/store/db children there. Non-app paths (Start SSR, marketing proxy, /_astro assets) keep the worker envelope span, and the flush-on-waitUntil still runs on both branches so batched exports survive the isolate. Verified against the suite motel: API scenario + browser scenario runs now record exactly one http.server per trace (worker scope only on / and SSR routes), browser→server join intact (executor-web client span → executor-cloud server span → workos/user_store/fumadb children); cloud observability unit tests green. Found via the prod Axiom dataset right after #981 made browser traces land — the duplicate was previously invisible without the join.
1 parent 2d86581 commit 51835e1

1 file changed

Lines changed: 23 additions & 0 deletions

File tree

apps/cloud/src/server.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import * as Sentry from "@sentry/cloudflare";
1010
import handler from "@tanstack/react-start/server-entry";
1111

12+
import { isAppOwnedPath } from "./app-paths";
1213
import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object";
1314
import { browserTracesResponse } from "./observability/browser-traces";
1415
import { flushTracerProvider, installTracerProvider } from "./observability/telemetry";
@@ -53,6 +54,16 @@ export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry(
5354
// migration — without the OTel-SDK version-conflict that package would now
5455
// drag in (it pins `@opentelemetry/otlp-* ^0.200.0`, we ship ^0.214.0).
5556
//
57+
// ONLY for paths the Effect app does not own. App-owned paths (/api/*, /mcp,
58+
// /.well-known/* — see app-paths.ts) get their `http.server` span from
59+
// Effect's own HttpMiddleware.tracer, which parses `traceparent` itself and
60+
// parents the workos/store/db child spans. Wrapping those here too produced
61+
// two identical sibling `http.server` spans per request (scope
62+
// `executor-cloud-worker` next to scope `executor-cloud`) — double ingest,
63+
// and the waterfall showed a childless twin. The worker span remains for
64+
// everything Effect never sees: Start SSR, the marketing proxy, /_astro
65+
// assets.
66+
//
5667
// SimpleSpanProcessor exports synchronously at span end but the underlying
5768
// `fetch()` to Axiom is fire-and-forget; the Worker may terminate before it
5869
// completes. `ctx.waitUntil(flushTracerProvider())` keeps the isolate alive
@@ -78,6 +89,18 @@ const cloudflareHandler: ExportedHandler<Env> = {
7889
return fetchHandler(request, env, ctx);
7990
}
8091
const url = new URL(request.url);
92+
// Effect-served paths bring their own http.server span (with traceparent
93+
// join) — opening one here too would duplicate it. See the header note.
94+
if (isAppOwnedPath(url.pathname)) {
95+
// The provider is installed (above) and the flush still must outlive
96+
// the request — Effect's BatchSpanProcessor ships on a timer.
97+
// oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; mirror the traced path's finally
98+
try {
99+
return await fetchHandler(request, env, ctx);
100+
} finally {
101+
ctx.waitUntil(flushTracerProvider());
102+
}
103+
}
81104
// Join the caller's W3C trace when the request carries one — the web UI
82105
// sends traceparent on every API fetch, so the browser's spans and this
83106
// request share one trace id end to end. Same parsing the DO path does

0 commit comments

Comments
 (0)