- cloud: resolves userId from the authenticated session and builds a [userOrgScope, orgScope] stack via createScopedExecutor(userId, orgId, orgName). userId threaded through execution-stack, mcp-session, and MCP dispatch init. - cloud env refactor: deletes the lazy Proxy apps/cloud/src/env.ts in favor of cloudflare:workers env imported at call sites; adds env-augment.d.ts to type secrets not present in generated wrangler types. - cloud/server.ts: env-guarded OTEL wiring (skips instrument() when AXIOM_TOKEN absent); captures rawFetch before instrument() mutates it so dev requests still land on the Hono app. - cloud/services/db.ts: prefers DATABASE_URL over the hyperdrive proxy so local dev works without the tunnel. - vite.config.ts: devCrashGuard plugin that prints uncaught Worker errors with a stable exit. - hosts/mcp: optional debug flag surfaces verbose session logging, wired up from cloud mcp-session. - local/examples/marketing: migrated to scopes: [scope].
Showing the changes to apps/cloud/src/server.ts (1 of 25 files). Show all
Unified · Split
| @@ -3,7 +3,6 @@ import handler from "@tanstack/react-start/server-entry"; · 2 unmodified lines | ||
| 3 | 3 | import { instrument, type TraceConfig } from "@microlabs/otel-cf-workers"; |
| 4 | 4 | |
| 5 | 5 | import { McpSessionDO as McpSessionDOBase } from "./mcp-session"; |
| 6 | −import { server } from "./env"; | |
| 7 | 6 | |
| 8 | 7 | // --------------------------------------------------------------------------- |
| 9 | 8 | // OTEL config for the main fetch handler — `otel-cf-workers` owns the global |
| @@ -15,40 +14,36 @@ import { server } from "./env"; · 5 unmodified lines | ||
| 15 | 14 | // DOMException "Illegal invocation". |
| 16 | 15 | // --------------------------------------------------------------------------- |
| 17 | 16 | |
| 18 | −const parseSampleRatio = (value: string): number => { | |
| 17 | +const parseSampleRatio = (value: string | undefined): number => { | |
| 19 | 18 | const n = Number(value); |
| 20 | 19 | if (!Number.isFinite(n)) return 1; |
| 21 | 20 | return Math.min(1, Math.max(0, n)); |
| 22 | 21 | }; |
| 23 | 22 | |
| 24 | −const otelConfig: TraceConfig = { | |
| 23 | +const otelConfig = (env: Env): TraceConfig => ({ | |
| 25 | 24 | service: { name: "executor-cloud", version: "1.0.0" }, |
| 26 | 25 | exporter: { |
| 27 | − url: server.AXIOM_TRACES_URL, | |
| 26 | + url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", | |
| 28 | 27 | headers: { |
| 29 | − Authorization: `Bearer ${server.AXIOM_TOKEN}`, | |
| 30 | − "X-Axiom-Dataset": server.AXIOM_DATASET, | |
| 28 | + Authorization: `Bearer ${env.AXIOM_TOKEN ?? ""}`, | |
| 29 | + "X-Axiom-Dataset": env.AXIOM_DATASET ?? "executor-cloud", | |
| 31 | 30 | }, |
| 32 | 31 | }, |
| 33 | 32 | sampling: { |
| 34 | 33 | headSampler: { |
| 35 | 34 | // Keep remote parent decisions and make local sampling policy explicit. |
| 36 | 35 | acceptRemote: true, |
| 37 | − ratio: parseSampleRatio(server.AXIOM_TRACES_SAMPLE_RATIO), | |
| 36 | + ratio: parseSampleRatio(env.AXIOM_TRACES_SAMPLE_RATIO), | |
| 38 | 37 | }, |
| 39 | 38 | }, |
| 40 | −}; | |
| 39 | +}); | |
| 41 | 40 | |
| 42 | 41 | // otel-cf-workers owns the global TracerProvider. Sentry's OTEL compat shim |
| 43 | 42 | // registers a ProxyTracerProvider of its own, which prevents otel-cf-workers |
| 44 | 43 | // from finding its WorkerTracer and breaks the whole request path with |
| 45 | 44 | // "global tracer is not of type WorkerTracer". |
| 46 | −// | |
| 47 | −// The `_env` parameter is unused — `server` from `./env` already gives us | |
| 48 | −// typed access to every secret. It's only in the signature so Sentry's | |
| 49 | −// generics infer the DO's `Env` type correctly. | |
| 50 | −const sentryOptions = (_env: Env) => ({ | |
| 51 | − dsn: server.SENTRY_DSN, | |
| 45 | +const sentryOptions = (env: Env) => ({ | |
| 46 | + dsn: env.SENTRY_DSN, | |
| 52 | 47 | tracesSampleRate: 0, |
| 53 | 48 | enableLogs: true, |
| 54 | 49 | sendDefaultPii: true, |
| @@ -76,6 +71,28 @@ export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry( · 21 unmodified lines | ||
| 76 | 71 | // Worker fetch handler |
| 77 | 72 | // --------------------------------------------------------------------------- |
| 78 | 73 | |
| 79 | −const instrumentedHandler = instrument({ fetch: handler.fetch }, otelConfig); | |
| 74 | +// Skip OTLP wiring when no Axiom token is configured (dev without secrets). | |
| 75 | +// Otherwise the exporter ships every span with `Bearer ` (empty), which | |
| 76 | +// returns 401 on every batch and eventually drops the keep-alive socket — | |
| 77 | +// the Node http agent's unhandled `'error'` then crashes the process with | |
| 78 | +// ECONNRESET. It also registers otel-cf-workers' `WorkerTracer` as the | |
| 79 | +// global tracer; spans started outside its config ALS then die with | |
| 80 | +// "Config is undefined". Matches the gate in `DoTelemetryLive`. | |
| 81 | +// `instrument()` mutates the handler it's given (replaces `.fetch` with the | |
| 82 | +// proxied version), so capture the raw fetch first and then build the | |
| 83 | +// instrumented handler from a separate object. | |
| 84 | +const rawFetch = handler.fetch; | |
| 85 | +const instrumentedHandler = instrument({ fetch: rawFetch }, otelConfig); | |
| 86 | + | |
| 87 | +const dispatchHandler = { | |
| 88 | + fetch: (request: Request, env: Env, ctx: unknown) => { | |
| 89 | + const fn = env.AXIOM_TOKEN ? instrumentedHandler.fetch! : rawFetch; | |
| 90 | + return (fn as (req: Request, env: Env, ctx: unknown) => Response | Promise<Response>)( | |
| 91 | + request, | |
| 92 | + env, | |
| 93 | + ctx, | |
| 94 | + ); | |
| 95 | + }, | |
| 96 | +}; | |
| 80 | 97 | |
| 81 | −export default Sentry.withSentry(sentryOptions, instrumentedHandler); | |
| 98 | +export default Sentry.withSentry(sentryOptions, dispatchHandler); | |