blaze-performance-tester 3.2.30 → 3.2.32

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.
Files changed (2) hide show
  1. package/dist/cli.js +362 -49
  2. 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 - (rawRequests[0]?.timestampMs || Date.now())) / 1e3).toFixed(2)),
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; display: flex; justify-content: space-between; }
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; display: flex; justify-content: space-between; align-items: center; }
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; 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; }
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; position: relative; }
6739
- h3 { margin-top: 0; color: #cbd5e1; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem; display: flex; justify-content: space-between; }
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">&plusmn;${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="#10b981" stroke-width="12" fill="transparent"
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} <button onclick="window.print()" class="export-btn">PDF</button></div>
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} <span id="telemetry-export"></span></h3>
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
- // --- 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
- }
6842
-
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(','));
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
- downloadBlob(csvRows.join('\\n'), filename + '.csv', 'text/csv');
6852
- }
7089
+ }
7090
+ });
6853
7091
 
6854
- function exportToJSON(data, filename) {
6855
- downloadBlob(JSON.stringify(data, null, 2), filename + '.json', 'application/json');
6856
- }
7092
+ const baseActiveThreads = ${JSON.stringify(activeThreadsData)};
7093
+ const baseTtfTrend = ${JSON.stringify(ttfTrendData)};
6857
7094
 
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
- \`;
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaze-performance-tester",
3
- "version": "3.2.30",
3
+ "version": "3.2.32",
4
4
  "description": "A high-performance, multi-threaded load testing engine built with Rust and QuickJS.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",