DocDoeAI / src /lib /telemetry.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
2.76 kB
// Forwards unhandled UI errors to the backend so they land in the same
// error-tracking pane as backend errors. Fire-and-forget: never throws, never
// blocks render. No-op on the server.
import { track } from "@vercel/analytics";
import { getApiBaseUrl } from "@/lib/api/client";
const SENSITIVE_ASSIGNMENT = /\b(authorization|cookie|password|token|secret|api[_-]?key|service[_-]?role)\b(\s*[:=]\s*)([^\s,;]+)/gi;
const BEARER_VALUE = /\bbearer\s+[a-z0-9._~+/=-]+/gi;
export function sanitizeClientErrorText(value: unknown, limit: number): string {
return String(value || "Unknown client error")
.replace(BEARER_VALUE, "Bearer [REDACTED]")
.replace(SENSITIVE_ASSIGNMENT, (_match, key: string, separator: string) => `${key}${separator}[REDACTED]`)
.slice(0, limit);
}
export function reportClientError(
error: unknown,
context: string,
extra?: { digest?: string },
): void {
if (typeof window === "undefined") return;
try {
const err = error as { message?: string; stack?: string } | undefined;
const body = JSON.stringify({
message: sanitizeClientErrorText(err?.message || error, 2000),
stack: sanitizeClientErrorText(err?.stack || "", 8000),
url: `${window.location.origin}${window.location.pathname}`,
digest: extra?.digest,
user_agent: navigator.userAgent,
context,
});
// A relative fallback keeps same-origin deployments observable. If the
// API is intentionally hosted elsewhere, configure the public base URL;
// never send an error to an accidental empty/undefined host.
const url = `${getApiBaseUrl()}/telemetry/client-error`;
// sendBeacon survives page unload; fall back to fetch keepalive.
if (navigator.sendBeacon) {
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
} else {
void fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
keepalive: true,
}).catch(() => {});
}
} catch {
// swallow — telemetry must never break the error UI
}
}
/** Privacy-safe funnel telemetry for non-payment product actions. */
export function trackClientEvent(
name: string,
properties?: Record<string, string | number | boolean>,
): void {
if (typeof window === "undefined") return;
try {
const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
const safeProperties = Object.fromEntries(
Object.entries(properties ?? {}).filter(
([key, value]) =>
/^[a-zA-Z0-9_.-]{1,40}$/.test(key) &&
(typeof value === "string" ? value.length <= 120 : true),
),
);
track(safeName, safeProperties);
} catch {
// Analytics must never affect the student workflow.
}
}