feat(studio): add resolver-shadow attempt counter for soak-gate denominator (#1926)

* feat(studio): add resolver-shadow attempt counter for soak-gate denominator

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(studio): harden attempt-counter exception safety and tab-hide flush ordering

PR review feedback (4 reviewers): recordAttempt() sat outside the try/catch
in all three emit functions, so a throw inside it (e.g. setInterval/
addEventListener failing in a non-standard environment) would break the
"never throws" contract. Also, the new visibilitychange listener races
studioTelemetry.ts's own tab-hide handler — whichever fires first can beacon
the queue before or after this module's rollup lands in it, silently
dropping the attempt count for short sessions closed before the 5-minute
timer fires.

Fixes: move recordAttempt() inside each function's try block; export
flushViaBeacon() from studioTelemetry.ts and call it explicitly after
queuing the rollup, so delivery no longer depends on listener registration
order; capture the visibilitychange handler by reference so
__resetAttemptSchedulingForTests() actually removes it instead of leaking a
duplicate on re-arm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-03 21:45:23 -07:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 74faa4b2a4
commit fe821ff8bc
3 changed files with 301 additions and 22 deletions
+27 -20
View File
@@ -90,27 +90,34 @@ async function flushEvents(): Promise<void> {
}
}
// Synchronously drains the queue via sendBeacon — safe to call from any
// tab-hide handler regardless of listener registration order. Exported so
// other modules (e.g. sdkResolverShadow.ts) can force delivery of an event
// they just queued without racing this module's own visibilitychange
// listener below.
export function flushViaBeacon(): void {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
if (queue.length === 0) return;
const batch = queue.map((e) => ({
event: e.event,
properties: { ...e.properties, $ip: null },
distinct_id: getDistinctId(),
timestamp: e.timestamp,
}));
queue = [];
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
try {
navigator.sendBeacon(`${POSTHOG_HOST}/batch/`, body);
} catch {
// best-effort
}
}
if (typeof window !== "undefined") {
window.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
if (queue.length === 0) return;
const batch = queue.map((e) => ({
event: e.event,
properties: { ...e.properties, $ip: null },
distinct_id: getDistinctId(),
timestamp: e.timestamp,
}));
queue = [];
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
try {
navigator.sendBeacon(`${POSTHOG_HOST}/batch/`, body);
} catch {
// best-effort
}
}
if (document.visibilityState === "hidden") flushViaBeacon();
});
}