- 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 | import { instrument, type TraceConfig } from "@microlabs/otel-cf-workers"; | 3 | import { instrument, type TraceConfig } from "@microlabs/otel-cf-workers"; |
| 4 | 4 | ||
| 5 | import { McpSessionDO as McpSessionDOBase } from "./mcp-session"; | 5 | import { McpSessionDO as McpSessionDOBase } from "./mcp-session"; |
| 6 | import { server } from "./env"; | ||
| 7 | 6 | ||
| 8 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
| 9 | // OTEL config for the main fetch handler — `otel-cf-workers` owns the global | 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 | // DOMException "Illegal invocation". | 14 | // DOMException "Illegal invocation". |
| 16 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
| 17 | 16 | ||
| 18 | const parseSampleRatio = (value: string): number => { | 17 | const parseSampleRatio = (value: string | undefined): number => { |
| 19 | const n = Number(value); | 18 | const n = Number(value); |
| 20 | if (!Number.isFinite(n)) return 1; | 19 | if (!Number.isFinite(n)) return 1; |
| 21 | return Math.min(1, Math.max(0, n)); | 20 | return Math.min(1, Math.max(0, n)); |
| 22 | }; | 21 | }; |
| 23 | 22 | ||
| 24 | const otelConfig: TraceConfig = { | 23 | const otelConfig = (env: Env): TraceConfig => ({ |
| 25 | service: { name: "executor-cloud", version: "1.0.0" }, | 24 | service: { name: "executor-cloud", version: "1.0.0" }, |
| 26 | exporter: { | 25 | exporter: { |
| 27 | url: server.AXIOM_TRACES_URL, | 26 | url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", |
| 28 | headers: { | 27 | headers: { |
| 29 | Authorization: `Bearer ${server.AXIOM_TOKEN}`, | 28 | Authorization: `Bearer ${env.AXIOM_TOKEN ?? ""}`, |
| 30 | "X-Axiom-Dataset": server.AXIOM_DATASET, | 29 | "X-Axiom-Dataset": env.AXIOM_DATASET ?? "executor-cloud", |
| 31 | }, | 30 | }, |
| 32 | }, | 31 | }, |
| 33 | sampling: { | 32 | sampling: { |
| 34 | headSampler: { | 33 | headSampler: { |
| 35 | // Keep remote parent decisions and make local sampling policy explicit. | 34 | // Keep remote parent decisions and make local sampling policy explicit. |
| 36 | acceptRemote: true, | 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 | // otel-cf-workers owns the global TracerProvider. Sentry's OTEL compat shim | 41 | // otel-cf-workers owns the global TracerProvider. Sentry's OTEL compat shim |
| 43 | // registers a ProxyTracerProvider of its own, which prevents otel-cf-workers | 42 | // registers a ProxyTracerProvider of its own, which prevents otel-cf-workers |
| 44 | // from finding its WorkerTracer and breaks the whole request path with | 43 | // from finding its WorkerTracer and breaks the whole request path with |
| 45 | // "global tracer is not of type WorkerTracer". | 44 | // "global tracer is not of type WorkerTracer". |
| 46 | // | 45 | const sentryOptions = (env: Env) => ({ |
| 47 | // The `_env` parameter is unused — `server` from `./env` already gives us | 46 | dsn: env.SENTRY_DSN, |
| 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, | ||
| 52 | tracesSampleRate: 0, | 47 | tracesSampleRate: 0, |
| 53 | enableLogs: true, | 48 | enableLogs: true, |
| 54 | sendDefaultPii: true, | 49 | sendDefaultPii: true, |
| @@ -76,6 +71,28 @@ export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry( · 21 unmodified lines | |||
| 76 | // Worker fetch handler | 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); |