UNPKG

@debugg-ai/debugg-ai-mcp

Version:

Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.

194 lines (193 loc) 7.52 kB
/** * harSummarizer — pure HAR + console aggregation utilities. * * Aggregation key for networkSummary: `origin + pathname` (per system reqs). * Refetch loops with varying query strings collapse into a single entry. * * Pure functions — no I/O, no async — so they can be reused by the future * `summarize_execution` tool. */ /** * Re-aggregate backend's pre-grouped network_summary entries by * `origin + pathname` (vs backend's full-URL key). Collapses refetch loops: * 5 separate `/api/poll?n=0..4` entries become 1 entry with count: 5. * * Backend `browser.capture` (commit 154e1e69) emits network_summary already * grouped by full URL with shape `{url, count, methods[], statuses{}, resource_types[]}`. * That preserves the per-request granularity but defeats the original * client #1 use case ("endpoint hit N times" refetch detection). MCP-side * re-aggregation runs once over the small pre-grouped list — cheap. */ export function reaggregateByOriginPath(entries) { if (!Array.isArray(entries)) return []; const buckets = new Map(); for (const e of entries) { try { const url = e?.url; if (typeof url !== 'string') continue; const parsed = new URL(url); const key = `${parsed.origin}${parsed.pathname}`; const count = typeof e.count === 'number' ? e.count : 0; const statuses = e.statuses ?? {}; const existing = buckets.get(key); if (existing) { existing.count += count; for (const [code, n] of Object.entries(statuses)) { if (typeof n === 'number') { existing.statuses[code] = (existing.statuses[code] ?? 0) + n; } } } else { const out = { url: key, count, statuses: { ...statuses }, totalBytes: 0, // Backend's pre-grouped shape doesn't expose response bytes; placeholder until we wire fetched-bytes. }; buckets.set(key, out); } } catch { // malformed URL — skip } } return [...buckets.values()].sort((a, b) => b.count - a.count); } /** * Map backend's console_slice entry shape to MCP's ConsoleErrorEntry. * Backend shape: {text, level, location: {url, line}, timestamp} * MCP shape: {level, text, source?, lineNumber?, timestamp?} */ export function mapConsoleSlice(entries) { if (!Array.isArray(entries)) return []; const out = []; for (const e of entries) { if (typeof e !== 'object' || e === null) continue; const entry = { level: typeof e.level === 'string' ? e.level : 'log', text: typeof e.text === 'string' ? e.text : '', }; const loc = e.location ?? {}; if (typeof loc.url === 'string' && loc.url) entry.source = loc.url; else if (typeof e.source === 'string' && e.source) entry.source = e.source; if (typeof loc.line === 'number') entry.lineNumber = loc.line; else if (typeof e.lineNumber === 'number') entry.lineNumber = e.lineNumber; // Backend timestamps are ISO strings; MCP type uses number (ms since epoch). // Coerce when possible; otherwise pass through unchanged. if (typeof e.timestamp === 'number') { entry.timestamp = e.timestamp; } else if (typeof e.timestamp === 'string') { const parsed = Date.parse(e.timestamp); if (!isNaN(parsed)) entry.timestamp = parsed; } out.push(entry); } return out; } /** * Aggregate HAR `log.entries` into per-endpoint NetworkSummary[], sorted * descending by request count (hottest endpoints first). Malformed entries * (missing request.url or response.status) are skipped, not thrown. */ export function summarizeHar(harEntries) { if (!Array.isArray(harEntries)) return []; const buckets = new Map(); for (const entry of harEntries) { try { const reqUrl = entry?.request?.url; const status = entry?.response?.status; if (typeof reqUrl !== 'string' || typeof status !== 'number') continue; // Aggregation key: origin + pathname (refetch loops collapse). let parsed; try { parsed = new URL(reqUrl); } catch { continue; } const key = `${parsed.origin}${parsed.pathname}`; const bytesRaw = entry?.response?.content?.size; const bytes = typeof bytesRaw === 'number' && bytesRaw >= 0 ? bytesRaw : 0; const mime = entry?.response?.content?.mimeType; const mimeStr = typeof mime === 'string' && mime ? mime : ''; const existing = buckets.get(key); if (existing) { existing.count++; const sk = String(status); existing.statuses[sk] = (existing.statuses[sk] ?? 0) + 1; existing.totalBytes += bytes; if (mimeStr) existing.mimeTypes.add(mimeStr); } else { buckets.set(key, { url: key, count: 1, statuses: { [String(status)]: 1 }, totalBytes: bytes, mimeTypes: mimeStr ? new Set([mimeStr]) : new Set(), }); } } catch { // malformed — skip } } return [...buckets.values()] .map(({ mimeTypes, url, count, statuses, totalBytes }) => { const out = { url, count, statuses, totalBytes }; // Only attach mimeType when homogeneous — mixed types omit the field. if (mimeTypes.size === 1) { out.mimeType = [...mimeTypes][0]; } return out; }) .sort((a, b) => b.count - a.count); } /** * Normalize a console-log JSON array into ConsoleErrorEntry[]. * Maps backend's snake_case (`line_number`, `url`) to MCP's camelCase * (`lineNumber`, `source`). Drops entries that aren't plain objects. */ export function summarizeConsole(consoleEntries) { if (!Array.isArray(consoleEntries)) return []; const out = []; for (const e of consoleEntries) { if (typeof e !== 'object' || e === null) continue; const entry = { level: typeof e.level === 'string' ? e.level : 'log', text: typeof e.text === 'string' ? e.text : '', }; // source: prefer `url` (backend convention), fall back to `source` const sourceVal = typeof e.url === 'string' && e.url ? e.url : (typeof e.source === 'string' && e.source ? e.source : undefined); if (sourceVal) entry.source = sourceVal; // lineNumber: snake_case from backend → camelCase const lineVal = typeof e.line_number === 'number' ? e.line_number : (typeof e.lineNumber === 'number' ? e.lineNumber : undefined); if (typeof lineVal === 'number') entry.lineNumber = lineVal; if (typeof e.timestamp === 'number') entry.timestamp = e.timestamp; out.push(entry); } return out; }