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 { 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, body: hasBody ? request.body : undefined, redirect: "manual", }).catch(() => null); if (!upstream) return new Response("Bad gateway\n", { status: 502 }); const responseHeaders = new Headers(upstream.headers); for (const name of DROPPED_RESPONSE_HEADERS) responseHeaders.delete(name); return new Response(upstream.body, { status: upstream.status, headers: responseHeaders }); }