217 lines
8.5 KiB
JavaScript

/**
* WalkGuide — k6 HTML Report Generator (Node.js)
* Mengkonversi parsed-report.json menjadi HTML report yang rapi untuk submission.
*
* Usage:
* node html-reporter.js <parsed-report.json> [output.html]
*
* Output: HTML report siap lampir ke laporan final exam.
*/
const fs = require("fs");
const path = require("path");
const inputFile = process.argv[2];
const outputFile = process.argv[3] || "k6-results/load-test-report.html";
if (!inputFile || !fs.existsSync(inputFile)) {
console.error(
"Usage: node html-reporter.js <parsed-report.json> [output.html]",
);
process.exit(1);
}
const report = JSON.parse(fs.readFileSync(inputFile, "utf8"));
// ── HTML Template ─────────────────────────────────────────────────────────────
function badge(result) {
if (!result) return '<span class="badge skip">SKIP</span>';
if (result.includes("PASS")) return '<span class="badge pass">✅ PASS</span>';
if (result.includes("FAIL")) return '<span class="badge fail">❌ FAIL</span>';
return `<span class="badge skip">${result}</span>`;
}
function metricRow(label, val) {
return `<tr><td>${label}</td><td><strong>${val || "N/A"}</strong></td></tr>`;
}
function endpointTableRows() {
const rows = [];
for (const [ep, metrics] of Object.entries(report.endpoints || {})) {
const dur = metrics["http_req_duration"];
if (!dur) continue;
rows.push(`
<tr>
<td><code>${ep}</code></td>
<td>${dur.count || "—"}</td>
<td>${dur.avg || "—"}</td>
<td>${dur.p95 || "—"}</td>
<td>${dur.p99 || "—"}</td>
<td>${dur.max || "—"}</td>
</tr>
`);
}
return rows.join("");
}
function thresholdRows() {
return (report.thresholdResults || [])
.map(
(t) => `
<tr>
<td>${t.name}</td>
<td><code>${t.metric}</code></td>
<td>${t.threshold}</td>
<td>${t.actual}</td>
<td>${badge(t.result)}</td>
</tr>
`,
)
.join("");
}
function wgMetricCards() {
const wg = report.walkguideMetrics || {};
const cards = [
{ label: "🔐 Auth", key: "authLatency", threshold: "< 800ms" },
{ label: "📍 Location", key: "locationLatency", threshold: "< 300ms" },
{ label: "🚧 Obstacle", key: "obstacleLatency", threshold: "< 400ms" },
{ label: "🚨 SOS", key: "sosLatency", threshold: "< 200ms" },
{ label: "📬 Notif", key: "notifLatency", threshold: "< 500ms" },
{ label: "📊 Timeline", key: "timelineLatency", threshold: "< 1000ms" },
{ label: "🔗 Pairing", key: "pairingLatency", threshold: "< 600ms" },
];
return cards
.map((c) => {
const data = wg[c.key];
return `
<div class="metric-card">
<div class="metric-label">${c.label}</div>
<div class="metric-p95">${data ? data.p95 : "N/A"}</div>
<div class="metric-sub">p95 | threshold ${c.threshold}</div>
<div class="metric-avg">avg: ${data ? data.avg : "N/A"} | p99: ${data ? data.p99 : "N/A"}</div>
</div>
`;
})
.join("");
}
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>WalkGuide — k6 Load Test Report</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
h1 { font-size: 2rem; color: #38bdf8; margin-bottom: 4px; }
h2 { font-size: 1.2rem; color: #7dd3fc; margin: 32px 0 12px; border-bottom: 1px solid #1e3a5f; padding-bottom: 6px; }
p { color: #94a3b8; margin-bottom: 8px; }
.header { margin-bottom: 32px; }
.header .meta { color: #64748b; font-size: 0.85rem; margin-top: 4px; }
/* Key Metrics Grid */
.key-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
.key-card { background: #1e293b; border-radius: 10px; padding: 18px; border: 1px solid #334155; }
.key-card .klabel { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
.key-card .kvalue { font-size: 1.5rem; font-weight: 700; color: #f0f9ff; margin: 6px 0 2px; }
.key-card .kdesc { font-size: 0.72rem; color: #475569; }
/* WalkGuide Metric Cards */
.wg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.metric-card { background: #1e293b; border-radius: 8px; padding: 14px; border: 1px solid #1e3a5f; text-align: center; }
.metric-label { font-size: 0.85rem; color: #94a3b8; margin-bottom: 6px; }
.metric-p95 { font-size: 1.6rem; font-weight: 700; color: #38bdf8; }
.metric-sub { font-size: 0.7rem; color: #475569; margin-top: 2px; }
.metric-avg { font-size: 0.72rem; color: #64748b; margin-top: 4px; }
/* Tables */
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; font-size: 0.88rem; }
th { background: #1e3a5f; color: #7dd3fc; text-align: left; padding: 10px 12px; }
td { padding: 9px 12px; border-bottom: 1px solid #1e293b; color: #cbd5e1; }
tr:nth-child(even) td { background: #0f1f35; }
code { background: #1e293b; padding: 2px 6px; border-radius: 4px; font-size: 0.82rem; color: #38bdf8; }
/* Badges */
.badge { padding: 3px 10px; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
.badge.pass { background: #064e3b; color: #34d399; }
.badge.fail { background: #450a0a; color: #f87171; }
.badge.skip { background: #1e293b; color: #94a3b8; }
footer { margin-top: 40px; text-align: center; color: #334155; font-size: 0.78rem; }
</style>
</head>
<body>
<div class="header">
<h1>🦺 WalkGuide — k6 Load Test Report</h1>
<p>Spring Boot Backend Performance Benchmark — Pillar 3</p>
<div class="meta">
Generated: ${report.generatedAt} |
Input: ${path.basename(report.inputFile || "")} |
Total Data Points: ${report.totalPoints}
</div>
</div>
<h2>📊 Key Metrics (Pillar 3 — 5 Required)</h2>
<div class="key-grid">
<div class="key-card">
<div class="klabel">1. Throughput</div>
<div class="kvalue">${report.keyMetrics.throughput.value || "N/A"}</div>
<div class="kdesc">Requests per second</div>
</div>
<div class="key-card">
<div class="klabel">2. p95 Latency</div>
<div class="kvalue">${report.keyMetrics.p95Latency.value || "N/A"}</div>
<div class="kdesc">95th percentile response time</div>
</div>
<div class="key-card">
<div class="klabel">3. Error Rate</div>
<div class="kvalue">${report.keyMetrics.errorRate.value || "N/A"}</div>
<div class="kdesc">Non-2xx responses | ${report.keyMetrics.errorRate.passFail || "—"}</div>
</div>
<div class="key-card">
<div class="klabel">4. DB Query Time</div>
<div class="kvalue">${report.keyMetrics.dbQueryTime.value || "N/A"}</div>
<div class="kdesc">Estimated via write endpoint p95</div>
</div>
<div class="key-card">
<div class="klabel">5. JVM Heap</div>
<div class="kvalue">${report.keyMetrics.jvmHeap.value || "N/A"}</div>
<div class="kdesc">Spring Actuator jvm.memory.used</div>
</div>
</div>
<h2>🏃 WalkGuide Endpoint Latency Breakdown</h2>
<div class="wg-grid">${wgMetricCards()}</div>
<h2>✅ Threshold Evaluation</h2>
<table>
<thead>
<tr><th>Check</th><th>Metric</th><th>Threshold</th><th>Actual</th><th>Result</th></tr>
</thead>
<tbody>${thresholdRows()}</tbody>
</table>
<h2>📋 Per-Endpoint Latency (ms)</h2>
<table>
<thead>
<tr><th>Endpoint</th><th>Requests</th><th>Avg</th><th>p95</th><th>p99</th><th>Max</th></tr>
</thead>
<tbody>${endpointTableRows()}</tbody>
</table>
<footer>
WalkGuide AI — Integrated Mobile Application Project Final Exam |
Generated by html-reporter.js
</footer>
</body>
</html>`;
// ── Write ─────────────────────────────────────────────────────────────────────
const outDir = path.dirname(outputFile);
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(outputFile, html);
console.log(`✅ HTML report written to: ${outputFile}`);