217 lines
8.5 KiB
JavaScript
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}`);
|