/** * Sync Failure Analysis Script * Query and analyze sync_history for failures and patterns * Run with: npx tsx scripts/analyze-sync-failures.ts [options] */ import { config } from 'dotenv'; import { resolve } from 'path'; import postgresClient from '../lib/services/postgres-client'; // Load environment variables config({ path: resolve(__dirname, '../.env.local') }); interface SyncHistoryRecord { id: number; entity_type: string; sync_type: string; status: string; started_at: Date; completed_at: Date | null; records_added: number; records_updated: number; records_deleted: number; error_message: string | null; triggered_by: string; } interface FailureSummary { entity_type: string; failure_count: number; last_failure: Date; common_errors: string[]; } /** * Parse command line arguments */ function parseArgs(): { days: number; entity?: string; status?: string; showAll: boolean; } { const args = process.argv.slice(2); const options = { days: 7, entity: undefined as string | undefined, status: undefined as string | undefined, showAll: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--days' || arg === '-d') { options.days = parseInt(args[++i]); } else if (arg === '--entity' || arg === '-e') { options.entity = args[++i]; } else if (arg === '--status' || arg === '-s') { options.status = args[++i]; } else if (arg === '--all' || arg === '-a') { options.showAll = true; } else if (arg === '--help' || arg === '-h') { console.log(` Sync Failure Analysis Script Usage: npx tsx scripts/analyze-sync-failures.ts [options] Options: -d, --days Number of days to look back (default: 7) -e, --entity Filter by entity type -s, --status Filter by status (failed, completed, started) -a, --all Show all records, not just failures -h, --help Show this help message Examples: # Show failures from last 7 days npx tsx scripts/analyze-sync-failures.ts # Show all syncs from yesterday npx tsx scripts/analyze-sync-failures.ts --days 1 --all # Show ticket sync failures from last 30 days npx tsx scripts/analyze-sync-failures.ts --days 30 --entity tickets # Show all completed syncs npx tsx scripts/analyze-sync-failures.ts --status completed --all `); process.exit(0); } } return options; } /** * Query sync history */ async function querySyncHistory( days: number, entity?: string, status?: string ): Promise { const params: any[] = []; let query = ` SELECT id, entity_type, sync_type, status, started_at, completed_at, records_added, records_updated, records_deleted, error_message, triggered_by FROM sync_history WHERE started_at >= NOW() - INTERVAL '${days} days' `; if (entity) { params.push(entity); query += ` AND entity_type = $${params.length}`; } if (status) { params.push(status); query += ` AND status = $${params.length}`; } query += ' ORDER BY started_at DESC'; const result = await postgresClient.query(query, params); return result.rows; } /** * Get failure summary by entity */ async function getFailureSummary(days: number): Promise { const query = ` SELECT entity_type, COUNT(*) as failure_count, MAX(started_at) as last_failure, ARRAY_AGG(DISTINCT SUBSTRING(error_message, 1, 100)) as common_errors FROM sync_history WHERE status = 'failed' AND started_at >= NOW() - INTERVAL '${days} days' GROUP BY entity_type ORDER BY failure_count DESC, last_failure DESC `; const result = await postgresClient.query(query); return result.rows.map(row => ({ entity_type: row.entity_type, failure_count: parseInt(row.failure_count), last_failure: row.last_failure, common_errors: row.common_errors.filter((e: string | null) => e !== null), })); } /** * Categorize error message */ function categorizeError(errorMessage: string | null): string { if (!errorMessage) return 'UNKNOWN'; if (errorMessage.includes('NETWORK_ERROR')) return 'NETWORK'; if (errorMessage.includes('AUTH_ERROR')) return 'AUTH'; if (errorMessage.includes('RATE_LIMIT_ERROR')) return 'RATE_LIMIT'; if (errorMessage.includes('DATABASE_CONSTRAINT_ERROR')) return 'DB_CONSTRAINT'; if (errorMessage.includes('DATABASE_ERROR')) return 'DATABASE'; if (errorMessage.includes('API_ERROR')) return 'API'; if (errorMessage.includes('MAPPING_ERROR')) return 'MAPPING'; if (errorMessage.includes('VALIDATION_ERROR')) return 'VALIDATION'; return 'OTHER'; } /** * Format duration */ function formatDuration(startedAt: Date, completedAt: Date | null): string { if (!completedAt) return 'N/A'; const duration = new Date(completedAt).getTime() - new Date(startedAt).getTime(); const seconds = Math.floor(duration / 1000); const minutes = Math.floor(seconds / 60); if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; } /** * Main analysis function */ async function analyzeSyncFailures() { console.log('🔍 Sync Failure Analysis\n'); console.log('═══════════════════════════════════════════════════════════\n'); const options = parseArgs(); try { // Test database connection const isConnected = await postgresClient.testConnection(); if (!isConnected) { console.error('❌ Database connection failed'); process.exit(1); } // Get sync history const records = await querySyncHistory( options.days, options.entity, options.status || (options.showAll ? undefined : 'failed') ); console.log(`📊 Analysis Period: Last ${options.days} day(s)`); if (options.entity) { console.log(`🎯 Entity Filter: ${options.entity}`); } if (options.status) { console.log(`📌 Status Filter: ${options.status}`); } console.log(`📝 Total Records: ${records.length}\n`); if (records.length === 0) { console.log('✅ No sync records found for the specified criteria.\n'); process.exit(0); } // Display failure summary if (!options.entity && !options.showAll) { console.log('═══════════════════════════════════════════════════════════'); console.log('📈 FAILURE SUMMARY BY ENTITY'); console.log('═══════════════════════════════════════════════════════════\n'); const summary = await getFailureSummary(options.days); if (summary.length === 0) { console.log('✅ No failures found in the specified period!\n'); } else { summary.forEach(item => { console.log(`Entity: ${item.entity_type}`); console.log(` Failures: ${item.failure_count}`); console.log(` Last Failure: ${new Date(item.last_failure).toLocaleString()}`); console.log(` Common Errors:`); item.common_errors.slice(0, 3).forEach(err => { console.log(` - ${err.substring(0, 80)}...`); }); console.log(); }); } } // Display detailed records console.log('═══════════════════════════════════════════════════════════'); console.log('📋 DETAILED SYNC RECORDS'); console.log('═══════════════════════════════════════════════════════════\n'); // Group by status const byStatus = records.reduce((acc, record) => { if (!acc[record.status]) acc[record.status] = []; acc[record.status].push(record); return acc; }, {} as Record); Object.entries(byStatus).forEach(([status, statusRecords]) => { const statusIcon = status === 'completed' ? '✅' : status === 'failed' ? '❌' : '⏳'; console.log(`${statusIcon} ${status.toUpperCase()} (${statusRecords.length} records)`); console.log('─────────────────────────────────────────────────────────\n'); statusRecords.forEach(record => { const duration = formatDuration(record.started_at, record.completed_at); const errorCategory = categorizeError(record.error_message); console.log(` ID: ${record.id}`); console.log(` Entity: ${record.entity_type}`); console.log(` Type: ${record.sync_type}`); console.log(` Started: ${new Date(record.started_at).toLocaleString()}`); console.log(` Duration: ${duration}`); console.log(` Records: +${record.records_added} ~${record.records_updated} -${record.records_deleted}`); console.log(` Triggered By: ${record.triggered_by}`); if (record.error_message) { console.log(` Error Category: ${errorCategory}`); console.log(` Error: ${record.error_message}`); } console.log(); }); }); // Statistics console.log('═══════════════════════════════════════════════════════════'); console.log('📊 STATISTICS'); console.log('═══════════════════════════════════════════════════════════\n'); const totalRecordsAdded = records.reduce((sum, r) => sum + r.records_added, 0); const totalRecordsUpdated = records.reduce((sum, r) => sum + r.records_updated, 0); const totalRecordsDeleted = records.reduce((sum, r) => sum + r.records_deleted, 0); const failedCount = records.filter(r => r.status === 'failed').length; const completedCount = records.filter(r => r.status === 'completed').length; const successRate = records.length > 0 ? ((completedCount / records.length) * 100).toFixed(1) : '0'; console.log(`Total Syncs: ${records.length}`); console.log(` Completed: ${completedCount}`); console.log(` Failed: ${failedCount}`); console.log(` Success Rate: ${successRate}%`); console.log(); console.log(`Total Records Processed:`); console.log(` Added: ${totalRecordsAdded}`); console.log(` Updated: ${totalRecordsUpdated}`); console.log(` Deleted: ${totalRecordsDeleted}`); console.log(); // Error categories if (failedCount > 0) { const errorCategories = records .filter(r => r.status === 'failed') .reduce((acc, r) => { const category = categorizeError(r.error_message); acc[category] = (acc[category] || 0) + 1; return acc; }, {} as Record); console.log('Error Categories:'); Object.entries(errorCategories) .sort(([, a], [, b]) => b - a) .forEach(([category, count]) => { console.log(` ${category}: ${count}`); }); console.log(); } console.log('═══════════════════════════════════════════════════════════\n'); } catch (error) { console.error('❌ Analysis failed:', error); if (error instanceof Error) { console.error('Stack trace:', error.stack); } process.exit(1); } finally { await postgresClient.close(); } } // Run analysis analyzeSyncFailures() .then(() => { console.log('✅ Analysis complete\n'); process.exit(0); }) .catch((error) => { console.error('❌ Fatal error:', error); process.exit(1); });