Skip to content

Commit 022b708

Browse files
authored
feat: write renderer and main traces with crashreporter (#2003)
1 parent 44f0b0d commit 022b708

6 files changed

Lines changed: 206 additions & 14 deletions

File tree

apps/code/src/main/bootstrap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import dns from "node:dns";
1818
import { mkdirSync } from "node:fs";
1919
import os from "node:os";
2020
import path from "node:path";
21-
import { app, protocol } from "electron";
21+
import { app, crashReporter, protocol } from "electron";
2222
import { fixPath } from "./utils/fixPath";
2323

2424
const isDev = !app.isPackaged;
@@ -57,6 +57,8 @@ app.commandLine.appendSwitch("enable-logging", "file");
5757
app.commandLine.appendSwitch("log-file", chromiumLogPath);
5858
app.commandLine.appendSwitch("log-level", "0");
5959

60+
crashReporter.start({ uploadToServer: false });
61+
6062
// Force IPv4 resolution when "localhost" is used so the agent hits 127.0.0.1
6163
// instead of ::1. This matches how the renderer already reaches the PostHog API.
6264
dns.setDefaultResultOrder("ipv4first");

apps/code/src/main/index.ts

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import "reflect-metadata";
22
import os from "node:os";
3-
import { app } from "electron";
3+
import { app, BrowserWindow } from "electron";
44
import log from "electron-log/main";
55
import "./utils/logger";
66
import "./services/index.js";
@@ -19,6 +19,7 @@ import type { NotificationService } from "./services/notification/service";
1919
import type { OAuthService } from "./services/oauth/service";
2020
import {
2121
captureException,
22+
getPostHogClient,
2223
initializePostHog,
2324
trackAppEvent,
2425
} from "./services/posthog-analytics";
@@ -43,6 +44,102 @@ if (!gotTheLock) {
4344
process.exit(0);
4445
}
4546

47+
const RECOVERABLE_RENDER_REASONS = new Set([
48+
"abnormal-exit",
49+
"killed",
50+
"crashed",
51+
"oom",
52+
"integrity-failure",
53+
"memory-eviction",
54+
]);
55+
const CRASH_LOOP_WINDOW_MS = 30_000;
56+
const CRASH_LOOP_THRESHOLD = 3;
57+
const recentCrashTimestamps: number[] = [];
58+
59+
function isCrashLoop(): boolean {
60+
const now = Date.now();
61+
while (
62+
recentCrashTimestamps.length > 0 &&
63+
now - recentCrashTimestamps[0] > CRASH_LOOP_WINDOW_MS
64+
) {
65+
recentCrashTimestamps.shift();
66+
}
67+
recentCrashTimestamps.push(now);
68+
return recentCrashTimestamps.length >= CRASH_LOOP_THRESHOLD;
69+
}
70+
71+
app.on("render-process-gone", (_event, webContents, details) => {
72+
const props = {
73+
source: "main",
74+
type: "render-process-gone",
75+
reason: details.reason,
76+
exitCode: String(details.exitCode),
77+
url: webContents.getURL(),
78+
title: webContents.getTitle(),
79+
webContentsId: String(webContents.id),
80+
};
81+
log.error("Renderer process gone", {
82+
...props,
83+
chromiumLogTail: readChromiumLogTail(),
84+
});
85+
captureException(
86+
new Error(`Renderer process gone: ${details.reason}`),
87+
props,
88+
);
89+
getPostHogClient()
90+
?.flush()
91+
.catch(() => {});
92+
93+
if (RECOVERABLE_RENDER_REASONS.has(details.reason)) {
94+
if (isCrashLoop()) {
95+
log.error("Crash loop detected, stopping auto-recovery", {
96+
crashesInWindow: recentCrashTimestamps.length,
97+
windowMs: CRASH_LOOP_WINDOW_MS,
98+
});
99+
return;
100+
}
101+
log.info("Recovering from renderer crash", { reason: details.reason });
102+
const win = BrowserWindow.fromWebContents(webContents);
103+
if (!win || win.isDestroyed()) {
104+
log.warn("No window to recover");
105+
return;
106+
}
107+
setImmediate(() => {
108+
if (win.isDestroyed()) return;
109+
log.info("Reloading webContents");
110+
win.webContents.reload();
111+
log.info("Bringing window to foreground");
112+
win.show();
113+
win.moveTop();
114+
win.focus();
115+
app.focus({ steal: true });
116+
});
117+
}
118+
});
119+
120+
app.on("child-process-gone", (_event, details) => {
121+
const props = {
122+
source: "main",
123+
type: "child-process-gone",
124+
processType: details.type,
125+
reason: details.reason,
126+
exitCode: String(details.exitCode),
127+
serviceName: details.serviceName ?? "",
128+
name: details.name ?? "",
129+
};
130+
log.error("Child process gone", {
131+
...props,
132+
chromiumLogTail: readChromiumLogTail(),
133+
});
134+
captureException(
135+
new Error(`Child process gone (${details.type}): ${details.reason}`),
136+
props,
137+
);
138+
getPostHogClient()
139+
?.flush()
140+
.catch(() => {});
141+
});
142+
46143
async function initializeServices(): Promise<void> {
47144
container.get<DatabaseService>(MAIN_TOKENS.DatabaseService);
48145
container.get<OAuthService>(MAIN_TOKENS.OAuthService);
@@ -111,17 +208,6 @@ app.on("window-all-closed", () => {
111208
app.quit();
112209
});
113210

114-
app.on("child-process-gone", (_event, details) => {
115-
log.error("Child process gone", {
116-
type: details.type,
117-
reason: details.reason,
118-
exitCode: details.exitCode,
119-
serviceName: details.serviceName,
120-
name: details.name,
121-
chromiumLogTail: readChromiumLogTail(),
122-
});
123-
});
124-
125211
app.on("before-quit", async (event) => {
126212
let lifecycleService: AppLifecycleService;
127213
try {

apps/code/src/main/menu.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { readdirSync, statSync } from "node:fs";
12
import os from "node:os";
3+
import path from "node:path";
24
import {
35
app,
46
BrowserWindow,
@@ -17,6 +19,31 @@ import type { UpdatesService } from "./services/updates/service";
1719
import { isDevBuild } from "./utils/env";
1820
import { getLogFilePath } from "./utils/logger";
1921

22+
function findLatestCrashDump(): string | null {
23+
const pendingDir = path.join(app.getPath("crashDumps"), "pending");
24+
let entries: string[];
25+
try {
26+
entries = readdirSync(pendingDir);
27+
} catch {
28+
return null;
29+
}
30+
let latest: { file: string; mtimeMs: number } | null = null;
31+
for (const name of entries) {
32+
if (!name.endsWith(".dmp")) continue;
33+
const full = path.join(pendingDir, name);
34+
let mtimeMs: number;
35+
try {
36+
mtimeMs = statSync(full).mtimeMs;
37+
} catch {
38+
continue;
39+
}
40+
if (!latest || mtimeMs > latest.mtimeMs) {
41+
latest = { file: full, mtimeMs };
42+
}
43+
}
44+
return latest?.file ?? null;
45+
}
46+
2047
function getSystemInfo(): string {
2148
const commit = __BUILD_COMMIT__ ?? "dev";
2249
const buildDate = __BUILD_DATE__ ?? "dev";
@@ -124,6 +151,64 @@ function buildFileMenu(): MenuItemConstructorOptions {
124151
shell.showItemInFolder(getLogFilePath());
125152
},
126153
},
154+
{
155+
label:
156+
process.platform === "darwin"
157+
? "Show crash dumps in Finder"
158+
: "Show crash dumps in file manager",
159+
click: () => {
160+
const latest = findLatestCrashDump();
161+
if (latest) {
162+
shell.showItemInFolder(latest);
163+
return;
164+
}
165+
const pendingDir = path.join(
166+
app.getPath("crashDumps"),
167+
"pending",
168+
);
169+
void shell.openPath(pendingDir).then((err) => {
170+
if (err) void shell.openPath(app.getPath("crashDumps"));
171+
});
172+
},
173+
},
174+
...(isDevBuild()
175+
? [
176+
{
177+
label: "Test: terminate renderer (forced shutdown, no fault)",
178+
click: () => {
179+
const win = BrowserWindow.getFocusedWindow();
180+
if (!win) return;
181+
win.webContents.forcefullyCrashRenderer();
182+
},
183+
},
184+
{
185+
label: "Test: crash renderer (in-process, EXC_BAD_ACCESS)",
186+
click: () => {
187+
const win = BrowserWindow.getFocusedWindow();
188+
if (!win) return;
189+
void win.webContents.executeJavaScript(
190+
"window.__posthogCodeTest.crash()",
191+
);
192+
},
193+
},
194+
{
195+
label: "Test: abort renderer (in-process, SIGABRT)",
196+
click: () => {
197+
const win = BrowserWindow.getFocusedWindow();
198+
if (!win) return;
199+
void win.webContents.executeJavaScript(
200+
"window.__posthogCodeTest.abort()",
201+
);
202+
},
203+
},
204+
{
205+
label: "Test: crash main process (SIGABRT)",
206+
click: () => {
207+
process.crash();
208+
},
209+
},
210+
]
211+
: []),
127212
{ type: "separator" },
128213
{
129214
label: "Invalidate OAuth token",

apps/code/src/main/preload.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ contextBridge.exposeInMainWorld("electronUtils", {
66
getPathForFile: (file: File) => webUtils.getPathForFile(file),
77
});
88

9+
if (process.argv.includes("--posthog-code-dev")) {
10+
contextBridge.exposeInMainWorld("__posthogCodeTest", {
11+
crash: () => {
12+
process.crash();
13+
},
14+
abort: () => {
15+
process.abort();
16+
},
17+
});
18+
}
19+
920
process.once("loaded", async () => {
1021
exposeElectronTRPC();
1122
});

apps/code/src/main/window.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from "node:path";
22
import { fileURLToPath } from "node:url";
33
import { createIPCHandler } from "@posthog/electron-trpc/main";
44
import {
5+
app,
56
BrowserWindow,
67
Menu,
78
type MenuItemConstructorOptions,
@@ -53,7 +54,7 @@ function getSavedWindowState(): WindowStateSchema {
5354
return state;
5455
}
5556

56-
function saveWindowState(window: BrowserWindow): void {
57+
export function saveWindowState(window: BrowserWindow): void {
5758
const isMaximized = window.isMaximized();
5859
windowStateStore.set("isMaximized", isMaximized);
5960

@@ -192,6 +193,7 @@ export function createWindow(): void {
192193
preload: path.join(__dirname, "preload.js"),
193194
enableBlinkFeatures: "GetDisplayMedia",
194195
partition: "persist:main",
196+
additionalArguments: isDev ? ["--posthog-code-dev"] : [],
195197
...(isDev && { webSecurity: false }),
196198
},
197199
});
@@ -205,6 +207,9 @@ export function createWindow(): void {
205207
mainWindow?.maximize();
206208
}
207209
mainWindow?.show();
210+
mainWindow?.moveTop();
211+
mainWindow?.focus();
212+
app.focus({ steal: true });
208213
};
209214

210215
mainWindow.once("ready-to-show", showWindow);

mprocs.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ procs:
2828
6-mobile-ios:
2929
shell: 'node scripts/pnpm-run.mjs --filter @posthog/mobile run ios'
3030
autostart: false
31+
32+
7-chromium-log:
33+
shell: 'tail -F ~/.posthog-code/logs-dev/chromium.log'

0 commit comments

Comments
 (0)