fix(studio): stop composition fetch-404 flood and cap error telemetry

Two fixes for the 3M+ unhandled_promise_rejection events/day spike:

1. Filter: suppress "Error fetching ... 404" rejections from composition
   code — these are asset-not-found content errors, not Studio bugs.

2. Rate-limit: cap both error and rejection telemetry at 50 per session.
   After the cap, emit a single *_cap_reached event so we know capping
   occurred without generating unlimited events.

3. Root cause: webAudioTransport now checks response.ok before decode
   and caches failed URLs in _failedSrcs so repeat ticks don't re-fetch
   the same 404 on every playback frame.

Also add playground/ to fallow ignorePatterns — local experiment
directory was tripping the audit gate.
This commit is contained in:
Miguel Ángel
2026-05-22 13:22:00 -04:00
parent aebb7b2660
commit 36de02c4bf
3 changed files with 41 additions and 1 deletions
+1
View File
@@ -34,6 +34,7 @@
"skills/**/scripts/**", "skills/**/scripts/**",
"registry/**", "registry/**",
"examples/**", "examples/**",
"playground/**",
".github/workflows/fixtures/**", ".github/workflows/fixtures/**",
], ],
"ignoreExports": [ "ignoreExports": [
@@ -18,6 +18,7 @@ export type ScheduledSource = {
export class WebAudioTransport { export class WebAudioTransport {
private _ctx: AudioContext | null = null; private _ctx: AudioContext | null = null;
private _bufferCache = new Map<string, AudioBuffer>(); private _bufferCache = new Map<string, AudioBuffer>();
private _failedSrcs = new Set<string>();
private _activeSources: ScheduledSource[] = []; private _activeSources: ScheduledSource[] = [];
private _masterGain: GainNode | null = null; private _masterGain: GainNode | null = null;
// Composition-time reference frame: at AudioContext time `_rateAnchorCtx`, // Composition-time reference frame: at AudioContext time `_rateAnchorCtx`,
@@ -53,14 +54,21 @@ export class WebAudioTransport {
const src = el.currentSrc || el.getAttribute("src"); const src = el.currentSrc || el.getAttribute("src");
if (!src) return null; if (!src) return null;
if (this._bufferCache.has(src)) return this._bufferCache.get(src)!; if (this._bufferCache.has(src)) return this._bufferCache.get(src)!;
if (this._failedSrcs.has(src)) return null;
if (!this._ctx) return null; if (!this._ctx) return null;
try { try {
const response = await fetch(src); const response = await fetch(src);
if (!response.ok) {
this._failedSrcs.add(src);
swallow("webAudioTransport.fetch", new Error(`${response.status} ${src}`));
return null;
}
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this._ctx.decodeAudioData(arrayBuffer); const audioBuffer = await this._ctx.decodeAudioData(arrayBuffer);
this._bufferCache.set(src, audioBuffer); this._bufferCache.set(src, audioBuffer);
return audioBuffer; return audioBuffer;
} catch (err) { } catch (err) {
this._failedSrcs.add(src);
swallow("webAudioTransport.decode", err); swallow("webAudioTransport.decode", err);
return null; return null;
} }
+32 -1
View File
@@ -22,6 +22,16 @@ function errorProps(value: unknown): {
return { error_message: String(value), error_name: null, stack_trace: null }; return { error_message: String(value), error_name: null, stack_trace: null };
} }
function isCompositionAssetError(msg: string): boolean {
return msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found"));
}
const ERROR_CAP = 50;
let errorCount = 0;
let rejectionCount = 0;
let errorCapSent = false;
let rejectionCapSent = false;
window.addEventListener("error", (event) => { window.addEventListener("error", (event) => {
if (event.message?.includes("ResizeObserver loop")) { if (event.message?.includes("ResizeObserver loop")) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@@ -29,6 +39,15 @@ window.addEventListener("error", (event) => {
return; return;
} }
errorCount++;
if (errorCount > ERROR_CAP) {
if (!errorCapSent) {
errorCapSent = true;
trackStudioEvent("error_cap_reached", { count: errorCount });
}
return;
}
trackStudioEvent("unhandled_error", { trackStudioEvent("unhandled_error", {
...errorProps(event.error), ...errorProps(event.error),
error_message: event.message, error_message: event.message,
@@ -39,7 +58,19 @@ window.addEventListener("error", (event) => {
}); });
window.addEventListener("unhandledrejection", (event) => { window.addEventListener("unhandledrejection", (event) => {
trackStudioEvent("unhandled_promise_rejection", errorProps(event.reason)); const props = errorProps(event.reason);
if (isCompositionAssetError(props.error_message)) return;
rejectionCount++;
if (rejectionCount > ERROR_CAP) {
if (!rejectionCapSent) {
rejectionCapSent = true;
trackStudioEvent("rejection_cap_reached", { count: rejectionCount });
}
return;
}
trackStudioEvent("unhandled_promise_rejection", props);
}); });
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(