mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat(crash-detection): add renderer crash handlers and Sentry reporting
Add comprehensive crash detection for renderer process failures that Sentry in the renderer cannot capture (because the process is dead, broken, or failed before Sentry initialized): - render-process-gone: captures crash, kill, OOM, launch-failed - unresponsive/responsive: tracks frozen window states - crashed: handles page-level crashes - did-fail-load: captures page load failures (network, invalid URLs) - preload-error: captures preload script failures before app loads - console-message: forwards critical renderer errors (TypeError, etc.) Reports to Sentry from main process with detailed context. Auto-reloads renderer after non-intentional crashes.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Window manager for creating and managing the main BrowserWindow.
|
||||
* Handles window state persistence, DevTools, and auto-updater initialization.
|
||||
* Handles window state persistence, DevTools, crash detection, and auto-updater initialization.
|
||||
*/
|
||||
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
@@ -9,6 +9,41 @@ import type { WindowState } from '../stores/types';
|
||||
import { logger } from '../utils/logger';
|
||||
import { initAutoUpdater } from '../auto-updater';
|
||||
|
||||
/** Sentry severity levels */
|
||||
type SentrySeverityLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';
|
||||
|
||||
/** Sentry module type for crash reporting */
|
||||
interface SentryModule {
|
||||
captureMessage: (
|
||||
message: string,
|
||||
captureContext?: { level?: SentrySeverityLevel; extra?: Record<string, unknown> }
|
||||
) => string;
|
||||
}
|
||||
|
||||
/** Cached Sentry module reference */
|
||||
let sentryModule: SentryModule | null = null;
|
||||
|
||||
/**
|
||||
* Reports a crash event to Sentry from the main process.
|
||||
* Lazily loads Sentry to avoid module initialization issues.
|
||||
*/
|
||||
async function reportCrashToSentry(
|
||||
message: string,
|
||||
level: SentrySeverityLevel,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!sentryModule) {
|
||||
const sentry = await import('@sentry/electron/main');
|
||||
sentryModule = sentry;
|
||||
}
|
||||
sentryModule.captureMessage(message, { level, extra });
|
||||
} catch {
|
||||
// Sentry not available (development mode or initialization failed)
|
||||
logger.debug('Sentry not available for crash reporting', 'Window');
|
||||
}
|
||||
}
|
||||
|
||||
/** Dependencies for window manager */
|
||||
export interface WindowManagerDependencies {
|
||||
/** Store for window state persistence */
|
||||
@@ -123,6 +158,115 @@ export function createWindowManager(deps: WindowManagerDependencies): WindowMana
|
||||
logger.info('Browser window closed', 'Window');
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// Renderer Process Crash Detection
|
||||
// ================================================================
|
||||
// These handlers capture crashes that Sentry in the renderer cannot
|
||||
// report (because the renderer process is dead or broken).
|
||||
|
||||
// Handle renderer process termination (crash, kill, OOM, etc.)
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
logger.error('Renderer process gone', 'Window', {
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
|
||||
// Report to Sentry from main process (always available)
|
||||
reportCrashToSentry(`Renderer process gone: ${details.reason}`, 'fatal', {
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
|
||||
// Auto-reload unless the process was intentionally killed
|
||||
if (details.reason !== 'killed' && details.reason !== 'clean-exit') {
|
||||
logger.info('Attempting to reload renderer after crash', 'Window');
|
||||
setTimeout(() => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.reload();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window becoming unresponsive (frozen renderer)
|
||||
mainWindow.on('unresponsive', () => {
|
||||
logger.warn('Window became unresponsive', 'Window');
|
||||
reportCrashToSentry('Window unresponsive', 'warning', {
|
||||
memoryUsage: process.memoryUsage(),
|
||||
});
|
||||
});
|
||||
|
||||
// Log when window recovers from unresponsive state
|
||||
mainWindow.on('responsive', () => {
|
||||
logger.info('Window became responsive again', 'Window');
|
||||
});
|
||||
|
||||
// Handle page crashes (less severe than render-process-gone)
|
||||
mainWindow.webContents.on('crashed', (_event, killed) => {
|
||||
logger.error('WebContents crashed', 'Window', { killed });
|
||||
reportCrashToSentry('WebContents crashed', killed ? 'warning' : 'error', { killed });
|
||||
});
|
||||
|
||||
// Handle page load failures (network issues, invalid URLs, etc.)
|
||||
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
||||
// Ignore aborted loads (user navigated away)
|
||||
if (errorCode === -3) return;
|
||||
|
||||
logger.error('Page failed to load', 'Window', {
|
||||
errorCode,
|
||||
errorDescription,
|
||||
url: validatedURL,
|
||||
});
|
||||
reportCrashToSentry(`Page failed to load: ${errorDescription}`, 'error', {
|
||||
errorCode,
|
||||
errorDescription,
|
||||
url: validatedURL,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle preload script errors
|
||||
mainWindow.webContents.on('preload-error', (_event, preloadPath, error) => {
|
||||
logger.error('Preload script error', 'Window', {
|
||||
preloadPath,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
reportCrashToSentry('Preload script error', 'fatal', {
|
||||
preloadPath,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
// Forward renderer console errors to main process logger and Sentry
|
||||
// This catches errors that happen before or outside React's error boundary
|
||||
mainWindow.webContents.on('console-message', (_event, level, message, line, sourceId) => {
|
||||
// Level 2 = error (0=verbose, 1=info, 2=warning, 3=error)
|
||||
if (level === 3) {
|
||||
logger.error(`Renderer console error: ${message}`, 'Window', {
|
||||
line,
|
||||
source: sourceId,
|
||||
});
|
||||
|
||||
// Report critical errors to Sentry
|
||||
// Filter out common noise (React dev warnings, etc.)
|
||||
const isCritical =
|
||||
message.includes('Uncaught') ||
|
||||
message.includes('TypeError') ||
|
||||
message.includes('ReferenceError') ||
|
||||
message.includes('Cannot read') ||
|
||||
message.includes('is not defined') ||
|
||||
message.includes('is not a function');
|
||||
|
||||
if (isCritical) {
|
||||
reportCrashToSentry(`Renderer error: ${message}`, 'error', {
|
||||
line,
|
||||
source: sourceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize auto-updater (only in production)
|
||||
if (!isDevelopment) {
|
||||
initAutoUpdater(mainWindow);
|
||||
|
||||
Reference in New Issue
Block a user