// Client-side exception capture. The fleet's Exceptions page is the one
// place a failure is meant to land, and until this existed it only ever
// heard about the half of the app that runs on the server: a broken
// handler on a phone was invisible unless the reader thought to say so.
//
// Deliberately small and defensive. It runs on every page, before any
// other script, and it must never itself become the thing that breaks a
// page: every path is wrapped, and a failure to report is dropped.
(function () {
var ENDPOINT = "/api/client-error";
// The server caps its ring anyway; this is about not making a loop on
// the page into a flood on the wire. One report per signature per
// page-load, and a hard ceiling however many distinct ones there are.
var sent = Object.create(null);
var budget = 8;
function post(kind, message, source, line, col, stack) {
try {
if (budget <= 0) return;
var sig = kind + "|" + message + "|" + source + "|" + line;
if (sent[sig]) return;
sent[sig] = true;
budget--;
var body = JSON.stringify({
kind: kind,
message: String(message || "").slice(0, 500),
source: String(source || "").slice(0, 300),
line: line || 0,
col: col || 0,
stack: String(stack || "").slice(0, 4000),
url: location.pathname + location.search,
ua: navigator.userAgent.slice(0, 200),
});
// sendBeacon survives the page going away, which is exactly when
// a navigation-triggered error would otherwise be lost. fetch is
// the fallback; both are best-effort and neither is awaited.
if (navigator.sendBeacon) {
navigator.sendBeacon(ENDPOINT, new Blob([body], { type: "application/json" }));
return;
}
fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body,
keepalive: true,
}).catch(function () {});
} catch (e) {
/* reporting must never throw */
}
}
window.addEventListener("error", function (ev) {
// A failed <img>/<script>/<link> fires error on the ELEMENT, not the
// window, and carries no message. Those are worth knowing about —
// a stylesheet that 404s is a broken page — but they are a
// different kind, not a thrown exception.
if (ev && ev.target && ev.target !== window && ev.target.tagName) {
var el = ev.target;
post("resource", el.tagName.toLowerCase() + " failed to load",
el.src || el.href || "", 0, 0, "");
return;
}
post("error", (ev && ev.message) || "unknown error",
ev && ev.filename, ev && ev.lineno, ev && ev.colno,
ev && ev.error && ev.error.stack);
}, true); // capture phase: resource errors do not bubble
window.addEventListener("unhandledrejection", function (ev) {
var r = ev && ev.reason;
post("unhandledrejection",
(r && (r.message || r)) || "unhandled promise rejection",
"", 0, 0, r && r.stack);
});
})();