mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user