Skip to content

Commit bb60350

Browse files
committed
Bump version to 0.1.6 and add comprehensive crash logging system with process lifecycle tracking
- Bump version from 0.1.5 to 0.1.6 in package.json - Add crash-logger module with JSON-based log persistence to ~/.open-cowork/logs/crash-YYYY-MM-DD.log - Implement payload normalization with depth limiting, truncation (20k chars), and Error/Array/Object serialization - Add crash event handlers for uncaughtException, unhandledRejection, child-process-gone, render-process-gone, unresponsive, and did-fail
1 parent 679a408 commit bb60350

59 files changed

Lines changed: 2439 additions & 284 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-cowork",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "An Electron application with React and TypeScript",
55
"main": "./out/main/index.js",
66
"author": "example.com",

‎src/main/crash-logger.ts‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { app } from 'electron'
2+
import { appendFileSync, mkdirSync } from 'fs'
3+
import { release } from 'os'
4+
import { join } from 'path'
5+
import { getDataDir } from './db/database'
6+
7+
const LOG_DIR = join(getDataDir(), 'logs')
8+
const MAX_PAYLOAD_CHARS = 20_000
9+
const MAX_OBJECT_KEYS = 80
10+
const MAX_ARRAY_ITEMS = 50
11+
const MAX_DEPTH = 4
12+
13+
type JsonRecord = Record<string, unknown>
14+
15+
function ensureLogDir(): void {
16+
mkdirSync(LOG_DIR, { recursive: true })
17+
}
18+
19+
function getLogFilePath(now: Date): string {
20+
const date = now.toISOString().slice(0, 10)
21+
return join(LOG_DIR, `crash-${date}.log`)
22+
}
23+
24+
function getAppVersionSafe(): string {
25+
try {
26+
return app.getVersion()
27+
} catch {
28+
return 'unknown'
29+
}
30+
}
31+
32+
function normalizeUnknown(value: unknown, depth = 0): unknown {
33+
if (depth > MAX_DEPTH) return '[max-depth-exceeded]'
34+
if (value == null) return value
35+
36+
const t = typeof value
37+
if (t === 'string' || t === 'number' || t === 'boolean') return value
38+
if (t === 'bigint') return String(value)
39+
if (value instanceof Error) {
40+
const out: JsonRecord = {
41+
name: value.name,
42+
message: value.message,
43+
stack: value.stack,
44+
}
45+
const withCause = value as Error & { cause?: unknown }
46+
if (withCause.cause !== undefined) {
47+
out.cause = normalizeUnknown(withCause.cause, depth + 1)
48+
}
49+
return out
50+
}
51+
52+
if (Array.isArray(value)) {
53+
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => normalizeUnknown(item, depth + 1))
54+
}
55+
56+
if (t === 'object') {
57+
const out: JsonRecord = {}
58+
const entries = Object.entries(value as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS)
59+
for (const [k, v] of entries) {
60+
out[k] = normalizeUnknown(v, depth + 1)
61+
}
62+
return out
63+
}
64+
65+
return String(value)
66+
}
67+
68+
function truncatePayload(payload: unknown): unknown {
69+
try {
70+
const raw = JSON.stringify(payload)
71+
if (raw.length <= MAX_PAYLOAD_CHARS) return payload
72+
return {
73+
truncated: true,
74+
totalChars: raw.length,
75+
preview: raw.slice(0, MAX_PAYLOAD_CHARS),
76+
}
77+
} catch {
78+
return payload
79+
}
80+
}
81+
82+
export interface CrashLogEntry {
83+
timestamp: string
84+
event: string
85+
pid: number
86+
ppid: number
87+
appVersion: string
88+
platform: NodeJS.Platform
89+
osRelease: string
90+
versions: {
91+
electron?: string
92+
node?: string
93+
chrome?: string
94+
v8?: string
95+
}
96+
payload?: unknown
97+
}
98+
99+
export function writeCrashLog(event: string, payload?: unknown): void {
100+
try {
101+
ensureLogDir()
102+
const now = new Date()
103+
const entry: CrashLogEntry = {
104+
timestamp: now.toISOString(),
105+
event,
106+
pid: process.pid,
107+
ppid: process.ppid,
108+
appVersion: getAppVersionSafe(),
109+
platform: process.platform,
110+
osRelease: release(),
111+
versions: {
112+
electron: process.versions.electron,
113+
node: process.versions.node,
114+
chrome: process.versions.chrome,
115+
v8: process.versions.v8,
116+
},
117+
...(payload === undefined
118+
? {}
119+
: { payload: truncatePayload(normalizeUnknown(payload)) }),
120+
}
121+
appendFileSync(getLogFilePath(now), `${JSON.stringify(entry)}\n`, 'utf8')
122+
} catch (err) {
123+
console.error('[CrashLogger] Failed to write crash log:', err)
124+
}
125+
}
126+
127+
export function getCrashLogDir(): string {
128+
return LOG_DIR
129+
}
130+

‎src/main/index.ts‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { PluginManager } from './plugins/plugin-manager'
2525
import { registerMcpHandlers } from './ipc/mcp-handlers'
2626
import { McpManager } from './mcp/mcp-manager'
2727
import { closeDb } from './db/database'
28+
import { writeCrashLog, getCrashLogDir } from './crash-logger'
2829

2930
import { createFeishuService } from './plugins/providers/feishu/feishu-service'
3031
import { createDingTalkService } from './plugins/providers/dingtalk/dingtalk-service'
@@ -39,6 +40,10 @@ let mainWindow: BrowserWindow | null = null
3940
let tray: Tray | null = null
4041
let isQuiting = false
4142

43+
function recordCrash(event: string, details: unknown): void {
44+
writeCrashLog(event, details)
45+
}
46+
4247
function configureChromiumCachePaths(): void {
4348
const sessionDataPath = join(app.getPath('userData'), 'session-data')
4449
const diskCachePath = join(sessionDataPath, 'Cache')
@@ -50,6 +55,7 @@ function configureChromiumCachePaths(): void {
5055
app.commandLine.appendSwitch('disk-cache-dir', diskCachePath)
5156
} catch (error) {
5257
console.error('[Main] Failed to configure Chromium cache paths:', error)
58+
recordCrash('configure_chromium_cache_failed', { error })
5359
}
5460
}
5561

@@ -221,6 +227,44 @@ function createWindow(): void {
221227

222228
})
223229

230+
window.webContents.on('render-process-gone', (_event, details) => {
231+
const crashInfo = {
232+
windowId: window.id,
233+
webContentsId: window.webContents.id,
234+
url: window.webContents.getURL(),
235+
details,
236+
}
237+
console.error('[Main] Window render process gone:', crashInfo)
238+
recordCrash('window_render_process_gone', crashInfo)
239+
})
240+
241+
window.webContents.on('unresponsive', () => {
242+
const hangInfo = {
243+
windowId: window.id,
244+
webContentsId: window.webContents.id,
245+
url: window.webContents.getURL(),
246+
}
247+
console.error('[Main] Renderer became unresponsive:', hangInfo)
248+
recordCrash('window_renderer_unresponsive', hangInfo)
249+
})
250+
251+
window.webContents.on(
252+
'did-fail-load',
253+
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
254+
if (!isMainFrame || errorCode === -3) return
255+
const failInfo = {
256+
windowId: window.id,
257+
webContentsId: window.webContents.id,
258+
url: window.webContents.getURL(),
259+
validatedURL,
260+
errorCode,
261+
errorDescription,
262+
}
263+
console.error('[Main] Renderer failed to load:', failInfo)
264+
recordCrash('window_did_fail_load', failInfo)
265+
}
266+
)
267+
224268

225269

226270
// HMR for renderer base on electron-vite cli.
@@ -249,16 +293,23 @@ function createWindow(): void {
249293

250294
process.on('uncaughtException', (err) => {
251295

252-
console.error('[Main] Uncaught exception:', err.message)
296+
console.error('[Main] Uncaught exception:', err)
297+
recordCrash('main_uncaught_exception', { error: err })
253298

254299
})
255300

256301
process.on('unhandledRejection', (reason) => {
257302

258303
console.error('[Main] Unhandled rejection:', reason)
304+
recordCrash('main_unhandled_rejection', { reason })
259305

260306
})
261307

308+
app.on('child-process-gone', (_event, details) => {
309+
console.error('[Main] App child-process-gone:', details)
310+
recordCrash('app_child_process_gone', { details })
311+
})
312+
262313
configureChromiumCachePaths()
263314

264315
const gotSingleInstanceLock = app.requestSingleInstanceLock()
@@ -273,6 +324,12 @@ if (gotSingleInstanceLock) {
273324

274325
app.whenReady().then(() => {
275326

327+
recordCrash('app_started', {
328+
userDataPath: app.getPath('userData'),
329+
crashLogDir: getCrashLogDir(),
330+
})
331+
console.log(`[CrashLogger] Logs will be written to ${getCrashLogDir()}`)
332+
276333
// Set app user model id for windows
277334

278335
electronApp.setAppUserModelId('com.electron')

0 commit comments

Comments
 (0)