blaze-performance-tester 3.2.30 → 3.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +362 -49
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
1
2
|
import { createRequire as __esbuild_createRequire } from 'module'; const require = __esbuild_createRequire(import.meta.url);
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -6669,10 +6670,146 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6669
6670
|
const waveListItems = history.map((h) => {
|
|
6670
6671
|
return `<div class="wave-item">Wave (${h.threads} VUs): P95 ${h.p95Latency.toFixed(1)}ms, Errors ${(h.errorRate * 100).toFixed(1)}%</div>`;
|
|
6671
6672
|
}).join("");
|
|
6673
|
+
const estimatedMonthlyCost = (cards.throughput * 0.045 * 24 * 30).toFixed(2);
|
|
6674
|
+
const recommendedCpuCores = Math.max(2, Math.ceil(cards.saturationKneePoint / 75));
|
|
6675
|
+
const recommendedRamGb = recommendedCpuCores * 2;
|
|
6676
|
+
const firstTimestamp = rawRequests[0]?.timestampMs || Date.now();
|
|
6672
6677
|
const scatterPoints = rawRequests.slice(0, 180).map((r) => ({
|
|
6673
|
-
x: Number(((r.timestampMs -
|
|
6678
|
+
x: Number(((r.timestampMs - firstTimestamp) / 1e3).toFixed(2)),
|
|
6674
6679
|
y: Number(((r.dnsTimeMs || 0) + (r.tcpTimeMs || 0) + (r.tlsTimeMs || 0) + r.durationMs * 0.3).toFixed(1))
|
|
6675
6680
|
}));
|
|
6681
|
+
let baselineComparisonHtml = "";
|
|
6682
|
+
if (baselineData) {
|
|
6683
|
+
const diffApdex = cards.apdex - baselineData.apdex;
|
|
6684
|
+
const diffThroughput = (cards.throughput - baselineData.throughput) / (baselineData.throughput || 1) * 100;
|
|
6685
|
+
const diffTtfb = (cards.ttfb - baselineData.ttfb) / (baselineData.ttfb || 1) * 100;
|
|
6686
|
+
const diffHealth = cards.healthScore - baselineData.healthScore;
|
|
6687
|
+
const formatDelta = (val, isInverse = false) => {
|
|
6688
|
+
const good = isInverse ? val <= 0 : val >= 0;
|
|
6689
|
+
const color = good ? "#10b981" : "#ef4444";
|
|
6690
|
+
const sign = val > 0 ? "+" : "";
|
|
6691
|
+
return `<span style="color: ${color}; font-weight: bold;">${sign}${val.toFixed(1)}%</span>`;
|
|
6692
|
+
};
|
|
6693
|
+
baselineComparisonHtml = `
|
|
6694
|
+
<div class="card" style="margin-top: 2rem; background: #131c2e; border: 1px solid #7c3aed;">
|
|
6695
|
+
<h3 style="color: #a78bfa; display: flex; align-items: center; gap: 0.6rem; font-size: 1.2rem; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem;">
|
|
6696
|
+
\u{1F4CA} Baseline Regression Comparison (Delta Engine)
|
|
6697
|
+
</h3>
|
|
6698
|
+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem;">
|
|
6699
|
+
<div style="background: #090d16; padding: 1rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
6700
|
+
<div class="card-title">Apdex Shift</div>
|
|
6701
|
+
<div style="font-size: 1.4rem; font-weight: bold; color: #ffffff;">${cards.apdex.toFixed(2)}</div>
|
|
6702
|
+
<div style="font-size: 0.8rem; margin-top: 0.2rem;">Delta: <span style="color: ${diffApdex >= 0 ? "#10b981" : "#ef4444"}; font-weight: bold;">${diffApdex >= 0 ? "+" : ""}${diffApdex.toFixed(2)}</span></div>
|
|
6703
|
+
</div>
|
|
6704
|
+
<div style="background: #090d16; padding: 1rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
6705
|
+
<div class="card-title">Throughput Delta</div>
|
|
6706
|
+
<div style="font-size: 1.4rem; font-weight: bold; color: #ffffff;">${cards.throughput.toFixed(0)}/s</div>
|
|
6707
|
+
<div style="font-size: 0.8rem; margin-top: 0.2rem;">Delta: ${formatDelta(diffThroughput, false)}</div>
|
|
6708
|
+
</div>
|
|
6709
|
+
<div style="background: #090d16; padding: 1rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
6710
|
+
<div class="card-title">TTFB Latency Shift</div>
|
|
6711
|
+
<div style="font-size: 1.4rem; font-weight: bold; color: #ffffff;">${cards.ttfb.toFixed(1)}ms</div>
|
|
6712
|
+
<div style="font-size: 0.8rem; margin-top: 0.2rem;">Delta: ${formatDelta(diffTtfb, true)}</div>
|
|
6713
|
+
</div>
|
|
6714
|
+
<div style="background: #090d16; padding: 1rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
6715
|
+
<div class="card-title">Health Score Diff</div>
|
|
6716
|
+
<div style="font-size: 1.4rem; font-weight: bold; color: #ffffff;">${cards.healthScore}/100</div>
|
|
6717
|
+
<div style="font-size: 0.8rem; margin-top: 0.2rem;">Delta: <span style="color: ${diffHealth >= 0 ? "#10b981" : "#ef4444"}; font-weight: bold;">${diffHealth >= 0 ? "+" : ""}${diffHealth} pts</span></div>
|
|
6718
|
+
</div>
|
|
6719
|
+
</div>
|
|
6720
|
+
</div>`;
|
|
6721
|
+
}
|
|
6722
|
+
const crossBrowserRows = getCrossBrowserMatrixData().map((m) => `
|
|
6723
|
+
<tr>
|
|
6724
|
+
<td><span class="engine-badge ${m.badgeClass}">${m.engine}</span></td>
|
|
6725
|
+
<td>${m.fcp}</td>
|
|
6726
|
+
<td>${m.lcp}</td>
|
|
6727
|
+
<td>${m.journeyDuration}</td>
|
|
6728
|
+
<td><span class="${m.statusClass}">${m.status}</span></td>
|
|
6729
|
+
</tr>
|
|
6730
|
+
`).join("");
|
|
6731
|
+
const crossBrowserMatrixSectionHtml = `
|
|
6732
|
+
<div class="card" style="margin-top: 2rem; background: #131c2e; border: 1px solid #38bdf8;">
|
|
6733
|
+
<h3 style="color: #38bdf8; display: flex; align-items: center; gap: 0.6rem; font-size: 1.2rem; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem;">
|
|
6734
|
+
\u{1F310} Cross-Browser Rendering Performance Matrix
|
|
6735
|
+
</h3>
|
|
6736
|
+
<table class="matrix-table" style="width: 100%; border-collapse: collapse; text-align: start; font-size: 14px;">
|
|
6737
|
+
<thead>
|
|
6738
|
+
<tr style="border-bottom: 1px solid #1e293b;">
|
|
6739
|
+
<th style="padding: 12px 14px; background: #090d16; color: #94a3b8; font-size: 12px; text-transform: uppercase;">Browser Engine</th>
|
|
6740
|
+
<th style="padding: 12px 14px; background: #090d16; color: #94a3b8; font-size: 12px; text-transform: uppercase;">FCP</th>
|
|
6741
|
+
<th style="padding: 12px 14px; background: #090d16; color: #94a3b8; font-size: 12px; text-transform: uppercase;">LCP</th>
|
|
6742
|
+
<th style="padding: 12px 14px; background: #090d16; color: #94a3b8; font-size: 12px; text-transform: uppercase;">Journey Duration</th>
|
|
6743
|
+
<th style="padding: 12px 14px; background: #090d16; color: #94a3b8; font-size: 12px; text-transform: uppercase;">Status</th>
|
|
6744
|
+
</tr>
|
|
6745
|
+
</thead>
|
|
6746
|
+
<tbody>
|
|
6747
|
+
${crossBrowserRows}
|
|
6748
|
+
</tbody>
|
|
6749
|
+
</table>
|
|
6750
|
+
</div>`;
|
|
6751
|
+
let logClustersHtml = "";
|
|
6752
|
+
if (semanticReport) {
|
|
6753
|
+
const totalLogCount = semanticReport.clusters.reduce((sum, c) => sum + c.count, 0);
|
|
6754
|
+
const uniquePatternsCount = semanticReport.clusters.length;
|
|
6755
|
+
logClustersHtml = `
|
|
6756
|
+
<div class="card" style="margin-top: 2rem; background: #131c2e; border: 1px solid #1e293b;">
|
|
6757
|
+
<h3 style="border-bottom: none; padding-bottom: 0rem; margin-bottom: 0.25rem; display: flex; align-items: center; gap: 0.5rem; color: #ffffff; font-size: 1.2rem;">
|
|
6758
|
+
\u{1F9E0} Semantic Log Clustering
|
|
6759
|
+
</h3>
|
|
6760
|
+
<div style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
6761
|
+
Captured <strong style="color: #f8fafc;">${totalLogCount}</strong> raw failures.
|
|
6762
|
+
</div>
|
|
6763
|
+
|
|
6764
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
6765
|
+
<thead>
|
|
6766
|
+
<tr style="border-bottom: 1px solid #1e293b;">
|
|
6767
|
+
<th style="width: 8%; padding: 0.75rem; text-align: start; color: #64748b; font-size: 0.85rem; font-weight: bold; text-transform: none;">Count</th>
|
|
6768
|
+
<th style="width: 22%; padding: 0.75rem; text-align: start; color: #64748b; font-size: 0.85rem; font-weight: bold; text-transform: none;">Category</th>
|
|
6769
|
+
<th style="width: 10%; padding: 0.75rem; text-align: start; color: #64748b; font-size: 0.85rem; font-weight: bold; text-transform: none;">Severity</th>
|
|
6770
|
+
<th style="width: 60%; padding: 0.75rem; text-align: start; color: #64748b; font-size: 0.85rem; font-weight: bold; text-transform: none;">Structural Blueprint</th>
|
|
6771
|
+
</tr>
|
|
6772
|
+
</thead>
|
|
6773
|
+
<tbody>
|
|
6774
|
+
${semanticReport.clusters.map((c) => {
|
|
6775
|
+
let catColor = "#fb923c";
|
|
6776
|
+
if (c.category === "Authentication_Error")
|
|
6777
|
+
catColor = "#c084fc";
|
|
6778
|
+
if (c.category === "Product_Error")
|
|
6779
|
+
catColor = "#f87171";
|
|
6780
|
+
let sevBg = c.severity === "CRITICAL" ? "#dc2626" : "#ea580c";
|
|
6781
|
+
const rawTemplate = c.template || "";
|
|
6782
|
+
const cleanedTemplate = rawTemplate.replace(/\[\d{4}-\d{2}-\d{2}.*?\]\s*/, "");
|
|
6783
|
+
return `
|
|
6784
|
+
<tr style="border-bottom: 1px solid #1e293b; vertical-align: top;">
|
|
6785
|
+
<td style="padding: 1.25rem 0.75rem; font-size: 1.2rem; font-weight: bold; color: #f8fafc;">${c.count}</td>
|
|
6786
|
+
<td style="padding: 1.25rem 0.75rem; color: ${catColor}; font-weight: 600; font-size: 0.95rem;">${c.category}</td>
|
|
6787
|
+
<td style="padding: 1.25rem 0.75rem;">
|
|
6788
|
+
<span style="background: ${sevBg}; color: #ffffff; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; font-weight: bold;">${c.severity}</span>
|
|
6789
|
+
</td>
|
|
6790
|
+
<td style="padding: 1.25rem 0.75rem;">
|
|
6791
|
+
<div style="background: #090d16; border-radius: 4px; padding: 0.6rem 0.8rem; border-inline-start: 3px solid #1e293b; margin-bottom: 0.5rem;">
|
|
6792
|
+
<code style="color: #cbd5e1; font-family: monospace; font-size: 0.9rem; white-space: pre-wrap; word-break: break-all;">${cleanedTemplate}</code>
|
|
6793
|
+
</div>
|
|
6794
|
+
</td>
|
|
6795
|
+
</tr>`;
|
|
6796
|
+
}).join("")}
|
|
6797
|
+
</tbody>
|
|
6798
|
+
</table>
|
|
6799
|
+
</div>`;
|
|
6800
|
+
}
|
|
6801
|
+
const breakdownRates = responseMatrix.breakdownRates || {};
|
|
6802
|
+
const breakdownGridHtml = [400, 401, 403, 404, 429, 500, 502, 503, 504].map((code) => {
|
|
6803
|
+
const rate = breakdownRates[code] || 0;
|
|
6804
|
+
const displayColor = rate > 0 ? code >= 500 ? "#f87171" : "#fde047" : "#64748b";
|
|
6805
|
+
return `
|
|
6806
|
+
<div style="background: #090d16; border: 1px solid #1e293b; border-radius: 4px; padding: 0.4rem 0.2rem; text-align: center;">
|
|
6807
|
+
<div style="font-size: 0.7rem; color: #64748b; font-weight: bold;">${dict.matrix.code} ${code}</div>
|
|
6808
|
+
<div style="font-size: 0.85rem; font-weight: bold; color: ${displayColor};">${rate.toFixed(2)}%</div>
|
|
6809
|
+
</div>
|
|
6810
|
+
`;
|
|
6811
|
+
}).join("");
|
|
6812
|
+
const gaugeColor = cards.healthScore >= 90 ? "#10b981" : cards.healthScore >= 70 ? "#f59e0b" : "#ef4444";
|
|
6676
6813
|
const htmlContent = `<!DOCTYPE html>
|
|
6677
6814
|
<html lang="${config.locale}" dir="${dir}">
|
|
6678
6815
|
<head>
|
|
@@ -6688,7 +6825,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6688
6825
|
|
|
6689
6826
|
.cards-wrapper { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
|
6690
6827
|
.metric-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 6px; padding: 1.25rem; position: relative; }
|
|
6691
|
-
.card-title { font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem;
|
|
6828
|
+
.card-title { font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
6692
6829
|
.card-value { font-size: 1.6rem; font-weight: 700; color: #ffffff; }
|
|
6693
6830
|
.card-value .unit { font-size: 0.9rem; font-weight: 500; color: #94a3b8; margin-inline-start: 0.2rem; }
|
|
6694
6831
|
.card-value.green { color: #10b981; }
|
|
@@ -6696,11 +6833,6 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6696
6833
|
.card-value.purple { color: #a855f7; }
|
|
6697
6834
|
.card-subtext { font-size: 0.8rem; color: #10b981; font-weight: 500; margin-inline-start: 0.4rem; display: inline-block; }
|
|
6698
6835
|
|
|
6699
|
-
/* Export Styles */
|
|
6700
|
-
.export-menu { position: absolute; top: 0.5rem; right: 0.5rem; }
|
|
6701
|
-
.export-btn { background: #1e293b; border: 1px solid #334155; color: #94a3b8; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; cursor: pointer; }
|
|
6702
|
-
.export-btn:hover { background: #334155; }
|
|
6703
|
-
|
|
6704
6836
|
.top-layout-grid { display: grid; grid-template-columns: 1fr 280px 380px; gap: 1.5rem; margin-bottom: 2rem; }
|
|
6705
6837
|
.verdict-box { background: #0f172a; border: 1px dashed #ea580c; border-radius: 8px; padding: 1.25rem; }
|
|
6706
6838
|
.verdict-title { text-transform: uppercase; font-size: 0.85rem; font-weight: 700; color: #f97316; margin-bottom: 0.5rem; }
|
|
@@ -6712,7 +6844,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6712
6844
|
.gauge-title { font-size: 0.85rem; font-weight: bold; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1rem; }
|
|
6713
6845
|
|
|
6714
6846
|
.matrix-box { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.25rem; }
|
|
6715
|
-
.matrix-title { font-size: 1.1rem; font-weight: bold; color: #ffffff; margin-bottom: 1rem;
|
|
6847
|
+
.matrix-title { font-size: 1.1rem; font-weight: bold; color: #ffffff; margin-bottom: 1rem; }
|
|
6716
6848
|
.matrix-box .matrix-row { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #1e293b; }
|
|
6717
6849
|
.matrix-box .matrix-row:last-child { border-bottom: none; }
|
|
6718
6850
|
.matrix-label { font-size: 0.9rem; color: #f8fafc; display: flex; align-items: center; gap: 0.5rem; }
|
|
@@ -6732,11 +6864,11 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6732
6864
|
.status-warning { color: #ca8a04; font-weight: 600; }
|
|
6733
6865
|
|
|
6734
6866
|
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; margin-bottom: 2rem; }
|
|
6735
|
-
.graph-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem;
|
|
6736
|
-
.graph-title { font-size: 1.1rem; font-weight: 700; color: #ffffff; margin-bottom: 1.2rem; display: flex; align-items: center;
|
|
6867
|
+
.graph-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem; }
|
|
6868
|
+
.graph-title { font-size: 1.1rem; font-weight: 700; color: #ffffff; margin-bottom: 1.2rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
6737
6869
|
|
|
6738
|
-
.card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem;
|
|
6739
|
-
h3 { margin-top: 0; color: #cbd5e1; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem;
|
|
6870
|
+
.card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem; }
|
|
6871
|
+
h3 { margin-top: 0; color: #cbd5e1; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem; }
|
|
6740
6872
|
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
6741
6873
|
th, td { padding: 0.75rem; text-align: start; border-bottom: 1px solid #1e293b; }
|
|
6742
6874
|
th { color: #94a3b8; font-weight: 600; }
|
|
@@ -6767,8 +6899,27 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6767
6899
|
<div class="card-title">${dict.metrics.ttfb}</div>
|
|
6768
6900
|
<div class="card-value">${cards.ttfb.toFixed(1)}<span class="unit">ms</span></div>
|
|
6769
6901
|
</div>
|
|
6902
|
+
<div class="metric-card">
|
|
6903
|
+
<div class="card-title">${dict.metrics.dns}</div>
|
|
6904
|
+
<div class="card-value">${cards.dns.toFixed(2)}<span class="unit">ms</span></div>
|
|
6905
|
+
</div>
|
|
6906
|
+
<div class="metric-card">
|
|
6907
|
+
<div class="card-title">${dict.metrics.tcp}</div>
|
|
6908
|
+
<div class="card-value">${cards.tcp.toFixed(2)}<span class="unit">ms</span></div>
|
|
6909
|
+
</div>
|
|
6910
|
+
<div class="metric-card">
|
|
6911
|
+
<div class="card-title">${dict.metrics.tls}</div>
|
|
6912
|
+
<div class="card-value">${cards.tls.toFixed(2)}<span class="unit">ms</span></div>
|
|
6913
|
+
</div>
|
|
6914
|
+
<div class="metric-card">
|
|
6915
|
+
<div class="card-title">${dict.metrics.stability}</div>
|
|
6916
|
+
<div class="card-value">±${cards.stdDev.toFixed(1)}<span class="unit">ms</span></div>
|
|
6917
|
+
</div>
|
|
6770
6918
|
</div>
|
|
6771
6919
|
|
|
6920
|
+
${baselineComparisonHtml}
|
|
6921
|
+
${crossBrowserMatrixSectionHtml}
|
|
6922
|
+
|
|
6772
6923
|
<div class="top-layout-grid" style="margin-top: 2rem;">
|
|
6773
6924
|
<div class="verdict-box">
|
|
6774
6925
|
<div class="verdict-title">${dict.verdict}</div>
|
|
@@ -6781,37 +6932,101 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6781
6932
|
<div style="position: relative; width: 140px; height: 140px; display: flex; align-items: center; justify-content: center;">
|
|
6782
6933
|
<svg width="140" height="140" viewBox="0 0 140 140" style="transform: rotate(-90deg);">
|
|
6783
6934
|
<circle cx="70" cy="70" r="58" stroke="#1e293b" stroke-width="12" fill="transparent"/>
|
|
6784
|
-
<circle cx="70" cy="70" r="58" stroke="
|
|
6935
|
+
<circle cx="70" cy="70" r="58" stroke="${gaugeColor}" stroke-width="12" fill="transparent"
|
|
6785
6936
|
stroke-dasharray="364.4" stroke-dashoffset="${364.4 - 364.4 * cards.healthScore / 100}"
|
|
6786
6937
|
stroke-linecap="round" style="transition: stroke-dashoffset 1s ease-out;"/>
|
|
6787
6938
|
</svg>
|
|
6788
6939
|
<div style="position: absolute; text-align: center;">
|
|
6789
6940
|
<div style="font-size: 2.2rem; font-weight: 800; color: #ffffff; line-height: 1;">${cards.healthScore}</div>
|
|
6941
|
+
<div style="font-size: 0.75rem; color: #64748b; font-weight: bold; margin-top: 0.2rem; text-transform: uppercase;">/ 100</div>
|
|
6790
6942
|
</div>
|
|
6791
6943
|
</div>
|
|
6944
|
+
<div style="margin-top: 0.75rem; font-size: 0.8rem; color: #94a3b8; font-weight: 500;">
|
|
6945
|
+
${dict.health.subtitle}
|
|
6946
|
+
</div>
|
|
6792
6947
|
</div>
|
|
6793
6948
|
|
|
6794
6949
|
<div class="matrix-box">
|
|
6795
|
-
<div class="matrix-title">${dict.matrix.title}
|
|
6950
|
+
<div class="matrix-title">${dict.matrix.title}</div>
|
|
6796
6951
|
<div class="matrix-row">
|
|
6797
|
-
<div class="matrix-label">${dict.matrix.info}</div>
|
|
6952
|
+
<div class="matrix-label">${dict.matrix.info} <span class="matrix-badge badge-1xx">1xx</span></div>
|
|
6798
6953
|
<div class="matrix-value">${responseMatrix.total1xx}</div>
|
|
6799
6954
|
</div>
|
|
6800
6955
|
<div class="matrix-row">
|
|
6801
|
-
<div class="matrix-label">${dict.matrix.success}</div>
|
|
6956
|
+
<div class="matrix-label">${dict.matrix.success} <span class="matrix-badge badge-2xx">2xx</span></div>
|
|
6802
6957
|
<div class="matrix-value">${responseMatrix.total2xx}</div>
|
|
6803
6958
|
</div>
|
|
6959
|
+
<div class="matrix-row">
|
|
6960
|
+
<div class="matrix-label">${dict.matrix.redirects} <span class="matrix-badge badge-3xx">3xx</span></div>
|
|
6961
|
+
<div class="matrix-value">${responseMatrix.total3xx}</div>
|
|
6962
|
+
</div>
|
|
6963
|
+
<div class="matrix-row">
|
|
6964
|
+
<div class="matrix-label">${dict.matrix.clientErr} <span class="matrix-badge badge-4xx">4xx</span></div>
|
|
6965
|
+
<div class="matrix-value">${responseMatrix.total4xx}</div>
|
|
6966
|
+
</div>
|
|
6967
|
+
<div class="matrix-row">
|
|
6968
|
+
<div class="matrix-label">${dict.matrix.serverErr} <span class="matrix-badge badge-5xx">5xx</span></div>
|
|
6969
|
+
<div class="matrix-value">${responseMatrix.total5xx}</div>
|
|
6970
|
+
</div>
|
|
6971
|
+
|
|
6972
|
+
<div class="matrix-title" style="margin-top: 1.5rem; font-size: 0.85rem; border-top: 1px solid #1e293b; padding-top: 1rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">
|
|
6973
|
+
${dict.matrix.breakdown}
|
|
6974
|
+
</div>
|
|
6975
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin-top: 0.5rem;">
|
|
6976
|
+
${breakdownGridHtml}
|
|
6977
|
+
</div>
|
|
6804
6978
|
</div>
|
|
6805
6979
|
</div>
|
|
6806
6980
|
|
|
6981
|
+
<div class="card" style="margin-top: 2rem; background: #0c172b; border: 1px solid #7c3aed;">
|
|
6982
|
+
<h3 style="color: #a78bfa; display: flex; align-items: center; gap: 0.6rem; font-size: 1.2rem; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem;">
|
|
6983
|
+
${dict.rca}
|
|
6984
|
+
</h3>
|
|
6985
|
+
<div style="font-size: 0.95rem; line-height: 1.6; color: #cbd5e1; padding: 0.5rem 0; white-space: pre-wrap; word-break: break-word;">
|
|
6986
|
+
${geminiAnalysisHtml}
|
|
6987
|
+
</div>
|
|
6988
|
+
</div>
|
|
6989
|
+
|
|
6990
|
+
<div class="charts-grid" style="margin-top: 2rem;">
|
|
6991
|
+
<div class="graph-card">
|
|
6992
|
+
<div class="graph-title">${dict.charts.volume}</div>
|
|
6993
|
+
<div style="height: 280px; position: relative;">
|
|
6994
|
+
<canvas id="volumeChart"></canvas>
|
|
6995
|
+
</div>
|
|
6996
|
+
</div>
|
|
6997
|
+
|
|
6998
|
+
<div class="graph-card">
|
|
6999
|
+
<div class="graph-title">${dict.charts.concurrency}</div>
|
|
7000
|
+
<div style="height: 280px; position: relative;">
|
|
7001
|
+
<canvas id="concurrencyChart"></canvas>
|
|
7002
|
+
</div>
|
|
7003
|
+
</div>
|
|
7004
|
+
|
|
7005
|
+
<div class="graph-card">
|
|
7006
|
+
<div class="graph-title">${dict.charts.status}</div>
|
|
7007
|
+
<div style="height: 280px; position: relative;">
|
|
7008
|
+
<canvas id="statusDonutChart"></canvas>
|
|
7009
|
+
</div>
|
|
7010
|
+
</div>
|
|
7011
|
+
|
|
7012
|
+
<div class="graph-card">
|
|
7013
|
+
<div class="graph-title">${dict.charts.scatter}</div>
|
|
7014
|
+
<div style="height: 280px; position: relative;">
|
|
7015
|
+
<canvas id="ttfbScatterChart"></canvas>
|
|
7016
|
+
</div>
|
|
7017
|
+
</div>
|
|
7018
|
+
</div>
|
|
7019
|
+
|
|
6807
7020
|
<div class="card" style="margin-top: 2rem;">
|
|
6808
|
-
<h3>${dict.telemetry.title}
|
|
7021
|
+
<h3>${dict.telemetry.title}</h3>
|
|
6809
7022
|
<table>
|
|
6810
7023
|
<thead>
|
|
6811
7024
|
<tr>
|
|
6812
7025
|
<th>${dict.telemetry.threads}</th>
|
|
6813
7026
|
<th>${dict.telemetry.throughput}</th>
|
|
6814
7027
|
<th>${dict.telemetry.avgLatency}</th>
|
|
7028
|
+
<th>${dict.telemetry.p95}</th>
|
|
7029
|
+
<th>${dict.telemetry.errors}</th>
|
|
6815
7030
|
<th>${dict.telemetry.status}</th>
|
|
6816
7031
|
</tr>
|
|
6817
7032
|
</thead>
|
|
@@ -6819,51 +7034,149 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6819
7034
|
${history.map((h) => {
|
|
6820
7035
|
const isPassed = h.apdex >= config.targetApdex && h.errorRate <= config.targetErrorRate;
|
|
6821
7036
|
return `<tr>
|
|
6822
|
-
<td>${h.threads}</td>
|
|
7037
|
+
<td><strong>${h.threads}</strong></td>
|
|
6823
7038
|
<td>${h.throughput.toFixed(0)} /s</td>
|
|
6824
7039
|
<td>${h.avgLatency.toFixed(1)} ms</td>
|
|
7040
|
+
<td>${h.p95Latency.toFixed(1)} ms</td>
|
|
7041
|
+
<td>${(h.errorRate * 100).toFixed(2)}%</td>
|
|
6825
7042
|
<td><span class="badge ${isPassed ? "badge-success" : "badge-fail"}">${isPassed ? dict.telemetry.pass : dict.telemetry.fail}</span></td>
|
|
6826
7043
|
</tr>`;
|
|
6827
7044
|
}).join("")}
|
|
6828
7045
|
</tbody>
|
|
6829
7046
|
</table>
|
|
6830
7047
|
</div>
|
|
7048
|
+
${logClustersHtml}
|
|
6831
7049
|
</div>
|
|
6832
|
-
|
|
7050
|
+
|
|
6833
7051
|
<script>
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7052
|
+
const labels = ${JSON.stringify(labels)};
|
|
7053
|
+
const successRequestsData = ${JSON.stringify(successRequestsData)};
|
|
7054
|
+
const failedWorkloadsData = ${JSON.stringify(failedWorkloadsData)};
|
|
7055
|
+
|
|
7056
|
+
new Chart(document.getElementById('volumeChart'), {
|
|
7057
|
+
type: 'bar',
|
|
7058
|
+
data: {
|
|
7059
|
+
labels: labels,
|
|
7060
|
+
datasets: [
|
|
7061
|
+
{
|
|
7062
|
+
label: '${dict.matrix.success}',
|
|
7063
|
+
data: successRequestsData,
|
|
7064
|
+
backgroundColor: '#10b981',
|
|
7065
|
+
stack: 'requests',
|
|
7066
|
+
barPercentage: 0.95,
|
|
7067
|
+
categoryPercentage: 0.95
|
|
7068
|
+
},
|
|
7069
|
+
{
|
|
7070
|
+
label: '${dict.matrix.serverErr}',
|
|
7071
|
+
data: failedWorkloadsData,
|
|
7072
|
+
backgroundColor: '#ef4444',
|
|
7073
|
+
stack: 'requests',
|
|
7074
|
+
barPercentage: 0.95,
|
|
7075
|
+
categoryPercentage: 0.95
|
|
7076
|
+
}
|
|
7077
|
+
]
|
|
7078
|
+
},
|
|
7079
|
+
options: {
|
|
7080
|
+
responsive: true,
|
|
7081
|
+
maintainAspectRatio: false,
|
|
7082
|
+
plugins: {
|
|
7083
|
+
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 } } }
|
|
7084
|
+
},
|
|
7085
|
+
scales: {
|
|
7086
|
+
x: { grid: { color: '#1e293b' }, ticks: { color: '#64748b' } },
|
|
7087
|
+
y: { stacked: true, grid: { color: '#1e293b' }, ticks: { color: '#64748b' } }
|
|
6850
7088
|
}
|
|
6851
|
-
|
|
6852
|
-
}
|
|
7089
|
+
}
|
|
7090
|
+
});
|
|
6853
7091
|
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
}
|
|
7092
|
+
const baseActiveThreads = ${JSON.stringify(activeThreadsData)};
|
|
7093
|
+
const baseTtfTrend = ${JSON.stringify(ttfTrendData)};
|
|
6857
7094
|
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
7095
|
+
new Chart(document.getElementById('concurrencyChart'), {
|
|
7096
|
+
type: 'line',
|
|
7097
|
+
data: {
|
|
7098
|
+
labels: labels,
|
|
7099
|
+
datasets: [
|
|
7100
|
+
{
|
|
7101
|
+
label: 'VUs',
|
|
7102
|
+
data: baseActiveThreads,
|
|
7103
|
+
borderColor: '#38bdf8',
|
|
7104
|
+
backgroundColor: 'rgba(56, 189, 248, 0.1)',
|
|
7105
|
+
borderWidth: 2.5,
|
|
7106
|
+
pointRadius: 4,
|
|
7107
|
+
fill: true,
|
|
7108
|
+
yAxisID: 'yThreads'
|
|
7109
|
+
},
|
|
7110
|
+
{
|
|
7111
|
+
label: 'TTF',
|
|
7112
|
+
data: baseTtfTrend,
|
|
7113
|
+
borderColor: '#f43f5e',
|
|
7114
|
+
borderWidth: 2,
|
|
7115
|
+
borderDash: [5, 5],
|
|
7116
|
+
pointRadius: 3,
|
|
7117
|
+
fill: false,
|
|
7118
|
+
yAxisID: 'yTtf'
|
|
7119
|
+
}
|
|
7120
|
+
]
|
|
7121
|
+
},
|
|
7122
|
+
options: {
|
|
7123
|
+
responsive: true,
|
|
7124
|
+
maintainAspectRatio: false,
|
|
7125
|
+
plugins: {
|
|
7126
|
+
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 } } }
|
|
7127
|
+
},
|
|
7128
|
+
scales: {
|
|
7129
|
+
x: { grid: { color: '#1e293b' }, ticks: { color: '#64748b' } },
|
|
7130
|
+
yThreads: { position: 'left', grid: { color: '#1e293b' }, ticks: { color: '#38bdf8' } },
|
|
7131
|
+
yTtf: { position: 'right', grid: { drawOnChartArea: false }, ticks: { color: '#f43f5e' } }
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
});
|
|
7135
|
+
|
|
7136
|
+
new Chart(document.getElementById('statusDonutChart'), {
|
|
7137
|
+
type: 'doughnut',
|
|
7138
|
+
data: {
|
|
7139
|
+
labels: ['1xx', '2xx', '3xx', '4xx', '5xx'],
|
|
7140
|
+
datasets: [{
|
|
7141
|
+
data: [${responseMatrix.total1xx}, ${responseMatrix.total2xx}, ${responseMatrix.total3xx}, ${responseMatrix.total4xx}, ${responseMatrix.total5xx}],
|
|
7142
|
+
backgroundColor: ['#38bdf8', '#4ade80', '#cbd5e1', '#fde047', '#f87171'],
|
|
7143
|
+
borderWidth: 1,
|
|
7144
|
+
borderColor: '#1e293b'
|
|
7145
|
+
}]
|
|
7146
|
+
},
|
|
7147
|
+
options: {
|
|
7148
|
+
responsive: true,
|
|
7149
|
+
maintainAspectRatio: false,
|
|
7150
|
+
plugins: {
|
|
7151
|
+
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 } } },
|
|
7152
|
+
cutout: '65%'
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
});
|
|
7156
|
+
|
|
7157
|
+
new Chart(document.getElementById('ttfbScatterChart'), {
|
|
7158
|
+
type: 'scatter',
|
|
7159
|
+
data: {
|
|
7160
|
+
datasets: [{
|
|
7161
|
+
label: 'Delay',
|
|
7162
|
+
data: ${JSON.stringify(scatterPoints)},
|
|
7163
|
+
backgroundColor: '#a855f7',
|
|
7164
|
+
pointRadius: 4,
|
|
7165
|
+
pointHoverRadius: 6
|
|
7166
|
+
}]
|
|
7167
|
+
},
|
|
7168
|
+
options: {
|
|
7169
|
+
responsive: true,
|
|
7170
|
+
maintainAspectRatio: false,
|
|
7171
|
+
plugins: {
|
|
7172
|
+
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 } } }
|
|
7173
|
+
},
|
|
7174
|
+
scales: {
|
|
7175
|
+
x: { grid: { color: '#1e293b' }, ticks: { color: '#64748b' } },
|
|
7176
|
+
y: { grid: { color: '#1e293b' }, ticks: { color: '#64748b' } }
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
});
|
|
6867
7180
|
</script></body></html>`;
|
|
6868
7181
|
try {
|
|
6869
7182
|
fs.writeFileSync(config.outputHtml, htmlContent, "utf-8");
|