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
73 // Fire-and-forget: the Terminate round-trip sometimes hangs, and73 // Fire-and-forget: the Terminate round-trip sometimes hangs, and
74 // we don't need to block scope close waiting for it.74 // we don't need to block scope close waiting for it.
75 Effect.sync(() => {75 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 );
77 }),84 }),
78 ),85 ),
79 );86 );
apps/cloud/src/start.ts+55−23
@@ -1,5 +1,6 @@
1import { env } from "cloudflare:workers";1import { env } from "cloudflare:workers";
2import { createMiddleware, createStart } from "@tanstack/react-start";2import { createMiddleware, createStart } from "@tanstack/react-start";
3import { Data, Effect, Schema } from "effect";
3import { handleApiRequest } from "./api";4import { handleApiRequest } from "./api";
4import { mcpFetch } from "./mcp";5import { mcpFetch } from "./mcp";
5 6
@@ -72,7 +73,7 @@ const mcpRequestMiddleware = createMiddleware({ type: "request" }).server( · 66 unmodified lines
72// ---------------------------------------------------------------------------73// ---------------------------------------------------------------------------
73 74
74const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server(75const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server(
75 async ({ pathname, request, next }) => {76 ({ pathname, request, next }) => {
76 if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") {77 if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") {
77 return next();78 return next();
78 }79 }
@@ -80,31 +81,62 @@ const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server( · 1 unmodified line
80 const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN;81 const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN;
81 if (!configuredDsn) return new Response(null, { status: 204 });82 if (!configuredDsn) return new Response(null, { status: 204 });
82 83
83 try {84 return Effect.runPromise(handleSentryTunnelRequest(request, configuredDsn));
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 }
105 },85 },
106);86);
107 87
88class SentryTunnelError extends Data.TaggedError("SentryTunnelError")<{
89 readonly cause?: unknown;
90}> {}
91
92const SentryEnvelopeHeader = Schema.Struct({
93 dsn: Schema.optional(Schema.String),
94});
95
96const decodeSentryEnvelopeHeader = Schema.decodeUnknownEffect(
97 Schema.fromJsonString(SentryEnvelopeHeader),
98);
99
100const badSentryEnvelopeResponse = () => new Response("bad envelope", { status: 400 });
101
102const 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
108// ---------------------------------------------------------------------------140// ---------------------------------------------------------------------------
109// PostHog reverse proxy — the browser SDK targets a build-randomized141// PostHog reverse proxy — the browser SDK targets a build-randomized
110// first-party path and we forward to PostHog's ingest + asset hosts. Keeps142// first-party path and we forward to PostHog's ingest + asset hosts. Keeps