// ---------------------------------------------------------------------------// Sentry tunnel — the browser SDK POSTs envelopes to /api/sentry-tunnel// (configured in routes/__root.tsx) to dodge adblockers and CSP. We parse the// envelope header to recover the DSN, validate against our own, and forward the// body to Sentry's ingest endpoint. See// https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option// --------------------------------------------------------------------------- import { env } from "cloudflare:workers";import { createMiddleware } from "@tanstack/react-start";import { Data, Effect, Schema } from "effect"; class SentryTunnelError extends Data.TaggedError("SentryTunnelError")<{ readonly cause?: unknown;}> {} const SentryEnvelopeHeader = Schema.Struct({ dsn: Schema.optional(Schema.String),}); const decodeSentryEnvelopeHeader = Schema.decodeUnknownEffect( Schema.fromJsonString(SentryEnvelopeHeader),); const badSentryEnvelopeResponse = () => new Response("bad envelope", { status: 400 }); export const handleSentryTunnelRequest = (request: Request, configuredDsn: string) => Effect.gen(function* () { const envelope = yield* Effect.tryPromise({ try: () => request.text(), catch: (cause) => new SentryTunnelError({ cause }), }); const firstLine = envelope.slice(0, envelope.indexOf("\n")); const header = yield* decodeSentryEnvelopeHeader(firstLine).pipe( Effect.mapError((cause) => new SentryTunnelError({ cause })), ); const dsn = header.dsn; if (!dsn) return new Response("missing dsn", { status: 400 }); const envelopeDsn = yield* Effect.try({ try: () => new URL(dsn), catch: (cause) => new SentryTunnelError({ cause }), }); const ourDsn = yield* Effect.try({ try: () => new URL(configuredDsn), catch: (cause) => new SentryTunnelError({ cause }), }); if (envelopeDsn.host !== ourDsn.host || envelopeDsn.pathname !== ourDsn.pathname) { return new Response("dsn mismatch", { status: 400 }); } const projectId = envelopeDsn.pathname.replace(/^\//, ""); const ingestUrl = `https://${envelopeDsn.host}/api/${projectId}/envelope/`; return yield* Effect.tryPromise({ try: () => fetch(ingestUrl, { method: "POST", body: envelope, headers: { "Content-Type": "application/x-sentry-envelope" }, }), catch: (cause) => new SentryTunnelError({ cause }), }); }).pipe(Effect.catch(() => Effect.succeed(badSentryEnvelopeResponse()))); export const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server( ({ pathname, request, next }) => { if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") { return next(); } const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN; if (!configuredDsn) return new Response(null, { status: 204 }); return Effect.runPromise(handleSentryTunnelRequest(request, configuredDsn)); },);
// ---------------------------------------------------------------------------
// Sentry tunnel — the browser SDK POSTs envelopes to /api/sentry-tunnel
// (configured in routes/__root.tsx) to dodge adblockers and CSP. We parse the
// envelope header to recover the DSN, validate against our own, and forward the
// body to Sentry's ingest endpoint. See
// https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option
// ---------------------------------------------------------------------------
import { env } from "cloudflare:workers";
import { createMiddleware } from "@tanstack/react-start";
import { Data, Effect, Schema } from "effect";
class SentryTunnelError extends Data.TaggedError("SentryTunnelError")<{
readonly cause?: unknown;
}> {}
const SentryEnvelopeHeader = Schema.Struct({
dsn: Schema.optional(Schema.String),
});
const decodeSentryEnvelopeHeader = Schema.decodeUnknownEffect(
Schema.fromJsonString(SentryEnvelopeHeader),
);
const badSentryEnvelopeResponse = () => new Response("bad envelope", { status: 400 });
export const handleSentryTunnelRequest = (request: Request, configuredDsn: string) =>
Effect.gen(function* () {
const envelope = yield* Effect.tryPromise({
try: () => request.text(),
catch: (cause) => new SentryTunnelError({ cause }),
});
const firstLine = envelope.slice(0, envelope.indexOf("\n"));
const header = yield* decodeSentryEnvelopeHeader(firstLine).pipe(
Effect.mapError((cause) => new SentryTunnelError({ cause })),
);
const dsn = header.dsn;
if (!dsn) return new Response("missing dsn", { status: 400 });
const envelopeDsn = yield* Effect.try({
try: () => new URL(dsn),
catch: (cause) => new SentryTunnelError({ cause }),
});
const ourDsn = yield* Effect.try({
try: () => new URL(configuredDsn),
catch: (cause) => new SentryTunnelError({ cause }),
});
if (envelopeDsn.host !== ourDsn.host || envelopeDsn.pathname !== ourDsn.pathname) {
return new Response("dsn mismatch", { status: 400 });
}
const projectId = envelopeDsn.pathname.replace(/^\//, "");
const ingestUrl = `https://${envelopeDsn.host}/api/${projectId}/envelope/`;
return yield* Effect.tryPromise({
try: () =>
fetch(ingestUrl, {
method: "POST",
body: envelope,
headers: { "Content-Type": "application/x-sentry-envelope" },
}),
catch: (cause) => new SentryTunnelError({ cause }),
});
}).pipe(Effect.catch(() => Effect.succeed(badSentryEnvelopeResponse())));
export const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server(
({ pathname, request, next }) => {
if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") {
return next();
}
const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN;
if (!configuredDsn) return new Response(null, { status: 204 });
return Effect.runPromise(handleSentryTunnelRequest(request, configuredDsn));
},
);