DEV Community

Zenovay
Zenovay

Posted on

Lightweight client side error tracking without a 90kb SDK

The popular error tracking SDKs are great and also large. For our analytics tool we wanted error capture that adds a couple of kb, not ninety. Here is the whole thing. It is smaller than you think.

Capture the two events that matter

type CapturedError = {
  message: string; stack?: string; url: string;
  line?: number; col?: number;
  kind: "error" | "unhandledrejection"; ts: number;
};

const queue: CapturedError[] = [];

window.addEventListener("error", (e) => {
  queue.push({ message: e.message, stack: e.error?.stack, url: location.href,
    line: e.lineno, col: e.colno, kind: "error", ts: Date.now() });
  schedule();
});

window.addEventListener("unhandledrejection", (e) => {
  const r = e.reason;
  queue.push({ message: typeof r === "string" ? r : r?.message ?? "unhandled rejection",
    stack: r?.stack, url: location.href, kind: "unhandledrejection", ts: Date.now() });
  schedule();
});
Enter fullscreen mode Exit fullscreen mode

That is most of the value right there. error catches thrown exceptions, unhandledrejection catches the async ones everyone forgets.

Batch, debounce, and survive unload

let timer: number | undefined;

function schedule() {
  clearTimeout(timer);
  timer = setTimeout(flush, 2000) as unknown as number;
  if (queue.length >= 20) flush();
}

function flush() {
  if (!queue.length) return;
  const batch = queue.splice(0, queue.length);
  navigator.sendBeacon("/errors", JSON.stringify(batch));
}

addEventListener("pagehide", flush);
Enter fullscreen mode Exit fullscreen mode

sendBeacon is the trick. A normal fetch on unload gets cancelled. Beacon does not.

Do not drown in noise

Two cheap filters save you from a flood:

const seen = new Set<string>();

function dedupe(err: CapturedError): boolean {
  const sig = err.message + ":" + err.line + ":" + err.col;
  if (seen.has(sig)) return false;   // already reported this page load
  seen.add(sig);
  return true;
}

const IGNORE = [/ResizeObserver loop/, /Script error\.?$/, /extension/i];
const ignored = (err: CapturedError) => IGNORE.some((re) => re.test(err.message));
Enter fullscreen mode Exit fullscreen mode

Script error. with no detail is almost always a cross origin script with no CORS headers. ResizeObserver loop limit exceeded is benign browser noise. Filter both.

The one thing you actually need a server for

Minified stacks are useless without source maps. Upload your source maps at build time and resolve the stack server side. That, not the capture, is the part worth real effort.

Takeaway

Two event listeners, a batched beacon, dedupe, and an ignore list gets you most of the value of a heavy SDK in a couple of kb. Spend your effort on source map resolution, not on the capture.

Disclosure: this is the error tracking we ship in Zenovay.

Top comments (0)