import * as Sentry from "@sentry/cloudflare"; import handler from "@tanstack/react-start/server-entry"; import { instrument, type TraceConfig } from "@microlabs/otel-cf-workers"; import { McpSessionDO as McpSessionDOBase } from "./mcp-session"; // --------------------------------------------------------------------------- // OTEL config for the main fetch handler — `otel-cf-workers` owns the global // TracerProvider and flushes via `ctx.waitUntil` at the end of each request. // The DO runs in a separate isolate and uses its own self-contained WebSdk // (see `services/telemetry.ts#DoTelemetryLive`); `instrumentDO` from // otel-cf-workers is NOT used because it breaks `this` binding on // `WorkerTransport`'s stream primitives and crashes every MCP request with // DOMException "Illegal invocation". // --------------------------------------------------------------------------- const parseSampleRatio = (value: string | undefined): number => { const n = Number(value); if (!Number.isFinite(n)) return 1; return Math.min(1, Math.max(0, n)); }; const otelConfig = (env: Env): TraceConfig => ({ service: { name: "executor-cloud", version: "1.0.0" }, exporter: { url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", headers: { Authorization: `Bearer ${env.AXIOM_TOKEN ?? ""}`, "X-Axiom-Dataset": env.AXIOM_DATASET ?? "executor-cloud", }, }, sampling: { headSampler: { // Keep remote parent decisions and make local sampling policy explicit. acceptRemote: true, ratio: parseSampleRatio(env.AXIOM_TRACES_SAMPLE_RATIO), }, }, }); // otel-cf-workers owns the global TracerProvider. Sentry's OTEL compat shim // registers a ProxyTracerProvider of its own, which prevents otel-cf-workers // from finding its WorkerTracer and breaks the whole request path with // "global tracer is not of type WorkerTracer". const sentryOptions = (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 0, enableLogs: true, sendDefaultPii: true, skipOpenTelemetrySetup: true, // Our DO methods (init/handleRequest/alarm) live on the prototype, not on // the instance. Sentry's default DO auto-wrap only visits own properties, // which misses prototype methods — so errors thrown inside init() never // reach Sentry. This flag opts into prototype-method instrumentation. instrumentPrototypeMethods: true, }); // --------------------------------------------------------------------------- // Durable Object — wrapped with Sentry so DO errors land in Sentry (inits the // client inside the DO isolate, which plain `Sentry.captureException` cannot // do on its own). We deliberately do NOT wrap with otel-cf-workers' // `instrumentDO` (see note above). // --------------------------------------------------------------------------- export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry( sentryOptions, McpSessionDOBase, ); // --------------------------------------------------------------------------- // Worker fetch handler // --------------------------------------------------------------------------- // Skip OTLP wiring when no Axiom token is configured (dev without secrets). // Otherwise the exporter ships every span with `Bearer ` (empty), which // returns 401 on every batch and eventually drops the keep-alive socket — // the Node http agent's unhandled `'error'` then crashes the process with // ECONNRESET. It also registers otel-cf-workers' `WorkerTracer` as the // global tracer; spans started outside its config ALS then die with // "Config is undefined". Matches the gate in `DoTelemetryLive`. // `instrument()` mutates the handler it's given (replaces `.fetch` with the // proxied version), so capture the raw fetch first and then build the // instrumented handler from a separate object. const rawFetch = handler.fetch; const instrumentedHandler = instrument({ fetch: rawFetch }, otelConfig); const dispatchHandler = { fetch: (request: Request, env: Env, ctx: unknown) => { const fn = env.AXIOM_TOKEN ? instrumentedHandler.fetch! : rawFetch; return (fn as (req: Request, env: Env, ctx: unknown) => Response | Promise)( request, env, ctx, ); }, }; export default Sentry.withSentry(sentryOptions, dispatchHandler);