Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ function getSslConfig(connectionString) {
}

/**
* Initialize the database connection pool and create schema
* @returns {Promise<pg.Pool>} 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) {
Expand All @@ -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 {
Expand Down
36 changes: 30 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.',
Expand All @@ -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 });
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Loading