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.
This commit is contained in:
Pedram Amini
2026-02-01 18:13:24 -06:00
parent 5ccebfd73e
commit c2239fee70
2 changed files with 104 additions and 0 deletions

View File

@@ -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);

74
src/main/utils/sentry.ts Normal file
View File

@@ -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, unknown> }
) => string;
captureException: (
exception: Error | unknown,
captureContext?: { level?: SentrySeverityLevel; extra?: Record<string, unknown> }
) => 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<string, unknown>
): Promise<void> {
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<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 message reporting', '[Sentry]');
}
}