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.
Files changed (2) hide show
  1. package/dist/cli.js +49 -362
  2. 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 - firstTimestamp) / 1e3).toFixed(2)),
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; gap: 0.5rem; }
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">&plusmn;${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="${gaugeColor}" stroke-width="12" fill="transparent"
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} <span class="matrix-badge badge-1xx">1xx</span></div>
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} <span class="matrix-badge badge-2xx">2xx</span></div>
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}</h3>
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><strong>${h.threads}</strong></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
- const baseActiveThreads = ${JSON.stringify(activeThreadsData)};
7093
- const baseTtfTrend = ${JSON.stringify(ttfTrendData)};
7094
-
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
- });
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
- 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%'
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
- 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
- });
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaze-performance-tester",
3
- "version": "3.2.27",
3
+ "version": "3.2.28",
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",