import { Router } from "express"; /** * Houses the Sentry tunnel */ const app = Router(); if (process.env.SENTRY_DSN && process.env.SENTRY_TUNNEL_PROJECT_IDS) { // only register the endpoint if the environment variables are set const SENTRY_HOST = new URL(process.env.SENTRY_DSN).hostname; const SENTRY_PROJECT_IDS: string[] = process.env.SENTRY_TUNNEL_PROJECT_IDS.split(","); app.post("/_meta", async (req, res) => { try { // read POST data as raw text const envelope = await new Promise((res) => { let data = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { data += chunk; }); req.on("end", () => { res(data); }); }); // sentry sends data to the tunnel in 3 different parts, all JSON separated by newlines // the following reads the header line and verifies the DSN provided matches the constants above const piece = envelope.split("\n")[0]; const header = JSON.parse(piece); const dsn = new URL(header["dsn"]); const project_id = dsn.pathname?.replace("/", ""); if (dsn.hostname !== SENTRY_HOST) { throw new Error(`Invalid sentry hostname: ${dsn.hostname}`); } if (!project_id || !SENTRY_PROJECT_IDS.includes(project_id)) { throw new Error(`Invalid sentry project id: ${project_id}`); } // forward the data to sentry const upstream_sentry_url = `https://${SENTRY_HOST}/api/${project_id}/envelope/`; await fetch(upstream_sentry_url, { method: "POST", body: envelope, }); res.json({}); } catch (e) { // allow console.log due to this being an error handler preventing loops // eslint-disable-next-line no-console console.error("error tunneling to sentry", e); res.status(500).json({ error: "error tunneling" }); } }); } export default app;