Skip to content
sentry.ts 1.76 KiB
Newer Older
Grant's avatar
Grant committed
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<string>((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) {
      console.error("error tunneling to sentry", e);
      res.status(500).json({ error: "error tunneling" });
    }
  });
}

export default app;