[eric] fix: fire the boot beacon only after backend-ready, not just first-paint, so it doesnt post into a dead socket

This commit is contained in:
Eric
2026-05-27 19:11:12 -07:00
parent 9990ab8e09
commit d4aa87a320
+17 -31
View File
@@ -53,43 +53,26 @@ function perfMark(name) {
try { console.log(`[perf] ${name} t=${t}`); } catch (_) {}
}
// Preflight: name the common "works on my machine, not theirs" causes up front,
// so a user-submitted backend.log explains a silent break without a repro. The
// existing python-exists log + the backend spawn 'error' handler already cover
// AV-quarantined / wrong-arch python; this adds the OTHER usual suspects. Pure
// logging, every probe individually guarded - it must never affect boot.
let _preflightInfo = {}; // captured by sendBootBeacon() below.
// Preflight: log the usual "works on mine, not theirs" causes (python is already covered by the exists-log + spawn handler; this adds the rest). Log-only, guarded, no PII (lengths/flags, never paths).
let _preflightInfo = {};
function logPreflight(backendPort) {
const info = {};
const probe = (label, fn) => { try { info[label] = fn(); } catch (_) { info[label] = 'ERR'; } };
try {
const userData = app.getPath('userData');
// Locked-down / read-only roaming profile -> the backend can't write its data.
probe('userDataWritable', () => { const t = path.join(userData, '.preflight'); fs.writeFileSync(t, 'x'); fs.unlinkSync(t); return true; });
// Non-ASCII or very long profile paths break some bundled tooling on Windows.
// We record only the length + an ascii flag, never the path itself (no PII).
probe('userDataAscii', () => /^[\x00-\x7F]*$/.test(userData));
probe('userDataLen', () => userData.length);
// OneDrive-redirected AppData is a known source of odd file-locking behavior.
probe('oneDriveProfile', () => /onedrive/i.test(userData));
// Falling out of the preferred range means the loopback probe was blocked
// (EDR/firewall) and we used an OS-assigned port - worth knowing on a break.
probe('portInPreferredRange', () => backendPort >= 8324 && backendPort <= 8424);
probe('freeDiskMB', () => Math.round((fs.statfsSync(userData).bavail * fs.statfsSync(userData).bsize) / 1048576));
// Bundled bits missing at launch = AV quarantine, wrong-arch, or partial install.
if (isPackaged) {
for (const bit of ['router', 'node', 'app.asar', 'frontend', 'backend', 'python-env']) {
probe(bit, () => fs.existsSync(getResourcePath(bit)));
}
}
if (isPackaged) for (const bit of ['router', 'node', 'app.asar', 'frontend', 'backend', 'python-env']) probe(bit, () => fs.existsSync(getResourcePath(bit)));
_preflightInfo = info;
console.log(`[preflight] ${Object.entries(info).map(([k, v]) => `${k}=${v}`).join(' | ')}`);
} catch (_) { /* preflight must never break boot */ }
} catch (_) { /* never break boot */ }
}
// crashReporter (top of file) keeps minidumps locally under userData/Crashpad.
// Count them so the boot beacon can attribute crashes to a version: a count that
// climbs release-over-release (the cloud diffs by install_id) flags a crashy build.
// Count local Crashpad minidumps so the beacon can flag a crashy build (the cloud diffs by install_id over time).
function countCrashDumps() {
try {
const base = path.join(app.getPath('userData'), 'Crashpad');
@@ -107,12 +90,7 @@ function countCrashDumps() {
} catch (_) { return -1; }
}
// Fleet self-report. After the UI paints, post a compact boot-outcome event to the
// LOCAL backend, which forwards it through the existing service client - so the
// user's analytics opt-out is honored and there's no separate network surface
// here. No PII: just the commit, perf marks, preflight flags (lengths/booleans,
// never paths), and a crash count. Enough to see "version X boots on real
// machines, version Y doesn't". Fire-and-forget + fully guarded; never affects boot.
// Fleet self-report: POST a compact boot outcome to the LOCAL backend, which forwards it via the existing service client (opt-out honored). No PII. Fire-and-forget, guarded.
function sendBootBeacon() {
try {
if (!isPackaged || !backendPort) return;
@@ -142,6 +120,15 @@ function sendBootBeacon() {
} catch (_) { /* beacon must never affect the app */ }
}
// Fire the beacon once first-paint AND backend-http-ready have both landed (the POST needs the backend listening); a touch later so it stays off the critical path.
let _beaconScheduled = false;
function maybeSendBootBeacon() {
if (_beaconScheduled) return;
if (_perfValues['first-paint'] == null || _perfValues['backend-http-ready'] == null) return;
_beaconScheduled = true;
setTimeout(() => sendBootBeacon(), 1500);
}
// Defender warmup: NSIS runs us with --prewarm right after install so Windows scans the bundled binaries while the user is already watching the installer instead of staring at a slow first launch.
if (process.argv.includes('--prewarm') && process.platform === 'win32') {
const touchExe = (rel) => {
@@ -824,6 +811,7 @@ async function startBackend() {
await waitForBackend(backendPort, { process: backendProcess });
perfMark('backend-http-ready');
console.log(`Backend ready on port ${backendPort}`);
maybeSendBootBeacon();
// Backend writes a per-install auth token file at startup. Read it
// here so the renderer can include it in WS URLs (`?token=...`) and
@@ -1001,9 +989,7 @@ function createWindow() {
// be a string (legacy) OR a {channel, url} object (v1.0.26+ OAuth claims).
mainWindow.webContents.once('did-finish-load', () => {
perfMark('first-paint');
// Off the critical path: once the UI has settled, report this boot's outcome
// so the fleet self-reports which builds boot cleanly on real machines.
setTimeout(() => sendBootBeacon(), 3000);
maybeSendBootBeacon();
if (pendingDeepLink) {
if (typeof pendingDeepLink === 'string') {
mainWindow.webContents.send('openswarm:auth-url', pendingDeepLink);