mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 03:37:44 +02:00
[eric] telemetry: crash recoveries, wedge recoveries, and rage clicks reach analytics (silent-failure family A)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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?.();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user