Skip to content
quarry Artifact [7d851be831]
Login

Artifact [7d851be831]

Artifact 7d851be831d87d54fe0ba099ef565c2fbcea160dbab4a4d5115a6fa38759db39:


// 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 */
    }
  }

  // ours reports whether a resource is one THIS SITE shipped.
  //
  // Every asset quarry references is same-origin by construction — the
  // privacy page states it as a promise: the pages load nothing from
  // another host. So a cross-origin resource error is never quarry's
  // asset failing. It is something else putting a URL on the page: an
  // intermediary injecting a script, a browser extension, or a README
  // badge pointing at another forge. None of those are "a reader's page
  // did not work", and journalling them as server exceptions buries the
  // ones that are.
  //
  // An unparseable or relative-but-odd URL counts as ours, because our
  // own URLs always parse and the cost of a false report is one row
  // while the cost of a false silence is a broken asset nobody hears
  // about.
  function ours(url) {
    if (!url) return true;
    try {
      var u = new URL(url, location.href);
      // data: and blob: have an opaque origin that matches nothing, but
      // only this page's own markup can produce one — an inline
      // identicon, an object URL we made. A failure there is ours.
      if (u.protocol === "data:" || u.protocol === "blob:") return true;
      return u.origin === location.origin;
    } catch (e) {
      return true;
    }
  }

  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;
      var url = el.src || el.href || "";
      if (!ours(url)) return;
      post("resource", el.tagName.toLowerCase() + " failed to load",
        url, 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;
    // A view transition that was skipped rejects its promise with this
    // message; Safari surfaces it as unhandled. Nothing failed for the
    // reader, so it is not an exception.
    var msg = String((r && (r.message || r)) || "");
    if (msg.indexOf("skipTransition") >= 0 || msg.indexOf("Skipping view transition") >= 0) { return; }
    post("unhandledrejection",
      (r && (r.message || r)) || "unhandled promise rejection",
      "", 0, 0, r && r.stack);
  });
})();

// Back-forward cache. Safari and Chrome restore a page from bfcache
// with the DOM it had when the reader left — a landing that said
// "waiting for checks" keeps saying it, and the streams that would have
// patched it are closed. A restored live page is a stale page, so it
// reloads; a fresh navigation (persisted false) is untouched.
(function () {
  try {
    window.addEventListener("pageshow", function (ev) {
      if (ev && ev.persisted && document.querySelector("[data-init]")) { location.reload(); }
    });
  } catch (e) {}
})();