diff --git a/src/db.js b/src/db.js index e7b10e86f..b32d09267 100644 --- a/src/db.js +++ b/src/db.js @@ -49,8 +49,15 @@ function getSslConfig(connectionString) { } /** - * Initialize the database connection pool and create schema - * @returns {Promise} The connection pool + * Initialize the database connection pool and create the required schema. + * + * Creates a pg.Pool configured from DATABASE_URL and PG_POOL_SIZE, verifies connectivity, + * and creates/migrates all required tables and indexes. On schema setup failure the + * created pool is closed and the error is rethrown. + * + * @returns {pg.Pool} The initialized PostgreSQL connection pool. + * @throws {Error} If initDb is already in progress. + * @throws {Error} If the DATABASE_URL environment variable is not set. */ export async function initDb() { if (initializing) { @@ -76,7 +83,7 @@ export async function initDb() { // Prevent unhandled pool errors from crashing the process pool.on('error', (err) => { - logError('Unexpected database pool error', { error: err.message }); + logError('Unexpected database pool error', { error: err.message, source: 'database_pool' }); }); try { diff --git a/src/index.js b/src/index.js index aca43c616..a99bea77a 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,9 @@ * - Structured logging */ +// Sentry must be imported before all other modules to instrument them +import './sentry.js'; + import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -218,7 +221,7 @@ client.on('interactionCreate', async (interaction) => { await command.execute(interaction); info('Command executed', { command: commandName, user: interaction.user.tag }); } catch (err) { - error('Command error', { command: commandName, error: err.message, stack: err.stack }); + error('Command error', { command: commandName, error: err.message, stack: err.stack, source: 'slash_command' }); const errorMessage = { content: '❌ An error occurred while executing this command.', @@ -238,8 +241,8 @@ client.on('interactionCreate', async (interaction) => { }); /** - * Perform an orderly shutdown: stop background services, persist in-memory state, remove logging transport, close the database pool, disconnect the Discord client, and exit the process. - * @param {string} signal - The signal name that initiated shutdown (e.g., "SIGINT", "SIGTERM"). + * Perform an orderly shutdown: stops background services, persists in-memory state, removes logging transports, closes database connections, flushes pending Sentry events, disconnects the Discord client, and exits the process. + * @param {string} signal - Signal name that initiated shutdown (e.g., "SIGINT" or "SIGTERM"). */ async function gracefulShutdown(signal) { info('Shutdown initiated', { signal }); @@ -283,11 +286,14 @@ async function gracefulShutdown(signal) { error('Failed to close database pool', { error: err.message }); } - // 5. Destroy Discord client + // 5. Flush Sentry events before exit (no-op if Sentry disabled) + await import('./sentry.js').then(({ Sentry }) => Sentry.flush(2000)).catch(() => {}); + + // 6. Destroy Discord client info('Disconnecting from Discord'); client.destroy(); - // 6. Log clean exit + // 7. Log clean exit info('Shutdown complete'); process.exit(0); } @@ -302,9 +308,16 @@ client.on('error', (err) => { error: err.message, stack: err.stack, code: err.code, + source: 'discord_client', }); }); +client.on('shardDisconnect', (event, shardId) => { + if (event.code !== 1000) { + warn('Shard disconnected unexpectedly', { shardId, code: event.code, source: 'discord_shard' }); + } +}); + // Start bot const token = process.env.DISCORD_TOKEN; if (!token) { @@ -313,7 +326,9 @@ if (!token) { } /** - * Perform full application startup: initialize the database and optional PostgreSQL logging, load configuration and conversation history, start background services (conversation cleanup, memory checks, triage, tempban scheduler), register event handlers, load slash commands, and log the Discord client in. + * Initialize and start all application subsystems, then log the Discord client in. + * + * Performs startup tasks including optional database initialization and restart recording, configuration loading, conversation history hydration, background service startup (conversation cleanup, triage, tempban scheduler, memory/opt-out setup), event handler registration, command loading, Sentry context tagging, and attempting to start the REST API server with WebSocket log streaming. */ async function startup() { // Initialize database @@ -422,6 +437,15 @@ async function startup() { await loadCommands(); await client.login(token); + // Set Sentry context now that we know the bot identity (no-op if disabled) + import('./sentry.js').then(({ Sentry, sentryEnabled }) => { + if (sentryEnabled) { + Sentry.setTag('bot.username', client.user?.tag || 'unknown'); + Sentry.setTag('bot.version', BOT_VERSION); + info('Sentry error monitoring enabled', { environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'production' }); + } + }).catch(() => {}); + // Start REST API server with WebSocket log streaming (non-fatal — bot continues without it) { let wsTransport = null;