590 lines
18 KiB
JavaScript
590 lines
18 KiB
JavaScript
// ========================================
|
|
// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS
|
|
// Description: Système de validation et rapport d'erreur
|
|
// ========================================
|
|
|
|
const { google } = require('googleapis');
|
|
const nodemailer = require('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
|
|
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
|
|
|
|
// 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();
|
|
tee.pipe(prettyStream).pipe(process.stdout);
|
|
|
|
// 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
|
|
function initWebSocketServer() {
|
|
if (!wsServer) {
|
|
wsServer = new WebSocket.Server({ port: process.env.LOG_WS_PORT || 8081 });
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
logger.info(`Log WebSocket server started on port ${process.env.LOG_WS_PORT || 8081}`);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
let sheets;
|
|
let auth;
|
|
|
|
async function initGoogleSheets() {
|
|
if (!sheets) {
|
|
// Configuration auth Google Sheets API
|
|
// Pour la démo, on utilise une clé de service (à configurer)
|
|
auth = new google.auth.GoogleAuth({
|
|
keyFile: process.env.GOOGLE_CREDENTIALS_PATH, // Chemin vers fichier JSON credentials
|
|
scopes: ['https://www.googleapis.com/auth/spreadsheets']
|
|
});
|
|
|
|
sheets = google.sheets({ version: 'v4', auth });
|
|
}
|
|
return sheets;
|
|
}
|
|
|
|
async function logSh(message, level = 'INFO') {
|
|
// Initialize WebSocket server if not already done
|
|
if (!wsServer) {
|
|
initWebSocketServer();
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Log to Google Sheets if enabled (async, non-blocking)
|
|
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
|
|
setImmediate(() => {
|
|
logToGoogleSheets(message, level).catch(err => {
|
|
// Silent fail for Google Sheets logging to avoid recursion
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
async function logToGoogleSheets(message, level) {
|
|
try {
|
|
const sheetsApi = await initGoogleSheets();
|
|
|
|
const values = [[
|
|
new Date().toISOString(),
|
|
level,
|
|
message,
|
|
'Node.js workflow'
|
|
]];
|
|
|
|
await sheetsApi.spreadsheets.values.append({
|
|
spreadsheetId: SHEET_ID,
|
|
range: 'Logs!A:D',
|
|
valueInputOption: 'RAW',
|
|
insertDataOption: 'INSERT_ROWS',
|
|
resource: { values }
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh('Échec log Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
|
|
}
|
|
}
|
|
|
|
// 🔄 NODE.JS : Version simplifiée cleanLogSheet
|
|
async function cleanLogSheet() {
|
|
try {
|
|
logSh('🧹 Nettoyage logs...', 'INFO'); // Using logSh instead of console.log
|
|
|
|
// 1. Nettoyer fichiers logs locaux (garder 7 derniers jours)
|
|
await cleanLocalLogs();
|
|
|
|
// 2. Nettoyer Google Sheets si activé
|
|
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
|
|
await cleanGoogleSheetsLogs();
|
|
}
|
|
|
|
logSh('✅ Logs nettoyés', 'INFO'); // Using logSh instead of console.log
|
|
|
|
} catch (error) {
|
|
logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); // Using logSh instead of console.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
|
|
}
|
|
}
|
|
|
|
async function cleanGoogleSheetsLogs() {
|
|
try {
|
|
const sheetsApi = await initGoogleSheets();
|
|
|
|
// Clear + remettre headers
|
|
await sheetsApi.spreadsheets.values.clear({
|
|
spreadsheetId: SHEET_ID,
|
|
range: 'Logs!A:D'
|
|
});
|
|
|
|
await sheetsApi.spreadsheets.values.update({
|
|
spreadsheetId: SHEET_ID,
|
|
range: 'Logs!A1:D1',
|
|
valueInputOption: 'RAW',
|
|
resource: {
|
|
values: [['Timestamp', 'Level', 'Message', 'Source']]
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh('Échec nettoyage Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
|
|
}
|
|
}
|
|
|
|
// ============= 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
|
|
|
|
// 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,
|
|
cleanLogSheet,
|
|
validateWorkflowIntegrity,
|
|
detectDuplicateTags,
|
|
detectMissingCSVVariables,
|
|
assessGenerationQuality,
|
|
sendErrorReport,
|
|
createHTMLReport
|
|
}; |