From 909102584bedef50a462f199bed7fd1b1fde6d61 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Mon, 4 May 2026 14:32:28 +0200 Subject: [PATCH] feat: write renderer and main traces with crashreporter --- apps/code/src/main/bootstrap.ts | 16 ++++++- apps/code/src/main/index.ts | 70 ++++++++++++++++++++++++++- apps/code/src/main/menu.ts | 85 +++++++++++++++++++++++++++++++++ apps/code/src/main/preload.ts | 9 ++++ apps/code/src/main/window.ts | 6 ++- mprocs.yaml | 3 ++ 6 files changed, 186 insertions(+), 3 deletions(-) diff --git a/apps/code/src/main/bootstrap.ts b/apps/code/src/main/bootstrap.ts index 591a247f68..a216a1cf3d 100644 --- a/apps/code/src/main/bootstrap.ts +++ b/apps/code/src/main/bootstrap.ts @@ -15,8 +15,10 @@ */ import dns from "node:dns"; +import { mkdirSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; -import { app, protocol } from "electron"; +import { app, crashReporter, protocol } from "electron"; import { fixPath } from "./utils/fixPath"; const isDev = !app.isPackaged; @@ -37,6 +39,18 @@ process.env.POSTHOG_CODE_DATA_DIR = userDataPath; process.env.POSTHOG_CODE_IS_DEV = String(isDev); process.env.POSTHOG_CODE_VERSION = app.getVersion(); +const logDir = path.join( + os.homedir(), + ".posthog-code", + isDev ? "logs-dev" : "logs", +); +mkdirSync(logDir, { recursive: true }); +app.commandLine.appendSwitch("enable-logging"); +app.commandLine.appendSwitch("log-file", path.join(logDir, "chromium.log")); +app.commandLine.appendSwitch("log-level", isDev ? "0" : "1"); + +crashReporter.start({ uploadToServer: false }); + // Force IPv4 resolution when "localhost" is used so the agent hits 127.0.0.1 // instead of ::1. This matches how the renderer already reaches the PostHog API. dns.setDefaultResultOrder("ipv4first"); diff --git a/apps/code/src/main/index.ts b/apps/code/src/main/index.ts index 9a9ea2c4e0..47e9031556 100644 --- a/apps/code/src/main/index.ts +++ b/apps/code/src/main/index.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; import os from "node:os"; -import { app } from "electron"; +import { app, BrowserWindow } from "electron"; import log from "electron-log/main"; import "./utils/logger"; import "./services/index.js"; @@ -19,6 +19,7 @@ import type { NotificationService } from "./services/notification/service"; import type { OAuthService } from "./services/oauth/service"; import { captureException, + getPostHogClient, initializePostHog, trackAppEvent, } from "./services/posthog-analytics"; @@ -38,6 +39,73 @@ if (!gotTheLock) { process.exit(0); } +app.on("render-process-gone", (_event, webContents, details) => { + const props = { + source: "main", + type: "render-process-gone", + reason: details.reason, + exitCode: String(details.exitCode), + url: webContents.getURL(), + title: webContents.getTitle(), + webContentsId: String(webContents.id), + }; + log.error("Renderer process gone", props); + captureException( + new Error(`Renderer process gone: ${details.reason}`), + props, + ); + getPostHogClient() + ?.flush() + .catch(() => {}); + + const recoverableReasons = new Set([ + "abnormal-exit", + "killed", + "crashed", + "oom", + "integrity-failure", + "memory-eviction", + ]); + if (recoverableReasons.has(details.reason)) { + log.info("Recovering from renderer crash", { reason: details.reason }); + const win = BrowserWindow.fromWebContents(webContents); + if (!win || win.isDestroyed()) { + log.warn("No window to recover"); + return; + } + setImmediate(() => { + if (win.isDestroyed()) return; + log.info("Reloading webContents"); + win.webContents.reload(); + log.info("Bringing window to foreground"); + win.show(); + win.moveTop(); + win.focus(); + app.focus({ steal: true }); + }); + } +}); + +app.on("child-process-gone", (_event, details) => { + const props = { + source: "main", + type: "child-process-gone", + processType: details.type, + reason: details.reason, + exitCode: String(details.exitCode), + serviceName: details.serviceName ?? "", + name: details.name ?? "", + }; + log.error("Child process gone", props); + captureException( + new Error(`Child process gone (${details.type}): ${details.reason}`), + props, + ); + getPostHogClient() + ?.flush() + .catch(() => {}); +}); + async function initializeServices(): Promise { container.get(MAIN_TOKENS.DatabaseService); container.get(MAIN_TOKENS.OAuthService); diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index 3474ba5531..18b4a9fa67 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -1,4 +1,6 @@ +import { readdirSync, statSync } from "node:fs"; import os from "node:os"; +import path from "node:path"; import { app, BrowserWindow, @@ -17,6 +19,31 @@ import type { UpdatesService } from "./services/updates/service"; import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; +function findLatestCrashDump(): string | null { + const pendingDir = path.join(app.getPath("crashDumps"), "pending"); + let entries: string[]; + try { + entries = readdirSync(pendingDir); + } catch { + return null; + } + let latest: { file: string; mtimeMs: number } | null = null; + for (const name of entries) { + if (!name.endsWith(".dmp")) continue; + const full = path.join(pendingDir, name); + let mtimeMs: number; + try { + mtimeMs = statSync(full).mtimeMs; + } catch { + continue; + } + if (!latest || mtimeMs > latest.mtimeMs) { + latest = { file: full, mtimeMs }; + } + } + return latest?.file ?? null; +} + function getSystemInfo(): string { const commit = __BUILD_COMMIT__ ?? "dev"; const buildDate = __BUILD_DATE__ ?? "dev"; @@ -124,6 +151,64 @@ function buildFileMenu(): MenuItemConstructorOptions { shell.showItemInFolder(getLogFilePath()); }, }, + { + label: + process.platform === "darwin" + ? "Show crash dumps in Finder" + : "Show crash dumps in file manager", + click: () => { + const latest = findLatestCrashDump(); + if (latest) { + shell.showItemInFolder(latest); + return; + } + const pendingDir = path.join( + app.getPath("crashDumps"), + "pending", + ); + void shell.openPath(pendingDir).then((err) => { + if (err) void shell.openPath(app.getPath("crashDumps")); + }); + }, + }, + ...(isDevBuild() + ? [ + { + label: "Test: terminate renderer (forced shutdown, no fault)", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (!win) return; + win.webContents.forcefullyCrashRenderer(); + }, + }, + { + label: "Test: crash renderer (in-process, EXC_BAD_ACCESS)", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (!win) return; + void win.webContents.executeJavaScript( + "window.__posthogCodeTest.crash()", + ); + }, + }, + { + label: "Test: abort renderer (in-process, SIGABRT)", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (!win) return; + void win.webContents.executeJavaScript( + "window.__posthogCodeTest.abort()", + ); + }, + }, + { + label: "Test: crash main process (SIGABRT)", + click: () => { + process.crash(); + }, + }, + ] + : []), { type: "separator" }, { label: "Invalidate OAuth token", diff --git a/apps/code/src/main/preload.ts b/apps/code/src/main/preload.ts index 51b354da7d..b57aa86612 100644 --- a/apps/code/src/main/preload.ts +++ b/apps/code/src/main/preload.ts @@ -6,6 +6,15 @@ contextBridge.exposeInMainWorld("electronUtils", { getPathForFile: (file: File) => webUtils.getPathForFile(file), }); +contextBridge.exposeInMainWorld("__posthogCodeTest", { + crash: () => { + process.crash(); + }, + abort: () => { + process.abort(); + }, +}); + process.once("loaded", async () => { exposeElectronTRPC(); }); diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index e8920c6b29..d41ce35ec0 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { createIPCHandler } from "@posthog/electron-trpc/main"; import { + app, BrowserWindow, Menu, type MenuItemConstructorOptions, @@ -53,7 +54,7 @@ function getSavedWindowState(): WindowStateSchema { return state; } -function saveWindowState(window: BrowserWindow): void { +export function saveWindowState(window: BrowserWindow): void { const isMaximized = window.isMaximized(); windowStateStore.set("isMaximized", isMaximized); @@ -183,6 +184,9 @@ export function createWindow(): void { mainWindow?.maximize(); } mainWindow?.show(); + mainWindow?.moveTop(); + mainWindow?.focus(); + app.focus({ steal: true }); }; mainWindow.once("ready-to-show", showWindow); diff --git a/mprocs.yaml b/mprocs.yaml index da56ee4d2f..0b6b1d45f8 100644 --- a/mprocs.yaml +++ b/mprocs.yaml @@ -28,3 +28,6 @@ procs: 6-mobile-ios: shell: 'node scripts/pnpm-run.mjs --filter @posthog/mobile run ios' autostart: false + + 7-chromium-log: + shell: 'tail -F ~/.posthog-code/logs-dev/chromium.log'