feat: enhance logging capabilities, implement runtime diagnostics, and update preinstalled skills metadata
This commit is contained in:
66
electron/utils/log-helpers.ts
Normal file
66
electron/utils/log-helpers.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export type SerializedError = {
|
||||
name?: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string | number;
|
||||
cause?: unknown;
|
||||
};
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function serializeError(error: Error): SerializedError {
|
||||
const code = (error as Error & { code?: string | number }).code;
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...(typeof code !== 'undefined' ? { code } : {}),
|
||||
...(typeof cause !== 'undefined' ? { cause: serializeUnknownForLog(cause) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeUnknownError(error: unknown): SerializedError {
|
||||
if (error instanceof Error) {
|
||||
return serializeError(error);
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return { message: error };
|
||||
}
|
||||
|
||||
if (isPlainObject(error) && typeof error.message === 'string') {
|
||||
return {
|
||||
message: error.message,
|
||||
...(typeof error.name === 'string' ? { name: error.name } : {}),
|
||||
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
||||
...(typeof error.code === 'string' || typeof error.code === 'number' ? { code: error.code } : {}),
|
||||
...(typeof error.cause !== 'undefined' ? { cause: serializeUnknownForLog(error.cause) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Non-Error value thrown: ${String(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeUnknownForLog(value: unknown): unknown {
|
||||
if (value instanceof Error) {
|
||||
return serializeError(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => serializeUnknownForLog(item));
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, serializeUnknownForLog(entryValue)]),
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
160
electron/utils/runtime-diagnostics.ts
Normal file
160
electron/utils/runtime-diagnostics.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { app, crashReporter, type BrowserWindow, type WebContents } from 'electron';
|
||||
import path from 'node:path';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { ensureDir, getUserDataDir } from './paths';
|
||||
import { serializeUnknownError, serializeUnknownForLog } from './log-helpers';
|
||||
|
||||
let diagnosticsInstalled = false;
|
||||
let crashReporterStarted = false;
|
||||
|
||||
function describeWebContents(webContents: WebContents): Record<string, unknown> {
|
||||
return {
|
||||
webContentsId: webContents.id,
|
||||
type: webContents.getType(),
|
||||
url: webContents.getURL(),
|
||||
loading: webContents.isLoading(),
|
||||
destroyed: webContents.isDestroyed(),
|
||||
};
|
||||
}
|
||||
|
||||
function describeWindow(window: BrowserWindow): Record<string, unknown> {
|
||||
return {
|
||||
windowId: window.id,
|
||||
title: window.getTitle(),
|
||||
visible: window.isVisible(),
|
||||
minimized: window.isMinimized(),
|
||||
maximized: window.isMaximized(),
|
||||
destroyed: window.isDestroyed(),
|
||||
...describeWebContents(window.webContents),
|
||||
};
|
||||
}
|
||||
|
||||
function getCrashDumpsDirectoryPath(): string {
|
||||
return ensureDir(path.join(getUserDataDir(), 'logs', 'crashDumps'));
|
||||
}
|
||||
|
||||
function attachWindowDiagnostics(window: BrowserWindow): void {
|
||||
logManager.info('BrowserWindow created', describeWindow(window));
|
||||
|
||||
window.on('unresponsive', () => {
|
||||
logManager.warn('BrowserWindow became unresponsive', describeWindow(window));
|
||||
});
|
||||
|
||||
window.on('responsive', () => {
|
||||
logManager.info('BrowserWindow became responsive again', describeWindow(window));
|
||||
});
|
||||
|
||||
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
|
||||
logManager.error('WebContents failed to load', {
|
||||
...describeWindow(window),
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
});
|
||||
});
|
||||
|
||||
window.webContents.on('render-process-gone', (_event, details) => {
|
||||
logManager.error('Window render process exited unexpectedly', {
|
||||
...describeWindow(window),
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function logStartupCheckpoint(stage: string, details: Record<string, unknown> = {}): void {
|
||||
logManager.info(`startup:${stage}`, {
|
||||
pid: process.pid,
|
||||
packaged: app.isPackaged,
|
||||
...details,
|
||||
});
|
||||
}
|
||||
|
||||
export function startLocalCrashReporter(): void {
|
||||
if (crashReporterStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const crashDumpsDir = getCrashDumpsDirectoryPath();
|
||||
|
||||
try {
|
||||
app.setPath('crashDumps', crashDumpsDir);
|
||||
} catch (error) {
|
||||
logManager.captureException('Failed to set crashDumps path', error, { crashDumpsDir });
|
||||
}
|
||||
|
||||
try {
|
||||
crashReporter.start({
|
||||
companyName: 'ZhiNian Team',
|
||||
productName: app.getName() || 'NIANXX',
|
||||
submitURL: 'https://127.0.0.1/disabled-crash-upload',
|
||||
uploadToServer: false,
|
||||
compress: true,
|
||||
ignoreSystemCrashHandler: false,
|
||||
rateLimit: false,
|
||||
globalExtra: {
|
||||
appVersion: app.getVersion(),
|
||||
packaged: String(app.isPackaged),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
});
|
||||
|
||||
crashReporterStarted = true;
|
||||
logManager.info('Crash reporter started for local dump collection', {
|
||||
crashDumpsDir,
|
||||
uploadToServer: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logManager.captureException('Failed to start crash reporter', error, { crashDumpsDir });
|
||||
}
|
||||
}
|
||||
|
||||
export function installRuntimeDiagnostics(): void {
|
||||
if (diagnosticsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
diagnosticsInstalled = true;
|
||||
|
||||
process.on('warning', (warning) => {
|
||||
logManager.warn('Node.js process warning', serializeUnknownError(warning));
|
||||
});
|
||||
|
||||
process.on('uncaughtExceptionMonitor', (error) => {
|
||||
logManager.captureException('uncaughtExceptionMonitor', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logManager.error('unhandledRejection observed by runtime diagnostics', {
|
||||
reason: serializeUnknownForLog(reason),
|
||||
});
|
||||
});
|
||||
|
||||
app.on('browser-window-created', (_event, window) => {
|
||||
attachWindowDiagnostics(window);
|
||||
});
|
||||
|
||||
app.on('render-process-gone', (_event, webContents, details) => {
|
||||
logManager.error('App render process exited unexpectedly', {
|
||||
...describeWebContents(webContents),
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
});
|
||||
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
logManager.error('Child process exited unexpectedly', serializeUnknownForLog(details));
|
||||
});
|
||||
|
||||
logManager.info('Runtime diagnostics installed', {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
pid: process.pid,
|
||||
userDataDir: getUserDataDir(),
|
||||
logsDir: logManager.getLogDirectoryPath(),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user