"use strict"; /** * PerformanceCollector — captures Navigation Timing and Core Web Vitals after each navigation. * Detects performance_degradation anomalies based on configurable thresholds. */ 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PerformanceCollector = exports.DEFAULT_PERF_CONFIG = void 0; const crypto = __importStar(require("crypto")); exports.DEFAULT_PERF_CONFIG = { enabled: true, lcpThresholdMs: 4000, clsThreshold: 0.25, inpThresholdMs: 500, ttfbThresholdMs: 1800, }; class PerformanceCollector { constructor(config = {}) { this.metricsStore = []; this.config = { ...exports.DEFAULT_PERF_CONFIG, ...config }; } async collect(page, stateId, sessionId, actionTrace) { if (!this.config.enabled) { const empty = { id: crypto.randomUUID(), sessionId, stateId, url: page.url(), ttfb: 0, domContentLoaded: 0, loadComplete: 0, lcp: null, cls: null, fid: null, inp: null, totalRequests: 0, failedRequests: 0, capturedAt: Date.now(), }; return { metrics: empty, anomalies: [] }; } // Capture Navigation Timing const timing = await page.evaluate(() => { const t = performance.timing; return { ttfb: t.responseStart - t.requestStart, domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart, loadComplete: t.loadEventEnd - t.navigationStart, }; }).catch(() => ({ ttfb: 0, domContentLoaded: 0, loadComplete: 0 })); // Capture Core Web Vitals via PerformanceObserver const vitals = await page.evaluate(() => { return new Promise((resolve) => { const result = { lcp: null, cls: null, inp: null, }; try { // Try to observe LCP if ('PerformanceObserver' in window) { try { const lcpObs = new PerformanceObserver((list) => { const entries = list.getEntries(); if (entries.length > 0) { result.lcp = entries[entries.length - 1].startTime; } }); lcpObs.observe({ type: 'largest-contentful-paint', buffered: true }); } catch { /* not supported */ } try { const clsObs = new PerformanceObserver((list) => { let clsScore = 0; for (const entry of list.getEntries()) { // eslint-disable-next-line @typescript-eslint/no-explicit-any clsScore += entry.value ?? 0; } result.cls = clsScore; }); clsObs.observe({ type: 'layout-shift', buffered: true }); } catch { /* not supported */ } } } catch { /* ignore */ } // Resolve after short wait setTimeout(() => resolve(result), 500); }); }).catch(() => ({ lcp: null, cls: null, inp: null })); const metrics = { id: crypto.randomUUID(), sessionId, stateId, url: page.url(), ttfb: timing.ttfb, domContentLoaded: timing.domContentLoaded, loadComplete: timing.loadComplete, lcp: vitals.lcp, cls: vitals.cls, fid: null, inp: vitals.inp, totalRequests: 0, failedRequests: 0, capturedAt: Date.now(), }; this.metricsStore.push(metrics); const anomalies = this.detectAnomalies(metrics, stateId, actionTrace); return { metrics, anomalies }; } getMetrics() { return this.metricsStore; } detectAnomalies(metrics, stateId, actionTrace) { const anomalies = []; const issues = []; let severityRank = 0; // 0=low,1=medium,2=high,3=critical if (metrics.lcp !== null && metrics.lcp > this.config.lcpThresholdMs) { issues.push(`LCP: ${metrics.lcp}ms (threshold: ${this.config.lcpThresholdMs}ms)`); if (severityRank < 2) severityRank = 2; // high } if (metrics.cls !== null && metrics.cls > this.config.clsThreshold) { issues.push(`CLS: ${metrics.cls.toFixed(3)} (threshold: ${this.config.clsThreshold})`); if (severityRank < 1) severityRank = 1; // medium } if (metrics.inp !== null && metrics.inp > this.config.inpThresholdMs) { issues.push(`INP: ${metrics.inp}ms (threshold: ${this.config.inpThresholdMs}ms)`); if (severityRank < 2) severityRank = 2; // high } if (metrics.ttfb > this.config.ttfbThresholdMs) { issues.push(`TTFB: ${metrics.ttfb}ms (threshold: ${this.config.ttfbThresholdMs}ms)`); if (severityRank < 1) severityRank = 1; // medium } const RANK_TO_SEVERITY = ['low', 'medium', 'high', 'critical']; const maxSeverity = RANK_TO_SEVERITY[severityRank] ?? 'low'; if (issues.length === 0) return anomalies; anomalies.push({ id: crypto.randomUUID(), type: 'performance_degradation', severity: maxSeverity, observationId: stateId, actionTrace, description: `Performance degradation at ${metrics.url}: ${issues[0]}`, evidence: { rawErrors: issues, }, timestamp: Date.now(), }); return anomalies; } } exports.PerformanceCollector = PerformanceCollector;