blaze-performance-tester 3.2.27 → 3.2.28
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 +49 -362
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env ts-node
|
|
2
1
|
import { createRequire as __esbuild_createRequire } from 'module'; const require = __esbuild_createRequire(import.meta.url);
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -6670,146 +6669,10 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6670
6669
|
const waveListItems = history.map((h) => {
|
|
6671
6670
|
return `<div class="wave-item">Wave (${h.threads} VUs): P95 ${h.p95Latency.toFixed(1)}ms, Errors ${(h.errorRate * 100).toFixed(1)}%</div>`;
|
|
6672
6671
|
}).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();
|
|
6677
6672
|
const scatterPoints = rawRequests.slice(0, 180).map((r) => ({
|
|
6678
|
-
x: Number(((r.timestampMs -
|
|
6673
|
+
x: Number(((r.timestampMs - (rawRequests[0]?.timestampMs || Date.now())) / 1e3).toFixed(2)),
|
|
6679
6674
|
y: Number(((r.dnsTimeMs || 0) + (r.tcpTimeMs || 0) + (r.tlsTimeMs || 0) + r.durationMs * 0.3).toFixed(1))
|
|
6680
6675
|
}));
|
|
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";
|
|
6813
6676
|
const htmlContent = `<!DOCTYPE html>
|
|
6814
6677
|
<html lang="${config.locale}" dir="${dir}">
|
|
6815
6678
|
<head>
|
|
@@ -6825,7 +6688,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6825
6688
|
|
|
6826
6689
|
.cards-wrapper { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
|
6827
6690
|
.metric-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 6px; padding: 1.25rem; position: relative; }
|
|
6828
|
-
.card-title { font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
6691
|
+
.card-title { font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; display: flex; justify-content: space-between; }
|
|
6829
6692
|
.card-value { font-size: 1.6rem; font-weight: 700; color: #ffffff; }
|
|
6830
6693
|
.card-value .unit { font-size: 0.9rem; font-weight: 500; color: #94a3b8; margin-inline-start: 0.2rem; }
|
|
6831
6694
|
.card-value.green { color: #10b981; }
|
|
@@ -6833,6 +6696,11 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6833
6696
|
.card-value.purple { color: #a855f7; }
|
|
6834
6697
|
.card-subtext { font-size: 0.8rem; color: #10b981; font-weight: 500; margin-inline-start: 0.4rem; display: inline-block; }
|
|
6835
6698
|
|
|
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
|
+
|
|
6836
6704
|
.top-layout-grid { display: grid; grid-template-columns: 1fr 280px 380px; gap: 1.5rem; margin-bottom: 2rem; }
|
|
6837
6705
|
.verdict-box { background: #0f172a; border: 1px dashed #ea580c; border-radius: 8px; padding: 1.25rem; }
|
|
6838
6706
|
.verdict-title { text-transform: uppercase; font-size: 0.85rem; font-weight: 700; color: #f97316; margin-bottom: 0.5rem; }
|
|
@@ -6844,7 +6712,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6844
6712
|
.gauge-title { font-size: 0.85rem; font-weight: bold; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1rem; }
|
|
6845
6713
|
|
|
6846
6714
|
.matrix-box { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.25rem; }
|
|
6847
|
-
.matrix-title { font-size: 1.1rem; font-weight: bold; color: #ffffff; margin-bottom: 1rem; }
|
|
6715
|
+
.matrix-title { font-size: 1.1rem; font-weight: bold; color: #ffffff; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; }
|
|
6848
6716
|
.matrix-box .matrix-row { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #1e293b; }
|
|
6849
6717
|
.matrix-box .matrix-row:last-child { border-bottom: none; }
|
|
6850
6718
|
.matrix-label { font-size: 0.9rem; color: #f8fafc; display: flex; align-items: center; gap: 0.5rem; }
|
|
@@ -6864,11 +6732,11 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6864
6732
|
.status-warning { color: #ca8a04; font-weight: 600; }
|
|
6865
6733
|
|
|
6866
6734
|
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; margin-bottom: 2rem; }
|
|
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;
|
|
6735
|
+
.graph-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem; position: relative; }
|
|
6736
|
+
.graph-title { font-size: 1.1rem; font-weight: 700; color: #ffffff; margin-bottom: 1.2rem; display: flex; align-items: center; justify-content: space-between; }
|
|
6869
6737
|
|
|
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; }
|
|
6738
|
+
.card { background: #131c2e; border: 1px solid #1e293b; border-radius: 8px; padding: 1.5rem; position: relative; }
|
|
6739
|
+
h3 { margin-top: 0; color: #cbd5e1; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem; display: flex; justify-content: space-between; }
|
|
6872
6740
|
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
6873
6741
|
th, td { padding: 0.75rem; text-align: start; border-bottom: 1px solid #1e293b; }
|
|
6874
6742
|
th { color: #94a3b8; font-weight: 600; }
|
|
@@ -6899,27 +6767,8 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6899
6767
|
<div class="card-title">${dict.metrics.ttfb}</div>
|
|
6900
6768
|
<div class="card-value">${cards.ttfb.toFixed(1)}<span class="unit">ms</span></div>
|
|
6901
6769
|
</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>
|
|
6918
6770
|
</div>
|
|
6919
6771
|
|
|
6920
|
-
${baselineComparisonHtml}
|
|
6921
|
-
${crossBrowserMatrixSectionHtml}
|
|
6922
|
-
|
|
6923
6772
|
<div class="top-layout-grid" style="margin-top: 2rem;">
|
|
6924
6773
|
<div class="verdict-box">
|
|
6925
6774
|
<div class="verdict-title">${dict.verdict}</div>
|
|
@@ -6932,101 +6781,37 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
6932
6781
|
<div style="position: relative; width: 140px; height: 140px; display: flex; align-items: center; justify-content: center;">
|
|
6933
6782
|
<svg width="140" height="140" viewBox="0 0 140 140" style="transform: rotate(-90deg);">
|
|
6934
6783
|
<circle cx="70" cy="70" r="58" stroke="#1e293b" stroke-width="12" fill="transparent"/>
|
|
6935
|
-
<circle cx="70" cy="70" r="58" stroke="
|
|
6784
|
+
<circle cx="70" cy="70" r="58" stroke="#10b981" stroke-width="12" fill="transparent"
|
|
6936
6785
|
stroke-dasharray="364.4" stroke-dashoffset="${364.4 - 364.4 * cards.healthScore / 100}"
|
|
6937
6786
|
stroke-linecap="round" style="transition: stroke-dashoffset 1s ease-out;"/>
|
|
6938
6787
|
</svg>
|
|
6939
6788
|
<div style="position: absolute; text-align: center;">
|
|
6940
6789
|
<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>
|
|
6942
6790
|
</div>
|
|
6943
6791
|
</div>
|
|
6944
|
-
<div style="margin-top: 0.75rem; font-size: 0.8rem; color: #94a3b8; font-weight: 500;">
|
|
6945
|
-
${dict.health.subtitle}
|
|
6946
|
-
</div>
|
|
6947
6792
|
</div>
|
|
6948
6793
|
|
|
6949
6794
|
<div class="matrix-box">
|
|
6950
|
-
<div class="matrix-title">${dict.matrix.title}</div>
|
|
6795
|
+
<div class="matrix-title">${dict.matrix.title} <button onclick="window.print()" class="export-btn">PDF</button></div>
|
|
6951
6796
|
<div class="matrix-row">
|
|
6952
|
-
<div class="matrix-label">${dict.matrix.info}
|
|
6797
|
+
<div class="matrix-label">${dict.matrix.info}</div>
|
|
6953
6798
|
<div class="matrix-value">${responseMatrix.total1xx}</div>
|
|
6954
6799
|
</div>
|
|
6955
6800
|
<div class="matrix-row">
|
|
6956
|
-
<div class="matrix-label">${dict.matrix.success}
|
|
6801
|
+
<div class="matrix-label">${dict.matrix.success}</div>
|
|
6957
6802
|
<div class="matrix-value">${responseMatrix.total2xx}</div>
|
|
6958
6803
|
</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>
|
|
6978
6804
|
</div>
|
|
6979
6805
|
</div>
|
|
6980
6806
|
|
|
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
|
-
|
|
7020
6807
|
<div class="card" style="margin-top: 2rem;">
|
|
7021
|
-
<h3>${dict.telemetry.title}
|
|
6808
|
+
<h3>${dict.telemetry.title} <span id="telemetry-export"></span></h3>
|
|
7022
6809
|
<table>
|
|
7023
6810
|
<thead>
|
|
7024
6811
|
<tr>
|
|
7025
6812
|
<th>${dict.telemetry.threads}</th>
|
|
7026
6813
|
<th>${dict.telemetry.throughput}</th>
|
|
7027
6814
|
<th>${dict.telemetry.avgLatency}</th>
|
|
7028
|
-
<th>${dict.telemetry.p95}</th>
|
|
7029
|
-
<th>${dict.telemetry.errors}</th>
|
|
7030
6815
|
<th>${dict.telemetry.status}</th>
|
|
7031
6816
|
</tr>
|
|
7032
6817
|
</thead>
|
|
@@ -7034,149 +6819,51 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
7034
6819
|
${history.map((h) => {
|
|
7035
6820
|
const isPassed = h.apdex >= config.targetApdex && h.errorRate <= config.targetErrorRate;
|
|
7036
6821
|
return `<tr>
|
|
7037
|
-
<td
|
|
6822
|
+
<td>${h.threads}</td>
|
|
7038
6823
|
<td>${h.throughput.toFixed(0)} /s</td>
|
|
7039
6824
|
<td>${h.avgLatency.toFixed(1)} ms</td>
|
|
7040
|
-
<td>${h.p95Latency.toFixed(1)} ms</td>
|
|
7041
|
-
<td>${(h.errorRate * 100).toFixed(2)}%</td>
|
|
7042
6825
|
<td><span class="badge ${isPassed ? "badge-success" : "badge-fail"}">${isPassed ? dict.telemetry.pass : dict.telemetry.fail}</span></td>
|
|
7043
6826
|
</tr>`;
|
|
7044
6827
|
}).join("")}
|
|
7045
6828
|
</tbody>
|
|
7046
6829
|
</table>
|
|
7047
6830
|
</div>
|
|
7048
|
-
${logClustersHtml}
|
|
7049
6831
|
</div>
|
|
7050
|
-
|
|
7051
|
-
<script>
|
|
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' } }
|
|
7088
|
-
}
|
|
7089
|
-
}
|
|
7090
|
-
});
|
|
7091
6832
|
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
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
|
-
});
|
|
6833
|
+
<script>
|
|
6834
|
+
// --- Export Data Logic ---
|
|
6835
|
+
function downloadBlob(content, fileName, contentType) {
|
|
6836
|
+
const a = document.createElement("a");
|
|
6837
|
+
const file = new Blob([content], { type: contentType });
|
|
6838
|
+
a.href = URL.createObjectURL(file);
|
|
6839
|
+
a.download = fileName;
|
|
6840
|
+
a.click();
|
|
6841
|
+
}
|
|
7135
6842
|
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
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%'
|
|
6843
|
+
function exportToCSV(data, filename) {
|
|
6844
|
+
if (!data || data.length === 0) return;
|
|
6845
|
+
const csvRows = [];
|
|
6846
|
+
const headers = Object.keys(data[0]);
|
|
6847
|
+
csvRows.push(headers.join(','));
|
|
6848
|
+
for (const row of data) {
|
|
6849
|
+
csvRows.push(headers.map(h => JSON.stringify(row[h] || '')).join(','));
|
|
7153
6850
|
}
|
|
7154
|
-
|
|
7155
|
-
}
|
|
6851
|
+
downloadBlob(csvRows.join('\\n'), filename + '.csv', 'text/csv');
|
|
6852
|
+
}
|
|
7156
6853
|
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
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
|
-
});
|
|
6854
|
+
function exportToJSON(data, filename) {
|
|
6855
|
+
downloadBlob(JSON.stringify(data, null, 2), filename + '.json', 'application/json');
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
// Inject dynamic data for export
|
|
6859
|
+
const dashboardHistoryData = ${JSON.stringify(history)};
|
|
6860
|
+
|
|
6861
|
+
// Create export buttons for table
|
|
6862
|
+
const tableExportContainer = document.getElementById('telemetry-export');
|
|
6863
|
+
tableExportContainer.innerHTML = \`
|
|
6864
|
+
<button onclick="exportToCSV(dashboardHistoryData, 'telemetry_data')" class="export-btn">CSV</button>
|
|
6865
|
+
<button onclick="exportToJSON(dashboardHistoryData, 'telemetry_data')" class="export-btn">JSON</button>
|
|
6866
|
+
\`;
|
|
7180
6867
|
</script></body></html>`;
|
|
7181
6868
|
try {
|
|
7182
6869
|
fs.writeFileSync(config.outputHtml, htmlContent, "utf-8");
|