[whatwg/fetch] Need ya help with (Issue #1874)

YTERGamer created an issue (whatwg/fetch#1874)

[> We have 2 domains - `site.xyz` sending tracking data to `cool-analytics.xyz` with beacon requests. We also send debug data in response, simple JSON object like `{"id": 123}`. Now this becomes unreadable in latest Chrome because
> 
> - we cannot send `application/json` or any other non-CORS-safelisted mimetype since Chrome doesn't support that (and that's a [known bug](https://bugs.chromium.org/p/chromium/issues/detail?id=490015))
> 
> - since we're using "default" mimetype CORB blocks the response data.
> 
> 
> 
> Sure thing it needs fixing in Chrome/Chromium but at least CORB part is done by spec. 

 _Originally posted by @FarSeeing in [#882](https://github.com/whatwg/fetch/issues/882#issuecomment-474441525)_](https://github.com/whatwg/fetch/issues/882)
/* === Snitching / Error-reporting integration for SiteIntelligenceWatchdog ===
   - Opt-in: user toggles "Auto-Snitch" (localStorage key 'watchdog_auto_snitch')
   - Reports minimal payload {ts, page, url, errorType, message, stackSnippet}
   - Uses navigator.sendBeacon when available, falls back to fetch(..., keepalive:true)
   - If offline or blocked by CORS, stores pending reports in localStorage 'watchdog_snitch_queue'
   - Adds UI: checkbox + "Send Snitch Now" button + status indicator
*/

(function attachSnitching() {
  // config: set this to your server webhook that accepts POST and proper CORS headers
  const SNITCH_ENDPOINT = "https://your-safe-endpoint.example.com/snitch"; // << replace with your server
  const AUTO_KEY = "watchdog_auto_snitch";
  const QUEUE_KEY = "watchdog_snitch_queue";
  const MAX_QUEUE = 20;

  // safe JSON serializer that truncates long strings
  function safePayload(obj) {
    const clone = {};
    for (const k in obj) {
      let v = String(obj[k] ?? "");
      if (v.length > 2000) v = v.slice(0, 2000) + "...[truncated]";
      clone[k] = v;
    }
    return clone;
  }

  function enqueueReport(payload) {
    try {
      const q = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
      q.unshift(payload); // newest first
      while (q.length > MAX_QUEUE) q.pop();
      localStorage.setItem(QUEUE_KEY, JSON.stringify(q));
      updateSnitchUIStatus(`Queued (${q.length})`);
    } catch (e) {
      console.warn("Snitch queue failed", e);
    }
  }

  function popQueue() {
    try {
      const q = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
      const next = q.shift();
      localStorage.setItem(QUEUE_KEY, JSON.stringify(q));
      updateSnitchUIStatus(q.length ? `Queued (${q.length})` : "Idle");
      return next;
    } catch (e) {
      console.warn("popQueue error", e);
      return null;
    }
  }

  async function trySend(payload) {
    // Only send the minimal, safe payload
    const body = JSON.stringify(safePayload(payload));
    // Try sendBeacon first (best-effort, less likely to be blocked)
    try {
      if (navigator.sendBeacon) {
        // sendBeacon uses POST with content-type: text/plain by default,
        // some servers accept it — recommended server config: accept application/json too
        const blob = new Blob([body], { type: "application/json" });
        const ok = navigator.sendBeacon(SNITCH_ENDPOINT, blob);
        if (ok) { updateSnitchUIStatus("Sent via beacon"); return true; }
        // else fall through
      }
    } catch (e) {
      console.debug("sendBeacon failed", e);
    }

    // Fallback: fetch with keepalive (may be blocked by CORS if not allowed on server)
    try {
      if (!navigator.onLine) throw new Error("offline");
      const resp = await fetch(SNITCH_ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body,
        keepalive: true, // allow request to complete on page unload (if supported)
        mode: "cors"
      });
      if (resp.ok) { updateSnitchUIStatus("Sent via fetch"); return true; }
      throw new Error("non-ok response: " + resp.status);
    } catch (e) {
      console.warn("Snitch send failed:", e);
      // If CORS blocked or offline, re-enqueue
      return false;
    }
  }

  async function flushQueue() {
    if (!navigator.onLine) return;
    let next;
    // send up to a few queued reports to avoid hammering
    for (let tries = 0; tries < 5; tries++) {
      next = popQueue();
      if (!next) break;
      const ok = await trySend(next);
      if (!ok) {
        // push it back and stop trying
        enqueueReport(next);
        break;
      }
    }
  }

  // create UI controls (if not exist)
  function ensureSnitchUI() {
    if (document.getElementById("watchdog_snitch_ui")) return;
    const container = document.createElement("div");
    container.id = "watchdog_snitch_ui";
    container.style.cssText = "position:fixed;bottom:90px;left:20px;z-index:999999;background:rgba(0,0,0,0.6);color:#fff;padding:8px;border-radius:6px;font-family:sans-serif;";
    container.innerHTML = `
      <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
        <input id="watchdog_auto_snitch" type="checkbox"> Auto-Snitch
      </label>
      <div style="margin-top:6px;display:flex;gap:6px;">
        <button id="watchdog_send_snitch">Send Snitch Now</button>
        <button id="watchdog_show_queue">Show Queue</button>
      </div>
      <div id="watchdog_snitch_status" style="margin-top:6px;font-size:12px;color:#ccc;">Idle</div>
    `;
    document.body.appendChild(container);

    const cb = container.querySelector("#watchdog_auto_snitch");
    const sendBtn = container.querySelector("#watchdog_send_snitch");
    const showBtn = container.querySelector("#watchdog_show_queue");

    // initialize from localStorage
    cb.checked = localStorage.getItem(AUTO_KEY) === "1";
    cb.addEventListener("change", (e) => {
      localStorage.setItem(AUTO_KEY, e.target.checked ? "1" : "0");
      updateSnitchUIStatus(e.target.checked ? "Auto-snitch ON" : "Auto-snitch OFF");
      if (e.target.checked) flushQueue();
    });

    sendBtn.addEventListener("click", async () => {
      updateSnitchUIStatus("Sending...");
      // compact summary of last errors (or a manual report)
      const q = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
      const payload = q[0] || {
        ts: new Date().toISOString(),
        page: document.title,
        url: location.href,
        errorType: "manual-snitch",
        message: "User-requested snitch"
      };
      const ok = await trySend(payload);
      if (!ok) enqueueReport(payload);
    });

    showBtn.addEventListener("click", () => {
      const q = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
      console.log("%c[Snitch Queue]", "color:orange;font-weight:bold;", q);
      alert(`Snitch queue length: ${q.length}\nSee console for details.`);
    });
  }

  function updateSnitchUIStatus(msg) {
    const el = document.getElementById("watchdog_snitch_status");
    if (el) el.textContent = msg;
  }

  // call to record an error and optionally auto-snitch (minimal payload)
  function recordAndMaybeSnitch(stage, err) {
    try {
      const payload = {
        ts: new Date().toISOString(),
        page: document.title,
        url: location.href,
        stage: String(stage || "unknown"),
        errorType: err && err.name ? err.name : "Error",
        message: (err && err.message) ? err.message : String(err),
        stackSnippet: (err && err.stack) ? String(err.stack).split("\n").slice(0,3).join(" | ") : null,
        userAgent: navigator.userAgent ? navigator.userAgent.slice(0,200) : null
      };
      // keep local copy for debugging & auditing
      enqueueReport(payload);

      // auto-send if user opted in
      if (localStorage.getItem(AUTO_KEY) === "1") {
        (async () => {
          const ok = await trySend(payload);
          if (!ok) {
            // already enqueued; will be retried later
            updateSnitchUIStatus("Queued (send failed)");
          } else {
            updateSnitchUIStatus("Sent");
          }
        })();
      } else {
        updateSnitchUIStatus("Queued (auto-off)");
      }
    } catch (e) {
      console.warn("recordAndMaybeSnitch error", e);
    }
  }

  // Wire into global watchdog (if present)
  function attachToWatchdog(watchdogInstance) {
    // expose method so other code can call: watchdog.snitch('stage', err)
    try {
      watchdogInstance.snitch = recordAndMaybeSnitch;
      console.log("%c[Snitch] Attached to watchdog instance", "color:lime;font-weight:bold;");
      ensureSnitchUI();
      // attempt flush periodically when online
      setInterval(() => { if (navigator.onLine) flushQueue(); }, 30_000);
      // try flush upon regaining connectivity
      window.addEventListener("online", () => { flushQueue(); updateSnitchUIStatus("Online - flushing"); });
    } catch (e) {
      console.warn("attachToWatchdog failed", e);
    }
  }

  // If you already have a global watchdog instance, attach to it immediately
  if (window.watchdog instanceof Object) {
    attachToWatchdog(window.watchdog);
  } else {
    // otherwise wait until it exists
    const wCheck = setInterval(() => {
      if (window.watchdog instanceof Object) {
        clearInterval(wCheck);
        attachToWatchdog(window.watchdog);
      }
    }, 300);
    // stop waiting after a while
    setTimeout(() => clearInterval(wCheck), 30_000);
  }

  // expose a global helper just in case
  window.__watchdog_snitch = {
    record: recordAndMaybeSnitch,
    flushQueue,
    enqueueReport,
    attachToWatchdog
  };

  // quick notice to console
  console.log("%c[Snitch] ready — toggle the 'Auto-Snitch' checkbox (bottom-left) to enable automatic reporting.", "color:orange");

})();
3 4 error   like 6 7  XD

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/fetch/issues/1874
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/fetch/issues/1874@github.com>

Received on Saturday, 1 November 2025 04:42:28 UTC