Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/code/src/main/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

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 { fixPath } from "./utils/fixPath";
Expand All @@ -37,6 +39,24 @@ process.env.POSTHOG_CODE_DATA_DIR = userDataPath;
process.env.POSTHOG_CODE_IS_DEV = String(isDev);
process.env.POSTHOG_CODE_VERSION = app.getVersion();

// Enable Chromium internal logging to a dedicated file. Without this, Chromium
// crashes (black screens, render-process-gone, GPU process death) leave no
// trail because Electron silently swallows the underlying logs. Must run
// before app.whenReady() so the switches take effect on the GPU/renderer
// child processes.
const chromiumLogDir = path.join(
os.homedir(),
".posthog-code",
isDev ? "logs-dev" : "logs",
);
Comment on lines +47 to +51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Log directory path duplicated (OnceAndOnlyOnce)

The same expression — join(os.homedir(), ".posthog-code", isDev ? "logs-dev" : "logs") — already appears verbatim in logger.ts as LOG_DIR. If the log directory ever changes, both sites need updating in sync. Consider extracting a shared helper (e.g. a small utils/paths.ts exporting getLogDir(isDev)) that both bootstrap.ts and logger.ts can import without creating a circular dependency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/bootstrap.ts
Line: 47-51

Comment:
**Log directory path duplicated (OnceAndOnlyOnce)**

The same expression — `join(os.homedir(), ".posthog-code", isDev ? "logs-dev" : "logs")` — already appears verbatim in `logger.ts` as `LOG_DIR`. If the log directory ever changes, both sites need updating in sync. Consider extracting a shared helper (e.g. a small `utils/paths.ts` exporting `getLogDir(isDev)`) that both `bootstrap.ts` and `logger.ts` can import without creating a circular dependency.

How can I resolve this? If you propose a fix, please make it concise.

mkdirSync(chromiumLogDir, { recursive: true });
const chromiumLogPath = path.join(chromiumLogDir, "chromium.log");
process.env.ELECTRON_ENABLE_LOGGING = "1";
process.env.POSTHOG_CODE_CHROMIUM_LOG_PATH = chromiumLogPath;
app.commandLine.appendSwitch("enable-logging", "file");
app.commandLine.appendSwitch("log-file", chromiumLogPath);
app.commandLine.appendSwitch("log-level", "0");

// 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");
Expand Down
19 changes: 19 additions & 0 deletions apps/code/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import type { TaskLinkService } from "./services/task-link/service";
import type { UpdatesService } from "./services/updates/service";
import type { WorkspaceService } from "./services/workspace/service";
import { ensureClaudeConfigDir } from "./utils/env";
import {
getChromiumLogFilePath,
getLogFilePath,
readChromiumLogTail,
} from "./utils/logger";
import { createWindow } from "./window";

// Single instance lock must be acquired FIRST before any other app setup
Expand Down Expand Up @@ -92,6 +97,9 @@ app.whenReady().then(async () => {
`OS: ${process.platform} ${process.arch} ${os.release()}`,
].join(" | "),
);
log.info(
`Logs: main=${getLogFilePath()} chromium=${getChromiumLogFilePath() ?? "(disabled)"}`,
);
ensureClaudeConfigDir();
registerMcpSandboxProtocol();
createWindow();
Expand All @@ -103,6 +111,17 @@ app.on("window-all-closed", () => {
app.quit();
});

app.on("child-process-gone", (_event, details) => {
log.error("Child process gone", {
type: details.type,
reason: details.reason,
exitCode: details.exitCode,
serviceName: details.serviceName,
name: details.name,
chromiumLogTail: readChromiumLogTail(),
});
});

app.on("before-quit", async (event) => {
let lifecycleService: AppLifecycleService;
try {
Expand Down
49 changes: 48 additions & 1 deletion apps/code/src/main/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
import {
closeSync,
existsSync,
fstatSync,
mkdirSync,
openSync,
readSync,
renameSync,
unlinkSync,
} from "node:fs";
import os from "node:os";
import { join } from "node:path";
import { initOtelTransport } from "@main/utils/otel-log-transport";
Expand Down Expand Up @@ -57,3 +66,41 @@ export { shutdownOtelTransport } from "@main/utils/otel-log-transport";
export function getLogFilePath(): string {
return join(LOG_DIR, LOG_FILE);
}

export function getChromiumLogFilePath(): string | undefined {
return process.env.POSTHOG_CODE_CHROMIUM_LOG_PATH;
}

const CHROMIUM_LOG_TAIL_BYTES = 32 * 1024;

/**
* Read the last ~32 KB of the Chromium internal log file. Used by crash
* handlers to attach the tail to OTEL/electron-log so PostHog gets the native
* V8/GPU output around a renderer death — Chromium writes chromium.log
* directly from native code and never goes through electron-log otherwise.
*/
export function readChromiumLogTail(): string | undefined {
const path = getChromiumLogFilePath();
if (!path) return undefined;

let fd: number | undefined;
try {
fd = openSync(path, "r");
const { size } = fstatSync(fd);
if (size === 0) return undefined;
const length = Math.min(size, CHROMIUM_LOG_TAIL_BYTES);
const buf = Buffer.alloc(length);
readSync(fd, buf, 0, length, size - length);
return buf.toString("utf8");
Comment on lines +93 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 readSync return value not used to bound toString

readSync returns the number of bytes actually read, which may be less than length (e.g. if the file was truncated between the fstatSync and readSync calls). Since Buffer.alloc zero-fills, any unread bytes at the end become null bytes in the UTF-8 string. Using bytesRead to slice the buffer is safer:

Suggested change
readSync(fd, buf, 0, length, size - length);
return buf.toString("utf8");
const bytesRead = readSync(fd, buf, 0, length, size - length);
return buf.subarray(0, bytesRead).toString("utf8");
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/utils/logger.ts
Line: 93-94

Comment:
**`readSync` return value not used to bound `toString`**

`readSync` returns the number of bytes actually read, which may be less than `length` (e.g. if the file was truncated between the `fstatSync` and `readSync` calls). Since `Buffer.alloc` zero-fills, any unread bytes at the end become null bytes in the UTF-8 string. Using `bytesRead` to slice the buffer is safer:

```suggestion
    const bytesRead = readSync(fd, buf, 0, length, size - length);
    return buf.subarray(0, bytesRead).toString("utf8");
```

How can I resolve this? If you propose a fix, please make it concise.

} catch {
return undefined;
} finally {
if (fd !== undefined) {
try {
closeSync(fd);
} catch {
// Best-effort close
}
}
}
}
Comment on lines +82 to +106
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Chromium log file grows unboundedly

chromium.log has no size limit or rotation, unlike main.log which is capped at 10 MB with 3 archives. With --log-level=0 (VERBOSE), Chromium is extremely chatty during normal operation and can produce several megabytes per session. Over time this will silently consume disk space with no cleanup mechanism. The same log directory already implements rotation for the main log — the same approach (or at least a maxSize guard that truncates the file at startup) should be applied here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/utils/logger.ts
Line: 82-106

Comment:
**Chromium log file grows unboundedly**

`chromium.log` has no size limit or rotation, unlike `main.log` which is capped at 10 MB with 3 archives. With `--log-level=0` (VERBOSE), Chromium is extremely chatty during normal operation and can produce several megabytes per session. Over time this will silently consume disk space with no cleanup mechanism. The same log directory already implements rotation for the main log — the same approach (or at least a `maxSize` guard that truncates the file at startup) should be applied here.

How can I resolve this? If you propose a fix, please make it concise.

25 changes: 24 additions & 1 deletion apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { buildApplicationMenu } from "./menu";
import type { ElectronMainWindow } from "./platform-adapters/electron-main-window";
import { trpcRouter } from "./trpc/router";
import { isDevBuild } from "./utils/env";
import { logger } from "./utils/logger";
import { logger, readChromiumLogTail } from "./utils/logger";
import { type WindowStateSchema, windowStateStore } from "./utils/store";

const log = logger.scope("window");
Expand Down Expand Up @@ -103,6 +103,28 @@ function setupExternalLinkHandlers(window: BrowserWindow): void {
});
}

function setupCrashLogging(window: BrowserWindow): void {
window.webContents.on("render-process-gone", (_event, details) => {
log.error("Renderer process gone", {
reason: details.reason,
exitCode: details.exitCode,
url: window.webContents.getURL(),
chromiumLogTail: readChromiumLogTail(),
});
});

window.on("unresponsive", () => {
log.warn("Window unresponsive", {
url: window.webContents.getURL(),
chromiumLogTail: readChromiumLogTail(),
});
});

window.on("responsive", () => {
log.info("Window responsive again");
});
}

function setupEditableContextMenu(window: BrowserWindow): void {
window.webContents.on("context-menu", (_event, params) => {
if (!params.isEditable) return;
Expand Down Expand Up @@ -212,6 +234,7 @@ export function createWindow(): void {

setupExternalLinkHandlers(mainWindow);
setupEditableContextMenu(mainWindow);
setupCrashLogging(mainWindow);
buildApplicationMenu();

if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
Expand Down
Loading