UNPKG

@stacksleuth/cli

Version:

Comprehensive command-line interface for StackSleuth - Interactive dashboard, real-time monitoring, performance reports, CI/CD integration, and automated performance optimization recommendations.

380 lines (362 loc) 13.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DashboardServer = void 0; const http_1 = __importDefault(require("http")); const ws_1 = require("ws"); // Dynamic import for chalk to handle ESM compatibility let chalk; async function initChalk() { if (!chalk) { chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default; } return chalk; } class DashboardServer { constructor(collector, port) { this.collector = collector; this.port = port; } async start() { return new Promise(async (resolve, reject) => { const c = await initChalk(); // Create HTTP server with embedded HTML dashboard this.server = http_1.default.createServer((req, res) => { if (req.url === '/') { this.serveDashboard(res); } else if (req.url === '/api/traces') { this.serveTraces(res); } else if (req.url === '/api/stats') { this.serveStats(res); } else { res.writeHead(404); res.end('Not Found'); } }); // Set up WebSocket server for real-time updates this.wsServer = new ws_1.WebSocketServer({ server: this.server }); this.setupWebSocket(); this.server.listen(this.port, () => { console.log(c.green(`📊 Dashboard available at http://localhost:${this.port}`)); resolve(); }); this.server.on('error', reject); }); } async stop() { return new Promise((resolve) => { if (this.wsServer) { this.wsServer.close(); } if (this.server) { this.server.close(() => { resolve(); }); } else { resolve(); } }); } setupWebSocket() { if (!this.wsServer) return; this.wsServer.on('connection', (ws) => { // Send initial data ws.send(JSON.stringify({ type: 'initial', data: { traces: this.collector.getAllTraces(), stats: this.collector.getStats() } })); // Forward real-time events const traceListener = (trace) => { ws.send(JSON.stringify({ type: 'trace:completed', data: trace })); }; const issueListener = (issue) => { ws.send(JSON.stringify({ type: 'performance:issue', data: issue })); }; this.collector.on('trace:completed', traceListener); this.collector.on('performance:issue', issueListener); ws.on('close', () => { this.collector.off('trace:completed', traceListener); this.collector.off('performance:issue', issueListener); }); }); } serveDashboard(res) { const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>StackSleuth Dashboard</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { background: #1e293b; padding: 20px 0; border-bottom: 1px solid #334155; margin-bottom: 30px; } h1 { color: #06b6d4; font-size: 2rem; font-weight: 700; text-align: center; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background: #1e293b; padding: 20px; border-radius: 8px; border: 1px solid #334155; } .stat-label { color: #94a3b8; font-size: 0.9rem; } .stat-value { color: #06b6d4; font-size: 1.8rem; font-weight: 700; } .traces-section h2 { color: #e2e8f0; margin-bottom: 20px; font-size: 1.5rem; } .trace-item { background: #1e293b; padding: 15px; margin-bottom: 10px; border-radius: 6px; border-left: 4px solid #06b6d4; border: 1px solid #334155; } .trace-name { color: #e2e8f0; font-weight: 600; } .trace-meta { color: #94a3b8; font-size: 0.9rem; margin-top: 5px; } .trace-duration { color: #06b6d4; font-weight: 500; } .trace-error { border-left-color: #ef4444; } .trace-slow { border-left-color: #f59e0b; } .status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: 500; } .status-success { background: #064e3b; color: #10b981; } .status-error { background: #450a0a; color: #ef4444; } .status-pending { background: #451a03; color: #f59e0b; } .issues-section { margin-top: 30px; } .issue-item { background: #1e293b; padding: 15px; margin-bottom: 10px; border-radius: 6px; border: 1px solid #334155; } .issue-critical { border-left: 4px solid #ef4444; } .issue-high { border-left: 4px solid #f59e0b; } .issue-medium { border-left: 4px solid #eab308; } .issue-low { border-left: 4px solid #6b7280; } .empty-state { text-align: center; color: #64748b; padding: 40px; font-style: italic; } .connection-status { position: fixed; top: 20px; right: 20px; padding: 8px 16px; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .connected { background: #064e3b; color: #10b981; } .disconnected { background: #450a0a; color: #ef4444; } </style> </head> <body> <div class="connection-status" id="connectionStatus">Connecting...</div> <header> <div class="container"> <h1>StackSleuth Dashboard</h1> </div> </header> <div class="container"> <div class="stats-grid" id="statsGrid"> <div class="stat-card"> <div class="stat-label">Total Traces</div> <div class="stat-value" id="totalTraces">0</div> </div> <div class="stat-card"> <div class="stat-label">Average Duration</div> <div class="stat-value" id="avgDuration">0ms</div> </div> <div class="stat-card"> <div class="stat-label">P95 Duration</div> <div class="stat-value" id="p95Duration">0ms</div> </div> <div class="stat-card"> <div class="stat-label">Total Spans</div> <div class="stat-value" id="totalSpans">0</div> </div> </div> <div class="traces-section"> <h2>Recent Traces</h2> <div id="tracesList"></div> </div> <div class="issues-section"> <h2>Performance Issues</h2> <div id="issuesList"></div> </div> </div> <script> const ws = new WebSocket('ws://localhost:${this.port}'); const statusEl = document.getElementById('connectionStatus'); const tracesEl = document.getElementById('tracesList'); const issuesEl = document.getElementById('issuesList'); let traces = []; let issues = []; ws.onopen = () => { statusEl.textContent = 'Connected'; statusEl.className = 'connection-status connected'; }; ws.onclose = () => { statusEl.textContent = 'Disconnected'; statusEl.className = 'connection-status disconnected'; }; ws.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'initial': traces = message.data.traces; updateStats(message.data.stats); renderTraces(); break; case 'trace:completed': traces.unshift(message.data); traces = traces.slice(0, 50); // Keep last 50 traces renderTraces(); break; case 'performance:issue': issues.unshift(message.data); issues = issues.slice(0, 20); // Keep last 20 issues renderIssues(); break; } }; function updateStats(stats) { document.getElementById('totalTraces').textContent = stats.traces.total; document.getElementById('avgDuration').textContent = stats.traces.avg.toFixed(2) + 'ms'; document.getElementById('p95Duration').textContent = stats.traces.p95.toFixed(2) + 'ms'; document.getElementById('totalSpans').textContent = stats.spans.total; } function renderTraces() { if (traces.length === 0) { tracesEl.innerHTML = '<div class="empty-state">No traces yet. Make requests to your application to see data.</div>'; return; } tracesEl.innerHTML = traces.map(trace => { const duration = trace.timing.duration || 0; const isError = trace.status === 'error'; const isSlow = duration > 1000; return \` <div class="trace-item \${isError ? 'trace-error' : isSlow ? 'trace-slow' : ''}"> <div class="trace-name">\${trace.name}</div> <div class="trace-meta"> <span class="trace-duration">\${duration.toFixed(2)}ms</span> • <span class="status status-\${trace.status}">\${trace.status}</span> • \${trace.spans.length} spans • \${new Date(trace.timing.start.millis).toLocaleTimeString()} </div> </div> \`; }).join(''); } function renderIssues() { if (issues.length === 0) { issuesEl.innerHTML = '<div class="empty-state">No performance issues detected.</div>'; return; } issuesEl.innerHTML = issues.map(issue => \` <div class="issue-item issue-\${issue.severity}"> <strong>\${issue.severity.toUpperCase()}: \${issue.message}</strong> \${issue.suggestion ? \`<div style="margin-top: 8px; color: #94a3b8;">💡 \${issue.suggestion}</div>\` : ''} </div> \`).join(''); } </script> </body> </html>`; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } serveTraces(res) { const traces = this.collector.getAllTraces(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(traces)); } serveStats(res) { const stats = this.collector.getStats(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats)); } } exports.DashboardServer = DashboardServer; //# sourceMappingURL=server.js.map