sourcefinder/export_logger/ErrorReporting.js
Alexis Trouvé a7bd6115b7
Some checks failed
SourceFinder CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
SourceFinder CI/CD Pipeline / Unit Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Integration Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Performance Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Code Coverage Report (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (16.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (18.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (20.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Regression Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Audit (push) Has been cancelled
SourceFinder CI/CD Pipeline / Notify Results (push) Has been cancelled
feat: Implémentation complète du système SourceFinder avec tests
- Architecture modulaire avec injection de dépendances
- Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation)
- Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités)
- API REST complète avec validation et rate limiting
- Repository JSON avec index mémoire et backup automatique
- Provider LLM modulaire pour génération de contenu
- Suite de tests complète (Jest) :
  * Tests unitaires pour sécurité et scoring
  * Tests d'intégration API end-to-end
  * Tests de sécurité avec simulation d'attaques
  * Tests de performance et charge
- Pipeline CI/CD avec GitHub Actions
- Logging structuré et monitoring
- Configuration ESLint et environnement de test

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:06:10 +08:00

547 lines
17 KiB
JavaScript

// ========================================
// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS
// Description: Système de validation et rapport d'erreur
// ========================================
// Lazy loading des modules externes
let nodemailer;
const fs = require('fs').promises;
const path = require('path');
const pino = require('pino');
const pretty = require('pino-pretty');
const { PassThrough } = require('stream');
const WebSocket = require('ws');
// Configuration (Google Sheets logging removed)
// WebSocket server for real-time logs
let wsServer;
const wsClients = new Set();
// Enhanced Pino logger configuration with real-time streaming and dated files
const now = new Date();
const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
const prettyStream = pretty({
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname',
});
const tee = new PassThrough();
// Lazy loading des pipes console (évite blocage à l'import)
let consolePipeInitialized = false;
// File destination with dated filename - FORCE DEBUG LEVEL
const fileDest = pino.destination({
dest: logFile,
mkdir: true,
sync: false,
minLength: 0 // Force immediate write even for small logs
});
tee.pipe(fileDest);
// Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = {
trace: 5, // Below debug (10)
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30,
error: 40,
fatal: 50
};
// Pino logger instance with enhanced configuration and custom levels
const logger = pino(
{
level: 'debug', // FORCE DEBUG LEVEL for file logging
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
customLevels: customLevels,
useOnlyCustomLevels: true
},
tee
);
// Initialize WebSocket server (only when explicitly requested)
function initWebSocketServer() {
if (!wsServer && process.env.ENABLE_LOG_WS === 'true') {
try {
const logPort = process.env.LOG_WS_PORT || 8082;
wsServer = new WebSocket.Server({ port: logPort });
wsServer.on('connection', (ws) => {
wsClients.add(ws);
logger.info('Client connected to log WebSocket');
ws.on('close', () => {
wsClients.delete(ws);
logger.info('Client disconnected from log WebSocket');
});
ws.on('error', (error) => {
logger.error('WebSocket error:', error.message);
wsClients.delete(ws);
});
});
wsServer.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
logger.warn(`WebSocket port ${logPort} already in use`);
wsServer = null;
} else {
logger.error('WebSocket server error:', error.message);
}
});
logger.info(`Log WebSocket server started on port ${logPort}`);
} catch (error) {
logger.warn(`Failed to start WebSocket server: ${error.message}`);
wsServer = null;
}
}
}
// Broadcast log to WebSocket clients
function broadcastLog(message, level) {
const logData = {
timestamp: new Date().toISOString(),
level: level.toUpperCase(),
message: message
};
wsClients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify(logData));
} catch (error) {
logger.error('Failed to send log to WebSocket client:', error.message);
wsClients.delete(ws);
}
}
});
}
// 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp)
// Google Sheets integration removed for export
async function logSh(message, level = 'INFO') {
// Initialize WebSocket server if not already done
if (!wsServer) {
initWebSocketServer();
}
// Initialize console pipe if needed (lazy loading)
if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') {
tee.pipe(prettyStream).pipe(process.stdout);
consolePipeInitialized = true;
}
// Convert level to lowercase for Pino
const pinoLevel = level.toLowerCase();
// Enhanced trace metadata for hierarchical logging
const traceData = {};
if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) {
traceData.trace = true;
traceData.evt = message.includes('▶') ? 'span.start' :
message.includes('✔') ? 'span.end' :
message.includes('✖') ? 'span.error' : 'span.event';
}
// Log with Pino (handles console output with pretty formatting and file logging)
switch (pinoLevel) {
case 'error':
logger.error(traceData, message);
break;
case 'warning':
case 'warn':
logger.warn(traceData, message);
break;
case 'debug':
logger.debug(traceData, message);
break;
case 'trace':
logger.trace(traceData, message);
break;
case 'prompt':
logger.prompt(traceData, message);
break;
case 'llm':
logger.llm(traceData, message);
break;
default:
logger.info(traceData, message);
}
// Broadcast to WebSocket clients for real-time viewing
broadcastLog(message, level);
// Force immediate flush to ensure real-time display and prevent log loss
logger.flush();
// Google Sheets logging removed for export
}
// Fonction pour déterminer si on doit logger en console
function shouldLogToConsole(messageLevel, configLevel) {
const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
return levels[messageLevel] >= levels[configLevel];
}
// Log to file is now handled by Pino transport
// This function is kept for compatibility but does nothing
async function logToFile(message, level) {
// Pino handles file logging via transport configuration
// This function is deprecated and kept for compatibility only
}
// 🔄 NODE.JS : Log vers Google Sheets (version async)
// Google Sheets logging functions removed for export
// 🔄 NODE.JS : Version simplifiée cleanLogSheet
async function cleanLogSheet() {
try {
logSh('🧹 Nettoyage logs...', 'INFO');
// 1. Nettoyer fichiers logs locaux (garder 7 derniers jours)
await cleanLocalLogs();
logSh('✅ Logs nettoyés', 'INFO');
} catch (error) {
logSh('Erreur nettoyage logs: ' + error.message, 'ERROR');
}
}
async function cleanLocalLogs() {
try {
// Note: With Pino, log files are managed differently
// This function is kept for compatibility with Google Sheets logs cleanup
// Pino log rotation should be handled by external tools like logrotate
// For now, we keep the basic cleanup for any remaining old log files
const logsDir = path.join(__dirname, '../logs');
try {
const files = await fs.readdir(logsDir);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours
for (const file of files) {
if (file.endsWith('.log')) {
const filePath = path.join(logsDir, file);
const stats = await fs.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs.unlink(filePath);
logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO');
}
}
}
} catch (error) {
// Directory might not exist, that's fine
}
} catch (error) {
// Silent fail
}
}
// cleanGoogleSheetsLogs function removed for export
// ============= VALIDATION PRINCIPALE - IDENTIQUE =============
function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) {
logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log
const errors = [];
const warnings = [];
const stats = {
elementsExtracted: elements.length,
contentGenerated: Object.keys(generatedContent).length,
tagsReplaced: 0,
tagsRemaining: 0
};
// TEST 1: Détection tags dupliqués
const duplicateCheck = detectDuplicateTags(elements);
if (duplicateCheck.hasDuplicates) {
errors.push({
type: 'DUPLICATE_TAGS',
severity: 'HIGH',
message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`,
impact: 'Certains contenus ne seront pas remplacés dans le XML final',
suggestion: 'Vérifier le template XML pour corriger la structure'
});
}
// TEST 2: Cohérence éléments extraits vs générés
const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]);
if (missingGeneration.length > 0) {
errors.push({
type: 'MISSING_GENERATION',
severity: 'HIGH',
message: `${missingGeneration.length} éléments extraits mais non générés`,
details: missingGeneration.map(el => el.originalTag),
impact: 'Contenu incomplet dans le XML final'
});
}
// TEST 3: Tags non remplacés dans XML final
const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
stats.tagsRemaining = remainingTags.length;
if (remainingTags.length > 0) {
errors.push({
type: 'UNREPLACED_TAGS',
severity: 'HIGH',
message: `${remainingTags.length} tags non remplacés dans le XML final`,
details: remainingTags.slice(0, 5),
impact: 'XML final contient des placeholders non remplacés'
});
}
// TEST 4: Variables CSV manquantes
const missingVars = detectMissingCSVVariables(csvData);
if (missingVars.length > 0) {
warnings.push({
type: 'MISSING_CSV_VARIABLES',
severity: 'MEDIUM',
message: `Variables CSV manquantes: ${missingVars.join(', ')}`,
impact: 'Système de génération de mots-clés automatique activé'
});
}
// TEST 5: Qualité génération IA
const generationQuality = assessGenerationQuality(generatedContent);
if (generationQuality.errorRate > 0.1) {
warnings.push({
type: 'GENERATION_QUALITY',
severity: 'MEDIUM',
message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`,
impact: 'Qualité du contenu potentiellement dégradée'
});
}
// CALCUL STATS FINALES
stats.tagsReplaced = elements.length - remainingTags.length;
stats.successRate = stats.elementsExtracted > 0 ?
((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100';
const report = {
timestamp: new Date().toISOString(),
csvData: { mc0: csvData.mc0, t0: csvData.t0 },
stats: stats,
errors: errors,
warnings: warnings,
status: errors.length === 0 ? 'SUCCESS' : 'ERROR'
};
const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR';
logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log
// ENVOYER RAPPORT SI ERREURS (async en arrière-plan)
if (errors.length > 0 || warnings.length > 2) {
sendErrorReport(report).catch(err => {
logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error
});
}
return report;
}
// ============= HELPERS - IDENTIQUES =============
function detectDuplicateTags(elements) {
const tagCounts = {};
const duplicates = [];
elements.forEach(element => {
const tag = element.originalTag;
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
if (tagCounts[tag] === 2) {
duplicates.push(tag);
logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error
}
});
return {
hasDuplicates: duplicates.length > 0,
duplicates: duplicates,
counts: tagCounts
};
}
function detectMissingCSVVariables(csvData) {
const missing = [];
if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) {
missing.push('MC+1 (insuffisant)');
}
if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) {
missing.push('T+1 (insuffisant)');
}
if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) {
missing.push('L+1 (insuffisant)');
}
return missing;
}
function assessGenerationQuality(generatedContent) {
let errorCount = 0;
let totalCount = Object.keys(generatedContent).length;
Object.values(generatedContent).forEach(content => {
if (content && (
content.includes('[ERREUR') ||
content.includes('ERROR') ||
content.length < 10
)) {
errorCount++;
}
});
return {
errorRate: totalCount > 0 ? errorCount / totalCount : 0,
totalGenerated: totalCount,
errorsFound: errorCount
};
}
// 🔄 NODE.JS : Email avec nodemailer (remplace MailApp)
async function sendErrorReport(report) {
try {
logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log
// Lazy load nodemailer seulement quand nécessaire
if (!nodemailer) {
nodemailer = require('nodemailer');
}
// Configuration nodemailer (Gmail par exemple)
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER, // 'your-email@gmail.com'
pass: process.env.EMAIL_APP_PASSWORD // App password Google
}
});
const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`;
const htmlBody = createHTMLReport(report);
const mailOptions = {
from: process.env.EMAIL_USER,
to: 'alexistrouve.pro@gmail.com',
subject: subject,
html: htmlBody,
attachments: [{
filename: `error-report-${Date.now()}.json`,
content: JSON.stringify(report, null, 2),
contentType: 'application/json'
}]
};
await transporter.sendMail(mailOptions);
logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log
} catch (error) {
logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
}
}
// ============= HTML REPORT - IDENTIQUE =============
function createHTMLReport(report) {
const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545';
let html = `
<div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
<h1 style="color: ${statusColor};">Rapport Workflow SEO Automatisé (Node.js)</h1>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Résumé Exécutif</h2>
<p><strong>Statut:</strong> <span style="color: ${statusColor};">${report.status}</span></p>
<p><strong>Article:</strong> ${report.csvData.t0}</p>
<p><strong>Mot-clé:</strong> ${report.csvData.mc0}</p>
<p><strong>Taux de réussite:</strong> ${report.stats.successRate}%</p>
<p><strong>Timestamp:</strong> ${report.timestamp}</p>
<p><strong>Plateforme:</strong> Node.js Server</p>
</div>`;
if (report.errors.length > 0) {
html += `<div style="background: #f8d7da; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Erreurs Critiques (${report.errors.length})</h2>`;
report.errors.forEach((error, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #dc3545;">
<h4>${i + 1}. ${error.type}</h4>
<p><strong>Message:</strong> ${error.message}</p>
<p><strong>Impact:</strong> ${error.impact}</p>
${error.suggestion ? `<p><strong>Solution:</strong> ${error.suggestion}</p>` : ''}
</div>`;
});
html += `</div>`;
}
if (report.warnings.length > 0) {
html += `<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Avertissements (${report.warnings.length})</h2>`;
report.warnings.forEach((warning, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #ffc107;">
<h4>${i + 1}. ${warning.type}</h4>
<p>${warning.message}</p>
</div>`;
});
html += `</div>`;
}
html += `
<div style="background: #e9ecef; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Statistiques Détaillées</h2>
<ul>
<li>Éléments extraits: ${report.stats.elementsExtracted}</li>
<li>Contenus générés: ${report.stats.contentGenerated}</li>
<li>Tags remplacés: ${report.stats.tagsReplaced}</li>
<li>Tags restants: ${report.stats.tagsRemaining}</li>
</ul>
</div>
<div style="background: #d1ecf1; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Informations Système</h2>
<ul>
<li>Plateforme: Node.js</li>
<li>Version: ${process.version}</li>
<li>Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB</li>
<li>Uptime: ${Math.round(process.uptime())}s</li>
</ul>
</div>
</div>`;
return html;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
logSh,
setupTracer: require('./trace').setupTracer,
cleanLogSheet,
validateWorkflowIntegrity,
detectDuplicateTags,
detectMissingCSVVariables,
assessGenerationQuality,
sendErrorReport,
createHTMLReport,
initWebSocketServer
};