49 lines · 2.4 KB
import { INGEST_PREFIX } from "~/lib/analytics";
import { analyticsConfig } from "./analytics";

const MAX_BODY_BYTES = 5 * 1024 * 1024;
const FORWARDED_REQUEST_HEADERS = ["content-type", "content-encoding", "accept", "accept-encoding", "user-agent"];
const DROPPED_RESPONSE_HEADERS = ["set-cookie", "access-control-allow-origin", "access-control-allow-credentials"];

/** PostHog serves the browser library's lazily loaded extensions from a separate assets host. */
export function ingestUpstream(host: string, path: string): string {
  const assets = /^\/(static|array)\//.test(path);
  const match = /^https:\/\/(us|eu)\.i\.posthog\.com$/.exec(host);
  if (assets && match) return `https://${match[1]}-assets.i.posthog.com`;
  return host;
}

/**
 * Same-origin reverse proxy for PostHog (/ingest/*), so the page never talks to a third party directly. Only
 * the headers PostHog needs are forwarded: no cookies, credentials or client IP, and upstream cookies are
 * dropped. Answers 404 when analytics is off.
 */
export async function handleIngestProxy(request: Request, url: URL): Promise<Response | null> {
  if (url.pathname !== INGEST_PREFIX && !url.pathname.startsWith(`${INGEST_PREFIX}/`)) return null;
  const config = analyticsConfig();
  if (!config) return new Response("Not found\n", { status: 404 });
  if (request.method !== "GET" && request.method !== "POST" && request.method !== "HEAD") {
    return new Response("Method not allowed\n", { status: 405 });
  }
  const length = Number(request.headers.get("content-length") ?? 0);
  if (length > MAX_BODY_BYTES) return new Response("Payload too large\n", { status: 413 });
  const path = url.pathname.slice(INGEST_PREFIX.length) || "/";
  const target = new URL(`${path}${url.search}`, ingestUpstream(config.host, path));
  const headers = new Headers();
  for (const name of FORWARDED_REQUEST_HEADERS) {
    const value = request.headers.get(name);
    if (value) headers.set(name, value);
  }
  const hasBody = request.method === "POST";
  const upstream = await fetch(target, {
    method: request.method,
    headers,