#!/usr/bin/env node /** * Console Error Aggregator * * Collects all console errors from Playwright sessions * Reports: error message, file, line number, stack trace * Auto-creates Gitea Issues for critical errors */ const http = require('http'); const https = require('https'); const { URL } = require('url'); // Configuration const config = { playwrightMcpUrl: process.env.PLAYWRIGHT_MCP_URL || 'http://localhost:8931/mcp', giteaApiUrl: process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1', giteaToken: process.env.GITEA_TOKEN || '', giteaRepo: process.env.GITEA_REPO || 'UniqueSoft/APAW', targetUrl: process.env.TARGET_URL || 'http://localhost:3000', reportsDir: process.env.REPORTS_DIR || './reports', autoCreateIssues: process.env.AUTO_CREATE_ISSUES === 'true', ignoredPatterns: (process.env.IGNORED_ERROR_PATTERNS || '').split(','), }; /** * Make HTTP request to Playwright MCP */ async function mcpRequest(method, params) { return new Promise((resolve, reject) => { const body = JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params, }); const url = new URL(config.playwrightMcpUrl); const req = http.request({ hostname: url.hostname, port: url.port || 8931, path: '/mcp', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }, }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve(JSON.parse(data))); }); req.on('error', reject); req.write(body); req.end(); }); } /** * Navigate to URL */ async function navigateTo(url) { return mcpRequest('tools/call', { name: 'browser_navigate', arguments: { url }, }); } /** * Get console messages */ async function getConsoleMessages(level = 'error', all = true) { return mcpRequest('tools/call', { name: 'browser_console_messages', arguments: { level, all }, }); } /** * Get network requests (for failed requests) */ async function getNetworkRequests(filter = 'failed') { return mcpRequest('tools/call', { name: 'browser_network_requests', arguments: { filter }, }); } /** * Take screenshot for error context */ async function takeScreenshot(filename) { return mcpRequest('tools/call', { name: 'browser_take_screenshot', arguments: { filename }, }); } /** * Parse console error to extract file and line number */ function parseErrorDetails(error) { const result = { message: error, file: null, line: null, column: null, stack: [], }; // Try to parse stack trace const stackMatch = error.match(/at\s+(?:(.+)\s+\()?([^:]+):(\d+):(\d+)\)?/); if (stackMatch) { result.file = stackMatch[2]; result.line = parseInt(stackMatch[3]); result.column = parseInt(stackMatch[4]); } // Parse Chrome-style stack traces const chromePattern = /at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/g; let match; while ((match = chromePattern.exec(error)) !== null) { result.stack.push({ function: match[1], file: match[2], line: parseInt(match[3]), column: parseInt(match[4]), }); } return result; } /** * Check if error should be ignored */ function shouldIgnoreError(error) { const message = error.message || error; return config.ignoredPatterns.some(pattern => pattern && message.includes(pattern) ); } /** * Create Gitea Issue for error */ async function createGiteaIssue(errorData) { if (!config.giteaToken || !config.autoCreateIssues) { return null; } const fs = require('fs'); const path = require('path'); const title = `[Console Error] ${errorData.parsed.message.slice(0, 100)}`; const body = `## Console Error **Error Type**: ${errorData.type} **Message**: \`\`\` ${errorData.parsed.message} \`\`\` **Location**: ${errorData.parsed.file || 'Unknown'}:${errorData.parsed.line || '?'} **Page URL**: ${errorData.pageUrl} ### Stack Trace \`\`\` ${errorData.parsed.stack.map(s => `${s.function} (${s.file}:${s.line}:${s.column})`).join('\n') || 'No stack trace available'} \`\`\` ## Auto-Fix Required - [ ] Investigate the root cause - [ ] Implement fix - [ ] Add test case - [ ] Verify fix --- **Detected by**: Kilo Code Web Testing `; return new Promise((resolve, reject) => { const url = new URL(`${config.giteaApiUrl}/repos/${config.giteaRepo}/issues`); const bodyData = JSON.stringify({ title, body }); const client = url.protocol === 'https:' ? https : http; const req = client.request({ hostname: url.hostname, port: url.port || 443, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `token ${config.giteaToken}`, 'Content-Length': Buffer.byteLength(bodyData), }, }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', reject); req.write(bodyData); req.end(); }); } /** * Main console monitoring function */ async function main() { console.log('=== Console Error Monitor ===\n'); console.log(`Target URL: ${config.targetUrl}`); console.log(`Auto-create Issues: ${config.autoCreateIssues}\n`); const errors = { consoleErrors: [], networkErrors: [], uncaughtExceptions: [], }; try { // Navigate to target console.log('šŸ“” Navigating to target URL...'); await navigateTo(config.targetUrl); // Wait a bit for page to load await new Promise(resolve => setTimeout(resolve, 2000)); // Get console messages console.log('šŸ” Collecting console messages...'); const consoleResult = await getConsoleMessages('error', true); if (consoleResult.result?.content) { const messages = consoleResult.result.content; for (const msg of messages) { if (shouldIgnoreError(msg)) { console.log(' ā­ļø Ignored:', msg.slice(0, 80)); continue; } const parsed = parseErrorDetails(msg); const errorData = { type: 'console', message: msg, parsed, pageUrl: config.targetUrl, timestamp: new Date().toISOString(), }; errors.consoleErrors.push(errorData); console.log(' āŒ Console Error:', msg.slice(0, 80)); } } // Get failed network requests console.log('šŸ” Checking network requests...'); const networkResult = await getNetworkRequests('failed'); if (networkResult.result?.content) { for (const req of networkResult.result.content) { if (req.status >= 400) { errors.networkErrors.push({ type: 'network', url: req.url, status: req.status, method: req.method, pageUrl: config.targetUrl, timestamp: new Date().toISOString(), }); console.log(` āŒ Network Error: ${req.status} ${req.url}`); } } } // Take screenshot for context const screenshotFilename = `error-context-${Date.now()}.png`; await takeScreenshot(screenshotFilename); console.log(`šŸ“ø Screenshot saved: ${screenshotFilename}`); // Create Gitea Issues for critical errors if (config.autoCreateIssues) { console.log('\nšŸ“ Creating Gitea Issues...'); for (const error of errors.consoleErrors) { try { const issue = await createGiteaIssue(error); error.giteaIssue = issue?.html_url || null; if (issue) { console.log(` āœ… Issue created: ${issue.html_url}`); error.issueNumber = issue.number; } } catch (err) { console.log(` āŒ Failed to create issue: ${err.message}`); } } } } catch (error) { console.error('Error during monitoring:', error.message); } // Generate report const fs = require('fs'); const path = require('path'); const report = { timestamp: new Date().toISOString(), config: { targetUrl: config.targetUrl, autoCreateIssues: config.autoCreateIssues, }, summary: { consoleErrors: errors.consoleErrors.length, networkErrors: errors.networkErrors.length, totalErrors: errors.consoleErrors.length + errors.networkErrors.length, }, errors, }; const reportPath = path.join(config.reportsDir, 'console-errors-report.json'); if (!fs.existsSync(config.reportsDir)) { fs.mkdirSync(config.reportsDir, { recursive: true }); } fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log('\nšŸ“Š Summary:'); console.log(` Console Errors: ${errors.consoleErrors.length}`); console.log(` Network Errors: ${errors.networkErrors.length}`); console.log(` Total Errors: ${report.summary.totalErrors}`); console.log(`\nšŸ“„ Report saved to: ${reportPath}`); // Exit with error if errors found process.exit(report.summary.totalErrors > 0 ? 1 : 0); } main().catch(err => { console.error('Fatal error:', err); process.exit(1); });