"use client";
import { usePathname } from "next/navigation";
import { useEffect, useRef } from "react";
/**
* Frontend crashes, relayed to s10.
*
* The browser cannot ship these itself — the ingest key never reaches client
* code — so the page hands each failure to jac-serve, which emits it under the
* same tenant as the request that caused it. That is the whole point: a Lowell
* crash and the `http.server.request` span behind it land on one timeline
* instead of in two places nobody correlates.
*
* What is sent: the error's own message and stack, the Next route, and how it
* surfaced. Those name code paths. Never repository content, never a file a
* visitor was reading, never anything a person typed.
*/
/** The same failure repeated is one bug, not four hundred. */
const SEEN_CAP = 40;
export function BugReporter() {
const pathname = usePathname() || "/";
const here = useRef(pathname);
here.current = pathname;
useEffect(() => {
const seen = new Set<string>();
const report = (
message: string,
stack: string | undefined,
kind: "error" | "unhandledrejection",
) => {
const trimmed = message.trim();
if (!trimmed) return;
// Dedupe on message + first stack frame: a render loop can throw the
// same error every frame, and reporting each one would drown the rest.
const key = `${trimmed}|${(stack ?? "").split("\n")[1] ?? ""}`;
if (seen.has(key)) return;
if (seen.size < SEEN_CAP) seen.add(key);
void fetch("/api/telemetry/bug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: trimmed,
stack,
pathname: here.current,
kind,
}),
// Survives the page being torn down by the same failure.
keepalive: true,
}).catch(() => {
/* reporting a bug must never itself become one */
});
};
const onError = (e: ErrorEvent) => {
report(e.message || String(e.error), e.error?.stack, "error");
};
const onRejection = (e: PromiseRejectionEvent) => {
const reason = e.reason as { message?: string; stack?: string } | string;
const message =
typeof reason === "string" ? reason : (reason?.message ?? "unhandled rejection");
report(message, typeof reason === "string" ? undefined : reason?.stack, "unhandledrejection");
};
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection);
return () => {
window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onRejection);
};
}, []);
return null;
}