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 | // API middleware — routes /api/* to the Effect HTTP layer | 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 | export const startInstance = createStart(() => ({ | 123 | export const startInstance = createStart(() => ({ |
| 82 | requestMiddleware: [marketingMiddleware, mcpRequestMiddleware, apiRequestMiddleware], | 124 | requestMiddleware: [ |
| 125 | marketingMiddleware, | ||
| 126 | mcpRequestMiddleware, | ||
| 127 | sentryTunnelMiddleware, | ||
| 128 | apiRequestMiddleware, | ||
| 129 | ], | ||
| 83 | })); | 130 | })); |