Skip to content
quarry Artifact [e9bd5f5bad]
Login

Artifact [e9bd5f5bad]

Artifact e9bd5f5bad32b16505db24c8dd39ad15fc76e4f4fe1ec31341245135d96d1744:


// 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);
  });
})();