From c2239fee706ac98b78812c78f2c1c896f6f1407c Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Sun, 1 Feb 2026 18:13:24 -0600 Subject: [PATCH] feat(stats-db): add Sentry error reporting for database failures Stats DB initialization failures were only logged locally, providing no visibility into production issues. This change adds Sentry reporting for: - Database corruption detection (integrity check failures) - Native module loading failures (architecture mismatches) - Database creation failures - General initialization failures Created reusable Sentry utilities in src/main/utils/sentry.ts that lazily load @sentry/electron to avoid module initialization issues. --- src/main/stats-db.ts | 30 ++++++++++++++++ src/main/utils/sentry.ts | 74 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/main/utils/sentry.ts diff --git a/src/main/stats-db.ts b/src/main/stats-db.ts index 5ecdce2d..d0b4bd2a 100644 --- a/src/main/stats-db.ts +++ b/src/main/stats-db.ts @@ -39,6 +39,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { app } from 'electron'; import { logger } from './utils/logger'; +import { captureException, captureMessage } from './utils/sentry'; import { QueryEvent, AutoRunSession, @@ -414,6 +415,13 @@ export class StatsDB { // This can happen if the native module fails to load const errorMessage = createError instanceof Error ? createError.message : String(createError); logger.error(`Failed to create database: ${errorMessage}`, LOG_CONTEXT); + + // Report to Sentry + void captureException(createError, { + context: 'initialize:createNewDatabase', + dbPath: this.dbPath, + }); + return { success: false, wasReset: false, @@ -451,6 +459,14 @@ export class StatsDB { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to initialize stats database: ${errorMessage}`, LOG_CONTEXT); + + // Report to Sentry + void captureException(error, { + context: 'initialize:outerCatch', + dbPath: this.dbPath, + wasReset, + }); + return { success: false, wasReset, @@ -1105,6 +1121,12 @@ export class StatsDB { const errors = result.map((row) => row.integrity_check); logger.error(`Database integrity check failed: ${errors.join(', ')}`, LOG_CONTEXT); + // Report corruption to Sentry for monitoring + void captureMessage('Stats database corruption detected', 'error', { + integrityErrors: errors, + dbPath: this.dbPath, + }); + // Close before recovery db.close(); } catch (error) { @@ -1112,6 +1134,14 @@ export class StatsDB { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to open database: ${errorMessage}`, LOG_CONTEXT); + // Report failure to Sentry + void captureException(error, { + context: 'openWithCorruptionHandling', + dbPath: this.dbPath, + isNativeModuleError: + errorMessage.includes('dlopen') || errorMessage.includes('better_sqlite3.node'), + }); + // Check if this is a native module loading issue (not recoverable by reset) if (errorMessage.includes('dlopen') || errorMessage.includes('better_sqlite3.node')) { logger.error('Native SQLite module failed to load - cannot recover', LOG_CONTEXT); diff --git a/src/main/utils/sentry.ts b/src/main/utils/sentry.ts new file mode 100644 index 00000000..f354c149 --- /dev/null +++ b/src/main/utils/sentry.ts @@ -0,0 +1,74 @@ +/** + * Sentry utilities for error reporting in the main process. + * + * These utilities lazily load Sentry to avoid module initialization issues + * that can occur when importing @sentry/electron/main before app.whenReady(). + */ + +import { logger } from './logger'; + +/** Sentry severity levels */ +export 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; + captureException: ( + exception: Error | unknown, + captureContext?: { level?: SentrySeverityLevel; extra?: Record } + ) => string; +} + +/** Cached Sentry module reference */ +let sentryModule: SentryModule | null = null; + +/** + * Reports an exception to Sentry from the main process. + * Lazily loads Sentry to avoid module initialization issues. + * + * @param error - The error to report + * @param extra - Additional context data + */ +export async function captureException( + error: Error | unknown, + extra?: Record +): Promise { + try { + if (!sentryModule) { + const sentry = await import('@sentry/electron/main'); + sentryModule = sentry; + } + sentryModule.captureException(error, { extra }); + } catch { + // Sentry not available (development mode or initialization failed) + logger.debug('Sentry not available for exception reporting', '[Sentry]'); + } +} + +/** + * Reports a message to Sentry from the main process. + * Lazily loads Sentry to avoid module initialization issues. + * + * @param message - The message to report + * @param level - Severity level + * @param extra - Additional context data + */ +export async function captureMessage( + message: string, + level: SentrySeverityLevel = 'error', + extra?: Record +): Promise { + 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 message reporting', '[Sentry]'); + } +}