fix(cloud): forward /api/sentry-tunnel to Sentry ingest
Unified · Split
| @@ -63,6 +63,48 @@ const mcpRequestMiddleware = createMiddleware({ type: "request" }).server( · 62 unmodified lines | ||
| 63 | 63 | }, |
| 64 | 64 | ); |
| 65 | 65 | |
| 66 | +// --------------------------------------------------------------------------- | |
| 67 | +// Sentry tunnel — the browser SDK POSTs envelopes to /api/sentry-tunnel | |
| 68 | +// (configured in routes/__root.tsx) to dodge adblockers and CSP. We parse | |
| 69 | +// the envelope header to recover the DSN, validate against our own, and | |
| 70 | +// forward the body to Sentry's ingest endpoint. See | |
| 71 | +// https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option | |
| 72 | +// --------------------------------------------------------------------------- | |
| 73 | + | |
| 74 | +const sentryTunnelMiddleware = createMiddleware({ type: "request" }).server( | |
| 75 | + async ({ pathname, request, next }) => { | |
| 76 | + if (pathname !== "/api/sentry-tunnel" || request.method !== "POST") { | |
| 77 | + return next(); | |
| 78 | + } | |
| 79 | + | |
| 80 | + const configuredDsn = (env as { SENTRY_DSN?: string }).SENTRY_DSN; | |
| 81 | + if (!configuredDsn) return new Response(null, { status: 204 }); | |
| 82 | + | |
| 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 | + } | |
| 105 | + }, | |
| 106 | +); | |
| 107 | + | |
| 66 | 108 | // --------------------------------------------------------------------------- |
| 67 | 109 | // API middleware — routes /api/* to the Effect HTTP layer |
| 68 | 110 | // --------------------------------------------------------------------------- |
| @@ -79,5 +121,10 @@ const apiRequestMiddleware = createMiddleware({ type: "request" }).server( · 10 unmodified lines | ||
| 79 | 121 | ); |
| 80 | 122 | |
| 81 | 123 | export const startInstance = createStart(() => ({ |
| 82 | − requestMiddleware: [marketingMiddleware, mcpRequestMiddleware, apiRequestMiddleware], | |
| 124 | + requestMiddleware: [ | |
| 125 | + marketingMiddleware, | |
| 126 | + mcpRequestMiddleware, | |
| 127 | + sentryTunnelMiddleware, | |
| 128 | + apiRequestMiddleware, | |
| 129 | + ], | |
| 83 | 130 | })); |