178 lines
7.3 KiB
JavaScript
178 lines
7.3 KiB
JavaScript
"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;
|