Update E2E tests and simplify performance tests
CI Pipeline / e2e (chromium) (pull_request) Successful in 6m13s
CI Pipeline / e2e (firefox) (pull_request) Successful in 7m3s
CI Pipeline / e2e (webkit) (pull_request) Successful in 5m52s
CI Pipeline / visual-regression (pull_request) Successful in 7m48s
CI Pipeline / performance (pull_request) Successful in 7m59s
CI Pipeline / lint (pull_request) Successful in 6m16s
CI Pipeline / build (pull_request) Successful in 5m30s
CI Pipeline / test (pull_request) Successful in 6m26s

This commit is contained in:
adilallo
2026-01-28 18:22:59 -07:00
parent 9cb89162ab
commit a30bf6be4c
124 changed files with 452 additions and 4190 deletions
-297
View File
@@ -1,297 +0,0 @@
#!/usr/bin/env node
/**
* Performance Monitoring Script
* Monitors Core Web Vitals and performance metrics
*/
const fs = require("fs");
const path = require("path");
const PERFORMANCE_BUDGETS = require("../performance-budgets.json");
const MONITORING_DIR = path.join(__dirname, "..", ".next", "monitoring");
class PerformanceMonitor {
constructor() {
this.metrics = {
timestamp: new Date().toISOString(),
coreWebVitals: {},
bundleMetrics: {},
recommendations: [],
};
}
/**
* Run comprehensive performance monitoring
*/
async monitorPerformance() {
console.log("📊 Starting performance monitoring...");
try {
// Ensure monitoring directory exists
if (!fs.existsSync(MONITORING_DIR)) {
fs.mkdirSync(MONITORING_DIR, { recursive: true });
}
// Run Lighthouse CI for Core Web Vitals
await this.runLighthouseCI();
// Analyze bundle performance
await this.analyzeBundlePerformance();
// Check performance budgets
this.checkPerformanceBudgets();
// Generate performance report
this.generatePerformanceReport();
console.log("✅ Performance monitoring complete!");
console.log(`📁 Results saved to: ${MONITORING_DIR}`);
} catch (error) {
console.error("❌ Performance monitoring failed:", error.message);
process.exit(1);
}
}
/**
* Run Lighthouse CI for Core Web Vitals
*/
async runLighthouseCI() {
console.log("🔍 Running Lighthouse CI...");
try {
// Check if server is running
const { execSync } = require("child_process");
try {
execSync("curl -s http://localhost:3000 > /dev/null", {
stdio: "pipe",
});
} catch {
console.warn(
"⚠️ Development server not running, skipping Lighthouse CI...",
);
return;
}
// Run Lighthouse CI with performance focus
execSync("npx lhci autorun --collect.url=http://localhost:3000", {
stdio: "inherit",
cwd: path.join(__dirname, ".."),
});
// Parse Lighthouse results
await this.parseLighthouseResults();
} catch {
console.warn("⚠️ Lighthouse CI failed, continuing with other metrics...");
}
}
/**
* Parse Lighthouse CI results
*/
async parseLighthouseResults() {
const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci");
if (fs.existsSync(lhciResultsPath)) {
const files = fs.readdirSync(lhciResultsPath);
const resultFile = files.find((f) => f.endsWith(".json"));
if (resultFile) {
const results = JSON.parse(
fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8"),
);
if (results.lhr && results.lhr.audits) {
this.metrics.coreWebVitals = {
lcp: this.getAuditScore(
results.lhr.audits,
"largest-contentful-paint",
),
fid: this.getAuditScore(results.lhr.audits, "max-potential-fid"),
cls: this.getAuditScore(
results.lhr.audits,
"cumulative-layout-shift",
),
fcp: this.getAuditScore(
results.lhr.audits,
"first-contentful-paint",
),
tti: this.getAuditScore(results.lhr.audits, "interactive"),
performance: results.lhr.categories.performance?.score * 100 || 0,
};
}
}
}
}
/**
* Get audit score from Lighthouse results
*/
getAuditScore(audits, auditId) {
const audit = audits[auditId];
if (!audit) return null;
return {
score: audit.score * 100,
value: audit.numericValue,
displayValue: audit.displayValue,
};
}
/**
* Analyze bundle performance
*/
async analyzeBundlePerformance() {
console.log("📦 Analyzing bundle performance...");
const bundleStatsPath = path.join(
__dirname,
"..",
".next",
"static",
"chunks",
);
if (fs.existsSync(bundleStatsPath)) {
const files = fs.readdirSync(bundleStatsPath);
let totalSize = 0;
let jsFiles = 0;
files.forEach((file) => {
if (file.endsWith(".js")) {
const filePath = path.join(bundleStatsPath, file);
const stats = fs.statSync(filePath);
totalSize += stats.size;
jsFiles++;
}
});
this.metrics.bundleMetrics = {
totalSizeKB: Math.round(totalSize / 1024),
totalSizeMB: Math.round((totalSize / (1024 * 1024)) * 100) / 100,
fileCount: jsFiles,
averageSizeKB: Math.round(totalSize / jsFiles / 1024),
};
}
}
/**
* Check performance budgets
*/
checkPerformanceBudgets() {
const budgets = PERFORMANCE_BUDGETS.budgets;
const violations = [];
// Check Core Web Vitals
if (this.metrics.coreWebVitals.lcp) {
const lcpValue = this.metrics.coreWebVitals.lcp.value;
const lcpBudget = budgets.find((b) => b.name === "lcp")?.maxValue;
if (lcpBudget && lcpValue > lcpBudget) {
violations.push({
metric: "LCP",
current: lcpValue,
budget: lcpBudget,
severity: lcpValue > lcpBudget * 1.5 ? "high" : "medium",
});
}
}
// Check bundle size
if (this.metrics.bundleMetrics.totalSizeKB > 2000) {
violations.push({
metric: "Bundle Size",
current: this.metrics.bundleMetrics.totalSizeKB,
budget: 2000,
severity: "medium",
});
}
this.metrics.budgetViolations = violations;
}
/**
* Generate performance report
*/
generatePerformanceReport() {
const reportPath = path.join(MONITORING_DIR, "performance-report.json");
fs.writeFileSync(reportPath, JSON.stringify(this.metrics, null, 2));
// Generate markdown report
this.generateMarkdownReport();
}
/**
* Generate markdown performance report
*/
generateMarkdownReport() {
const reportPath = path.join(MONITORING_DIR, "performance-report.md");
let report = `# Performance Monitoring Report\n\n`;
report += `**Generated:** ${this.metrics.timestamp}\n\n`;
// Core Web Vitals
if (Object.keys(this.metrics.coreWebVitals).length > 0) {
report += `## Core Web Vitals\n\n`;
report += `| Metric | Score | Value | Status |\n`;
report += `|--------|-------|-------|--------|\n`;
Object.entries(this.metrics.coreWebVitals).forEach(([metric, data]) => {
if (data && typeof data === "object" && data.score !== undefined) {
const status = this.getMetricStatus(metric, data.score);
report += `| ${metric.toUpperCase()} | ${data.score} | ${
data.displayValue || "N/A"
} | ${status} |\n`;
}
});
}
// Bundle Metrics
if (Object.keys(this.metrics.bundleMetrics).length > 0) {
report += `\n## Bundle Metrics\n\n`;
report += `- **Total Size:** ${this.metrics.bundleMetrics.totalSizeMB}MB (${this.metrics.bundleMetrics.totalSizeKB}KB)\n`;
report += `- **File Count:** ${this.metrics.bundleMetrics.fileCount}\n`;
report += `- **Average Size:** ${this.metrics.bundleMetrics.averageSizeKB}KB per file\n`;
}
// Budget Violations
if (
this.metrics.budgetViolations &&
this.metrics.budgetViolations.length > 0
) {
report += `\n## Budget Violations\n\n`;
this.metrics.budgetViolations.forEach((violation) => {
report += `- **${violation.metric}**: ${violation.current} (exceeds ${
violation.budget
}) - ${violation.severity.toUpperCase()}\n`;
});
}
// Recommendations
report += `\n## Recommendations\n\n`;
report += `- Monitor Core Web Vitals regularly\n`;
report += `- Implement code splitting for large bundles\n`;
report += `- Use dynamic imports for non-critical components\n`;
report += `- Optimize images and fonts\n`;
report += `- Enable compression and caching\n`;
fs.writeFileSync(reportPath, report);
}
/**
* Get status emoji for metric score
*/
getMetricStatus(metric, score) {
if (score >= 90) return "✅ Good";
if (score >= 50) return "⚠️ Needs Improvement";
return "❌ Poor";
}
}
// Run monitoring if called directly
if (require.main === module) {
const monitor = new PerformanceMonitor();
monitor.monitorPerformance().catch(console.error);
}
module.exports = PerformanceMonitor;
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env node
/**
* Simple test script to verify LHCI configuration
* This script validates the configuration without running actual tests
*/
const fs = require("fs");
const path = require("path");
console.log("🔍 Testing LHCI Configuration...\n");
// Check if .lighthouserc.json exists
const configPath = path.join(process.cwd(), ".lighthouserc.json");
if (fs.existsSync(configPath)) {
console.log("✅ .lighthouserc.json found");
try {
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
console.log("✅ Configuration is valid JSON");
if (config.ci && config.ci.collect && config.ci.assert) {
console.log("✅ Configuration has required sections (collect, assert)");
console.log(`✅ Testing ${config.ci.collect.numberOfRuns} runs`);
console.log(`✅ URL: ${config.ci.collect.url[0]}`);
} else {
console.log("❌ Configuration missing required sections");
}
} catch (error) {
console.log("❌ Configuration is not valid JSON:", error.message);
}
} else {
console.log("❌ .lighthouserc.json not found");
}
// Check if @lhci/cli is installed
try {
const { execSync } = require("child_process");
execSync("npx lhci --version", { stdio: "pipe" });
console.log("✅ @lhci/cli package is installed and working");
} catch (error) {
console.log("❌ @lhci/cli package is not working:", error.message);
}
// Check package.json scripts
const packagePath = path.join(process.cwd(), "package.json");
if (fs.existsSync(packagePath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
if (packageJson.scripts && packageJson.scripts.lhci) {
console.log("✅ LHCI script found in package.json");
} else {
console.log("❌ LHCI script not found in package.json");
}
} catch (error) {
console.log("❌ Error reading package.json:", error.message);
}
}
console.log("\n🎉 LHCI Configuration Test Complete!");
console.log(
"Note: Actual LHCI tests may fail locally due to Node.js architecture issues on macOS.",
);
console.log(
"The CI environment should work correctly with the provided configuration.",
);
-349
View File
@@ -1,349 +0,0 @@
#!/usr/bin/env node
/**
* Comprehensive Performance Testing Script
* Integrates bundle analysis, performance monitoring, and Web Vitals tracking
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const TEST_RESULTS_DIR = path.join(__dirname, "..", ".next", "test-results");
class PerformanceTester {
constructor() {
this.results = {
timestamp: new Date().toISOString(),
bundleAnalysis: {},
performanceMonitoring: {},
webVitals: {},
lighthouse: {},
summary: {
passed: 0,
failed: 0,
warnings: 0,
total: 0,
},
};
}
/**
* Run comprehensive performance testing
*/
async runTests() {
console.log("🧪 Starting comprehensive performance testing...");
try {
// Ensure test results directory exists
if (!fs.existsSync(TEST_RESULTS_DIR)) {
fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true });
}
// 1. Bundle Analysis
console.log("📊 Running bundle analysis...");
await this.runBundleAnalysis();
// 2. Performance Monitoring
console.log("📈 Running performance monitoring...");
await this.runPerformanceMonitoring();
// 3. Web Vitals Tracking
console.log("📊 Setting up Web Vitals tracking...");
await this.runWebVitalsTracking();
// 4. Lighthouse CI (if server is available)
console.log("🔍 Running Lighthouse CI...");
await this.runLighthouseCI();
// 5. Generate comprehensive report
this.generateComprehensiveReport();
console.log("✅ Performance testing complete!");
console.log(`📁 Results saved to: ${TEST_RESULTS_DIR}`);
// Return exit code based on results
const hasFailures = this.results.summary.failed > 0;
if (hasFailures) {
console.log("❌ Performance tests failed");
process.exit(1);
} else {
console.log("✅ All performance tests passed");
process.exit(0);
}
} catch (error) {
console.error("❌ Performance testing failed:", error.message);
process.exit(1);
}
}
/**
* Run bundle analysis
*/
async runBundleAnalysis() {
try {
execSync("npm run bundle:analyze", { stdio: "inherit" });
// Parse bundle analysis results
const bundleReportPath = path.join(
__dirname,
"..",
".next",
"analyze",
"bundle-analysis.json",
);
if (fs.existsSync(bundleReportPath)) {
const bundleData = JSON.parse(
fs.readFileSync(bundleReportPath, "utf8"),
);
this.results.bundleAnalysis = bundleData;
// Check for budget violations
if (
bundleData.budgetViolations &&
bundleData.budgetViolations.length > 0
) {
this.results.summary.failed += bundleData.budgetViolations.length;
console.log(
`⚠️ Found ${bundleData.budgetViolations.length} budget violations`,
);
} else {
this.results.summary.passed += 1;
console.log("✅ Bundle analysis passed");
}
}
this.results.summary.total += 1;
} catch (error) {
console.error("❌ Bundle analysis failed:", error.message);
this.results.summary.failed += 1;
this.results.summary.total += 1;
}
}
/**
* Run performance monitoring
*/
async runPerformanceMonitoring() {
try {
execSync("npm run performance:monitor", { stdio: "inherit" });
// Parse performance monitoring results
const perfReportPath = path.join(
__dirname,
"..",
".next",
"monitoring",
"performance-report.json",
);
if (fs.existsSync(perfReportPath)) {
const perfData = JSON.parse(fs.readFileSync(perfReportPath, "utf8"));
this.results.performanceMonitoring = perfData;
// Check for budget violations
if (perfData.budgetViolations && perfData.budgetViolations.length > 0) {
this.results.summary.failed += perfData.budgetViolations.length;
console.log(
`⚠️ Found ${perfData.budgetViolations.length} performance violations`,
);
} else {
this.results.summary.passed += 1;
console.log("✅ Performance monitoring passed");
}
}
this.results.summary.total += 1;
} catch (error) {
console.error("❌ Performance monitoring failed:", error.message);
this.results.summary.failed += 1;
this.results.summary.total += 1;
}
}
/**
* Run Web Vitals tracking
*/
async runWebVitalsTracking() {
try {
execSync("npm run web-vitals:track", { stdio: "inherit" });
// Parse Web Vitals results
const vitalsReportPath = path.join(
__dirname,
"..",
".next",
"web-vitals",
"report.json",
);
if (fs.existsSync(vitalsReportPath)) {
const vitalsData = JSON.parse(
fs.readFileSync(vitalsReportPath, "utf8"),
);
this.results.webVitals = vitalsData;
console.log("✅ Web Vitals tracking setup complete");
}
this.results.summary.passed += 1;
this.results.summary.total += 1;
} catch (error) {
console.error("❌ Web Vitals tracking failed:", error.message);
this.results.summary.failed += 1;
this.results.summary.total += 1;
}
}
/**
* Run Lighthouse CI
*/
async runLighthouseCI() {
try {
// Check if server is running
try {
execSync("curl -s http://localhost:3000 > /dev/null", {
stdio: "pipe",
});
} catch {
console.warn(
"⚠️ Development server not running, skipping Lighthouse CI...",
);
this.results.summary.warnings += 1;
this.results.summary.total += 1;
return;
}
execSync("npm run lhci", { stdio: "inherit" });
// Parse Lighthouse results
const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci");
if (fs.existsSync(lhciResultsPath)) {
const files = fs.readdirSync(lhciResultsPath);
const resultFile = files.find((f) => f.endsWith(".json"));
if (resultFile) {
const lhciData = JSON.parse(
fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8"),
);
this.results.lighthouse = lhciData;
console.log("✅ Lighthouse CI completed");
}
}
this.results.summary.passed += 1;
this.results.summary.total += 1;
} catch (error) {
console.warn("⚠️ Lighthouse CI failed:", error.message);
this.results.summary.warnings += 1;
this.results.summary.total += 1;
}
}
/**
* Generate comprehensive test report
*/
generateComprehensiveReport() {
// Ensure test results directory exists
if (!fs.existsSync(TEST_RESULTS_DIR)) {
fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true });
}
const reportPath = path.join(
TEST_RESULTS_DIR,
"performance-test-report.json",
);
fs.writeFileSync(reportPath, JSON.stringify(this.results, null, 2));
// Generate markdown report
this.generateMarkdownReport();
}
/**
* Generate markdown test report
*/
generateMarkdownReport() {
const reportPath = path.join(
TEST_RESULTS_DIR,
"performance-test-report.md",
);
let report = `# Performance Test Report\n\n`;
report += `**Generated:** ${this.results.timestamp}\n\n`;
// Summary
report += `## Test Summary\n\n`;
report += `- **Total Tests:** ${this.results.summary.total}\n`;
report += `- **Passed:** ${this.results.summary.passed}\n`;
report += `- **Failed:** ${this.results.summary.failed}\n`;
report += `- **Warnings:** ${this.results.summary.warnings} ⚠️\n\n`;
// Bundle Analysis Results
if (Object.keys(this.results.bundleAnalysis).length > 0) {
report += `## Bundle Analysis\n\n`;
if (
this.results.bundleAnalysis.budgetViolations &&
this.results.bundleAnalysis.budgetViolations.length > 0
) {
report += `### Budget Violations\n\n`;
this.results.bundleAnalysis.budgetViolations.forEach((violation) => {
report += `- **${violation.file}**: ${
violation.currentSize
}KB (exceeds ${violation.maxSize}KB by ${
violation.overage
}KB) - ${violation.severity.toUpperCase()}\n`;
});
} else {
report += `✅ No bundle budget violations found\n\n`;
}
}
// Performance Monitoring Results
if (Object.keys(this.results.performanceMonitoring).length > 0) {
report += `## Performance Monitoring\n\n`;
if (
this.results.performanceMonitoring.budgetViolations &&
this.results.performanceMonitoring.budgetViolations.length > 0
) {
report += `### Budget Violations\n\n`;
this.results.performanceMonitoring.budgetViolations.forEach(
(violation) => {
report += `- **${violation.metric}**: ${
violation.current
} (exceeds ${
violation.budget
}) - ${violation.severity.toUpperCase()}\n`;
},
);
} else {
report += `✅ No performance budget violations found\n\n`;
}
}
// Web Vitals Results
if (Object.keys(this.results.webVitals).length > 0) {
report += `## Web Vitals Tracking\n\n`;
report += `✅ Web Vitals tracking setup complete\n\n`;
}
// Lighthouse Results
if (Object.keys(this.results.lighthouse).length > 0) {
report += `## Lighthouse CI\n\n`;
report += `✅ Lighthouse CI completed successfully\n\n`;
}
// Recommendations
report += `## Recommendations\n\n`;
report += `- Monitor bundle sizes regularly\n`;
report += `- Track Core Web Vitals in production\n`;
report += `- Run performance tests in CI/CD pipeline\n`;
report += `- Set up performance budgets and alerts\n`;
fs.writeFileSync(reportPath, report);
}
}
// Run if called directly
if (require.main === module) {
const tester = new PerformanceTester();
tester.runTests().catch(console.error);
}
module.exports = PerformanceTester;
-335
View File
@@ -1,335 +0,0 @@
#!/usr/bin/env node
/**
* Web Vitals Tracker
* Real-time monitoring of Core Web Vitals in production
*/
const fs = require("fs");
const path = require("path");
const WEB_VITALS_DIR = path.join(__dirname, "..", ".next", "web-vitals");
class WebVitalsTracker {
constructor() {
this.metrics = {
timestamp: new Date().toISOString(),
vitals: {
lcp: [],
fid: [],
cls: [],
fcp: [],
ttfb: [],
},
summary: {},
};
}
/**
* Track Web Vitals from client-side
*/
trackWebVitals() {
const trackingCode = `
// Web Vitals Tracking Script
(function() {
// Import web-vitals library
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
const vitals = {};
// Track Largest Contentful Paint
getLCP((metric) => {
vitals.lcp = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('lcp', vitals.lcp);
});
// Track First Input Delay
getFID((metric) => {
vitals.fid = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('fid', vitals.fid);
});
// Track Cumulative Layout Shift
getCLS((metric) => {
vitals.cls = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('cls', vitals.cls);
});
// Track First Contentful Paint
getFCP((metric) => {
vitals.fcp = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('fcp', vitals.fcp);
});
// Track Time to First Byte
getTTFB((metric) => {
vitals.ttfb = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('ttfb', vitals.ttfb);
});
});
// Send vitals to server
function sendVitals(metric, data) {
if (typeof window !== 'undefined' && window.fetch) {
fetch('/api/web-vitals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metric,
data,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
}).catch(console.error);
}
}
})();
`;
return trackingCode;
}
/**
* Create API endpoint for receiving Web Vitals
*/
createAPIEndpoint() {
const apiCode = `
// API endpoint for Web Vitals tracking
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { metric, data, url, userAgent, timestamp } = req.body;
// Store the metric data
const vitalsData = {
metric,
data,
url,
userAgent,
timestamp: new Date(timestamp).toISOString()
};
// In production, you would save this to a database
// For now, we'll log it
console.log('Web Vital received:', vitalsData);
res.status(200).json({ success: true });
} catch (error) {
console.error('Error processing web vital:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
`;
return apiCode;
}
/**
* Generate Web Vitals dashboard
*/
generateDashboard() {
const dashboardCode = `
import React, { useState, useEffect } from 'react';
const WebVitalsDashboard = () => {
const [vitals, setVitals] = useState({
lcp: { value: 0, rating: 'unknown' },
fid: { value: 0, rating: 'unknown' },
cls: { value: 0, rating: 'unknown' },
fcp: { value: 0, rating: 'unknown' },
ttfb: { value: 0, rating: 'unknown' }
});
useEffect(() => {
// In a real implementation, you would fetch from your database
// For now, we'll use localStorage for demo purposes
const storedVitals = localStorage.getItem('web-vitals');
if (storedVitals) {
setVitals(JSON.parse(storedVitals));
}
}, []);
const getRatingColor = (rating) => {
switch (rating) {
case 'good': return 'text-green-600';
case 'needs-improvement': return 'text-yellow-600';
case 'poor': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getRatingIcon = (rating) => {
switch (rating) {
case 'good': return '✅';
case 'needs-improvement': return '⚠️';
case 'poor': return '❌';
default: return '❓';
}
};
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6">Web Vitals Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(vitals).map(([metric, data]) => (
<div key={metric} className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{metric.toUpperCase()}</h3>
<span className="text-2xl">{getRatingIcon(data.rating)}</span>
</div>
<div className="text-sm text-gray-600">
<div>Value: {data.value}ms</div>
<div className={\`font-medium \${getRatingColor(data.rating)}\`}>
Rating: {data.rating.replace('-', ' ')}
</div>
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold mb-2">Performance Guidelines</h3>
<ul className="text-sm space-y-1">
<li>• <strong>LCP:</strong> Good &lt; 2.5s, Needs Improvement 2.5-4s, Poor &gt; 4s</li>
<li>• <strong>FID:</strong> Good &lt; 100ms, Needs Improvement 100-300ms, Poor &gt; 300ms</li>
<li>• <strong>CLS:</strong> Good &lt; 0.1, Needs Improvement 0.1-0.25, Poor &gt; 0.25</li>
<li>• <strong>FCP:</strong> Good &lt; 1.8s, Needs Improvement 1.8-3s, Poor &gt; 3s</li>
<li>• <strong>TTFB:</strong> Good &lt; 800ms, Needs Improvement 800-1800ms, Poor &gt; 1800ms</li>
</ul>
</div>
</div>
);
};
export default WebVitalsDashboard;
`;
return dashboardCode;
}
/**
* Save Web Vitals data
*/
saveVitalsData(metric, data) {
if (!fs.existsSync(WEB_VITALS_DIR)) {
fs.mkdirSync(WEB_VITALS_DIR, { recursive: true });
}
const filePath = path.join(WEB_VITALS_DIR, `${metric}.json`);
let existingData = [];
if (fs.existsSync(filePath)) {
try {
existingData = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
console.warn("Could not parse existing vitals data:", error.message);
}
}
existingData.push({
...data,
timestamp: new Date().toISOString(),
});
// Keep only last 100 entries
if (existingData.length > 100) {
existingData = existingData.slice(-100);
}
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
}
/**
* Generate Web Vitals report
*/
generateReport() {
if (!fs.existsSync(WEB_VITALS_DIR)) {
console.log("No Web Vitals data found");
return;
}
const files = fs.readdirSync(WEB_VITALS_DIR);
const report = {
timestamp: new Date().toISOString(),
metrics: {},
};
files.forEach((file) => {
if (file.endsWith(".json")) {
const metric = file.replace(".json", "");
const data = JSON.parse(
fs.readFileSync(path.join(WEB_VITALS_DIR, file), "utf8"),
);
if (data.length > 0) {
const values = data
.map((d) => d.value)
.filter((v) => v !== undefined);
const ratings = data
.map((d) => d.rating)
.filter((r) => r !== undefined);
report.metrics[metric] = {
count: data.length,
average:
values.length > 0
? Math.round(values.reduce((a, b) => a + b, 0) / values.length)
: 0,
min: values.length > 0 ? Math.min(...values) : 0,
max: values.length > 0 ? Math.max(...values) : 0,
goodCount: ratings.filter((r) => r === "good").length,
needsImprovementCount: ratings.filter(
(r) => r === "needs-improvement",
).length,
poorCount: ratings.filter((r) => r === "poor").length,
};
}
}
});
const reportPath = path.join(WEB_VITALS_DIR, "report.json");
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("📊 Web Vitals report generated:", reportPath);
return report;
}
}
// Run if called directly
if (require.main === module) {
const tracker = new WebVitalsTracker();
tracker.generateReport();
}
module.exports = WebVitalsTracker;