- From: YTERGamer <notifications@github.com>
- Date: Fri, 31 Oct 2025 21:42:23 -0700
- To: whatwg/fetch <fetch@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/1874@github.com>
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