History

cloud/local/examples: thread per-user ScopeStack through HTTP + MCP

- 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].

Rhys Sullivan committed 6 months ago

Commit d4e311e1 parent 427699c25 files · +320 −174Browse files

Showing the changes to apps/cloud/src/server.ts (1 of 25 files). Show all

Unified · Split

apps/cloud/src/server.ts+33−16
@@ -3,7 +3,6 @@ import handler from "@tanstack/react-start/server-entry"; · 2 unmodified lines
33 import { instrument, type TraceConfig } from "@microlabs/otel-cf-workers";
44
55 import { McpSessionDO as McpSessionDOBase } from "./mcp-session";
6−import { server } from "./env";
76
87 // ---------------------------------------------------------------------------
98 // OTEL config for the main fetch handler — `otel-cf-workers` owns the global
@@ -15,40 +14,36 @@ import { server } from "./env"; · 5 unmodified lines
1514 // DOMException "Illegal invocation".
1615 // ---------------------------------------------------------------------------
1716
18−const parseSampleRatio = (value: string): number => {
17+const parseSampleRatio = (value: string | undefined): number => {
1918 const n = Number(value);
2019 if (!Number.isFinite(n)) return 1;
2120 return Math.min(1, Math.max(0, n));
2221 };
2322
24−const otelConfig: TraceConfig = {
23+const otelConfig = (env: Env): TraceConfig => ({
2524 service: { name: "executor-cloud", version: "1.0.0" },
2625 exporter: {
27− url: server.AXIOM_TRACES_URL,
26+ url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces",
2827 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",
3130 },
3231 },
3332 sampling: {
3433 headSampler: {
3534 // Keep remote parent decisions and make local sampling policy explicit.
3635 acceptRemote: true,
37− ratio: parseSampleRatio(server.AXIOM_TRACES_SAMPLE_RATIO),
36+ ratio: parseSampleRatio(env.AXIOM_TRACES_SAMPLE_RATIO),
3837 },
3938 },
40−};
39+});
4140
4241 // otel-cf-workers owns the global TracerProvider. Sentry's OTEL compat shim
4342 // registers a ProxyTracerProvider of its own, which prevents otel-cf-workers
4443 // from finding its WorkerTracer and breaks the whole request path with
4544 // "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,
5247 tracesSampleRate: 0,
5348 enableLogs: true,
5449 sendDefaultPii: true,
@@ -76,6 +71,28 @@ export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry( · 21 unmodified lines
7671 // Worker fetch handler
7772 // ---------------------------------------------------------------------------
7873
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+};
8097
81−export default Sentry.withSentry(sentryOptions, instrumentedHandler);
98+export default Sentry.withSentry(sentryOptions, dispatchHandler);