History

Normalize cloud startup boundaries

Rhys Sullivan committed 6 months ago

Commit a995f281 parent b039b6e2 files · +63 −24Browse files

Unified · Split

2 files
  1. apps/cloud/src/services/db.ts +8 −1
  2. apps/cloud/src/start.ts +55 −23
apps/cloud/src/services/db.ts+8−1
@@ -73,7 +73,14 @@ export class DbService extends Context.Service< · 72 unmodified lines
7373 // Fire-and-forget: the Terminate round-trip sometimes hangs, and
7474 // we don't need to block scope close waiting for it.
7575 Effect.sync(() => {
76− sql.end({ timeout: 0 }).catch(() => undefined);
76+ void Effect.runFork(
77+ Effect.ignore(
78+ Effect.tryPromise({
79+ try: () => sql.end({ timeout: 0 }),
80+ catch: (cause) => cause,
81+ }),
82+ ),
83+ );
7784 }),
7885 ),
7986 );
apps/cloud/src/start.ts+55−23
@@ -1,5 +1,6 @@
11 import { env } from "cloudflare:workers";
22 import { createMiddleware, createStart } from "@tanstack/react-start";
3+import { Data, Effect, Schema } from "effect";
34 import { handleApiRequest } from "./api";
45 import { mcpFetch } from "./mcp";
56
@@ -72,7 +73,7 @@ const mcpRequestMiddleware = createMiddleware({ type: "request" }).server( · 66 unmodified lines
7273 // ---------------------------------------------------------------------------
7374
7475 const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server(
75− async ({ pathname, request, next }) => {
76+ ({ pathname, request, next }) => {
7677 if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") {
7778 return next();
7879 }
@@ -80,31 +81,62 @@ const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server( · 1 unmodified line
8081 const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN;
8182 if (!configuredDsn) return new Response(null, { status: 204 });
8283
83− try {
84− const envelope = await request.text();
85− const firstLine = envelope.slice(0, envelope.indexOf("\n"));
86− const header = JSON.parse(firstLine) as { dsn?: string };
87− if (!header.dsn) return new Response("missing dsn", { status: 400 });
88−
89− const envelopeDsn = new URL(header.dsn);
90− const ourDsn = new URL(configuredDsn);
91− if (envelopeDsn.host !== ourDsn.host || envelopeDsn.pathname !== ourDsn.pathname) {
92− return new Response("dsn mismatch", { status: 400 });
93− }
94−
95− const projectId = envelopeDsn.pathname.replace(/^\//, "");
96− const ingestUrl = `https://${envelopeDsn.host}/api/${projectId}/envelope/`;
97− return fetch(ingestUrl, {
98− method: "POST",
99− body: envelope,
100− headers: { "Content-Type": "application/x-sentry-envelope" },
101− });
102− } catch {
103− return new Response("bad envelope", { status: 400 });
104− }
84+ return Effect.runPromise(handleSentryTunnelRequest(request, configuredDsn));
10585 },
10686 );
10787
88+class SentryTunnelError extends Data.TaggedError("SentryTunnelError")<{
89+ readonly cause?: unknown;
90+}> {}
91+
92+const SentryEnvelopeHeader = Schema.Struct({
93+ dsn: Schema.optional(Schema.String),
94+});
95+
96+const decodeSentryEnvelopeHeader = Schema.decodeUnknownEffect(
97+ Schema.fromJsonString(SentryEnvelopeHeader),
98+);
99+
100+const badSentryEnvelopeResponse = () => new Response("bad envelope", { status: 400 });
101+
102+const handleSentryTunnelRequest = (request: Request, configuredDsn: string) =>
103+ Effect.gen(function* () {
104+ const envelope = yield* Effect.tryPromise({
105+ try: () => request.text(),
106+ catch: (cause) => new SentryTunnelError({ cause }),
107+ });
108+ const firstLine = envelope.slice(0, envelope.indexOf("\n"));
109+ const header = yield* decodeSentryEnvelopeHeader(firstLine).pipe(
110+ Effect.mapError((cause) => new SentryTunnelError({ cause })),
111+ );
112+ const dsn = header.dsn;
113+ if (!dsn) return new Response("missing dsn", { status: 400 });
114+
115+ const envelopeDsn = yield* Effect.try({
116+ try: () => new URL(dsn),
117+ catch: (cause) => new SentryTunnelError({ cause }),
118+ });
119+ const ourDsn = yield* Effect.try({
120+ try: () => new URL(configuredDsn),
121+ catch: (cause) => new SentryTunnelError({ cause }),
122+ });
123+ if (envelopeDsn.host !== ourDsn.host || envelopeDsn.pathname !== ourDsn.pathname) {
124+ return new Response("dsn mismatch", { status: 400 });
125+ }
126+
127+ const projectId = envelopeDsn.pathname.replace(/^\//, "");
128+ const ingestUrl = `https://${envelopeDsn.host}/api/${projectId}/envelope/`;
129+ return yield* Effect.tryPromise({
130+ try: () =>
131+ fetch(ingestUrl, {
132+ method: "POST",
133+ body: envelope,
134+ headers: { "Content-Type": "application/x-sentry-envelope" },
135+ }),
136+ catch: (cause) => new SentryTunnelError({ cause }),
137+ });
138+ }).pipe(Effect.catch(() => Effect.succeed(badSentryEnvelopeResponse())));
139+
108140 // ---------------------------------------------------------------------------
109141 // PostHog reverse proxy — the browser SDK targets a build-randomized
110142 // first-party path and we forward to PostHog's ingest + asset hosts. Keeps