[eric] telemetry: crash recoveries, wedge recoveries, and rage clicks reach analytics (silent-failure family A)

This commit is contained in:
ciregenz
2026-08-06 22:45:32 -07:00
parent 565009f0c5
commit 751747b293
4 changed files with 82 additions and 1 deletions
+14
View File
@@ -1475,6 +1475,20 @@ function createWindow() {
mainWindow.webContents.on('preload-error', (_event, preloadPath, err) => {
console.error('[diag][main:preload-error]', preloadPath, err && err.stack || err);
});
// Frozen-but-not-crashed is the silent class no crash log sees; Chromium's own unresponsive
// signal costs nothing and the report fires from the renderer AFTER it recovers.
let wedgeStartedAt = 0;
mainWindow.webContents.on('unresponsive', () => {
wedgeStartedAt = Date.now();
console.error('[diag][main] renderer unresponsive');
});
mainWindow.webContents.on('responsive', () => {
if (!wedgeStartedAt) return;
const ms = Date.now() - wedgeStartedAt;
wedgeStartedAt = 0;
console.error('[diag][main] renderer responsive again after', ms, 'ms');
try { mainWindow.webContents.send('diag:wedge', { ms }); } catch (_) { /* window mid-teardown */ }
});
mainWindow.webContents.on('render-process-gone', (_event, details) => {
const reason = details && details.reason;
if (reason === 'clean-exit') return;
+6
View File
@@ -152,6 +152,12 @@ contextBridge.exposeInMainWorld('openswarm', {
openApplication: (name) => ipcRenderer.invoke('open-application', name),
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
getCrashRecoveryInfo: () => ipcRenderer.invoke('get-crash-recovery-info'),
// Fires after the renderer RECOVERS from a Chromium-detected freeze, with how long it was wedged.
onWedge: (cb) => {
const listener = (_e, info) => cb(info);
ipcRenderer.on('diag:wedge', listener);
return () => ipcRenderer.removeListener('diag:wedge', listener);
},
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
+13 -1
View File
@@ -66,6 +66,7 @@ if (typeof window !== 'undefined') {
else window.setTimeout(prefetchAll, 500);
}
import { report, reportAppOpened, getSessionTraceState, getRecentActions } from '@/shared/serviceClient';
import { installUxSignals } from '@/shared/uxSignals';
import { useRouteTracker } from '@/shared/hooks/useRouteTracker';
import { useDeepLink } from '@/shared/hooks/useDeepLink';
import { useWindowFocus } from '@/shared/hooks/useWindowFocus';
@@ -277,6 +278,7 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
if (!loaded) return;
(window as any).openswarm?.setAllowPrerelease?.(allowExperimentalUpdates);
}, [loaded, allowExperimentalUpdates]);
useEffect(() => installUxSignals(), []);
// Hold paint until the settings fetch SETTLES so the user's theme renders first; Electron's ready-to-show relies on this. Settling, not succeeding: a backend that never answers used to leave a blank window forever.
if (!settled) return null;
return <>{children}</>;
@@ -416,7 +418,17 @@ const CrashRecoveryChip: React.FC = () => {
const api = (window as any).openswarm as OpenSwarmAPI | undefined;
if (!api?.getCrashRecoveryInfo) return;
api.getCrashRecoveryInfo().then((info) => {
if (info) { setMounted(true); setShow(true); }
if (info) {
setMounted(true); setShow(true);
// The crash was captured locally but analytics never heard about it; a silent GPU/renderer
// death is exactly the failure class telemetry must count (flight-recorder family A).
const i = info as { kind?: string; details?: { reason?: string; exitCode?: number } };
report('process', 'crash_recovered', {
crash_kind: i.kind ?? 'unknown',
reason: i.details?.reason ?? null,
exit_code: i.details?.exitCode ?? null,
});
}
}).catch(() => {});
}, []);
React.useEffect(() => {
+49
View File
@@ -0,0 +1,49 @@
// Silent-failure UX sensors: rage clicks (the user telling us a button did nothing) and renderer
// wedge recoveries. Threshold-emission only; nothing here runs per-frame or per-render.
import { report } from '@/shared/serviceClient';
const RAGE_COUNT = 3;
const RAGE_WINDOW_MS = 2000;
const RAGE_THROTTLE_MS = 60_000;
let p_lastTarget: EventTarget | null = null;
let p_clickTimes: number[] = [];
let p_lastRageReport = 0;
function describeTarget(el: Element | null): string {
if (!el) return 'unknown';
const sel = el.closest('[data-select-type]');
if (sel) return sel.getAttribute('data-select-type') || 'card';
const btn = el.closest('button, [role="button"]');
if (btn) return (btn.getAttribute('aria-label') || btn.textContent || 'button').trim().slice(0, 40);
return el.tagName.toLowerCase();
}
export function installUxSignals(): () => void {
const onClick = (e: MouseEvent): void => {
const now = Date.now();
if (e.target !== p_lastTarget) {
p_lastTarget = e.target;
p_clickTimes = [now];
return;
}
p_clickTimes = [...p_clickTimes.filter((t) => now - t < RAGE_WINDOW_MS), now];
if (p_clickTimes.length >= RAGE_COUNT && now - p_lastRageReport > RAGE_THROTTLE_MS) {
p_lastRageReport = now;
report('ux', 'rage_click', {
target: describeTarget(e.target as Element | null),
clicks: p_clickTimes.length,
});
p_clickTimes = [];
}
};
window.addEventListener('click', onClick, true);
const bridge = window as unknown as { openswarm?: { onWedge?: (cb: (info: { ms: number }) => void) => () => void } };
const offWedge = bridge.openswarm?.onWedge?.((info) => {
report('process', 'wedge_recovered', { wedge_ms: info?.ms ?? -1 });
});
return () => {
window.removeEventListener('click', onClick, true);
offWedge?.();
};
}