Files
Website-Stress-Test/script.js

1491 lines
43 KiB
JavaScript

// ===================================
// STRESS TESTING TOOL - MAIN SCRIPT
// Enhanced with Crawler & Advanced Features
// ===================================
// ===================================
// WEBSITE CRAWLER CLASS
// ===================================
class WebsiteCrawler {
constructor() {
this.visitedUrls = new Set();
this.urlQueue = [];
}
extractLinks(html, baseUrl) {
const links = [];
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const anchorTags = doc.querySelectorAll("a[href]");
const baseUrlObj = new URL(baseUrl);
anchorTags.forEach((anchor) => {
try {
const href = anchor.getAttribute("href");
if (
!href ||
href.startsWith("#") ||
href.startsWith("javascript:") ||
href.startsWith("mailto:")
) {
return;
}
const absoluteUrl = new URL(href, baseUrl);
// Only include links from the same domain if configured
if (absoluteUrl.hostname === baseUrlObj.hostname) {
const urlString = absoluteUrl.href;
if (!this.visitedUrls.has(urlString)) {
links.push(urlString);
}
}
} catch (e) {
// Invalid URL, skip
}
});
return links;
}
getNextUrl(currentUrl, html, config) {
// Extract links from current page
const links = this.extractLinks(html, currentUrl);
// Add new links to queue (limit per page)
const linksToAdd = links.slice(0, config.linksPerPage || 10);
linksToAdd.forEach((link) => {
if (!this.visitedUrls.has(link) && this.urlQueue.length < 100) {
this.urlQueue.push(link);
}
});
// Mark current URL as visited
this.visitedUrls.add(currentUrl);
// Get next URL from queue
if (this.urlQueue.length > 0) {
const randomIndex = Math.floor(Math.random() * this.urlQueue.length);
return this.urlQueue.splice(randomIndex, 1)[0];
}
return null;
}
reset() {
this.visitedUrls.clear();
this.urlQueue = [];
}
}
// ===================================
// UTILITY FUNCTIONS
// ===================================
function calculatePercentile(arr, percentile) {
if (arr.length === 0) return 0;
const sorted = [...arr].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
function categorizeError(statusCode, errorMessage) {
if (statusCode >= 400 && statusCode < 500) return "4xx";
if (statusCode >= 500) return "5xx";
if (errorMessage && errorMessage.includes("timeout")) return "timeout";
return "network";
}
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}
// ===================================
// MAIN STRESS TESTING TOOL CLASS
// ===================================
class StressTestingTool {
constructor() {
this.config = {
targetUrl: "",
userCount: 100,
duration: 60,
trafficPattern: "steady",
httpMethod: "GET",
customHeaders: {},
requestBody: null,
thinkTime: 1000,
proxyUrl:
window.location.protocol === "file:" ||
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1"
? "http://localhost:3000"
: "/proxy",
// Crawler settings
crawlerEnabled: false,
crawlDepth: 2,
linksPerPage: 10,
stayOnDomain: true,
};
this.state = {
status: "idle",
startTime: null,
pauseTime: null,
elapsedTime: 0,
activeUsers: 0,
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
responseTimes: [],
requestsPerSecond: [],
workers: [],
updateInterval: null,
chartUpdateInterval: null,
userErrorData: [],
errorThreshold: null,
// Enhanced metrics
errorsByCategory: {
"4xx": 0,
"5xx": 0,
timeout: 0,
network: 0,
},
totalBytesSent: 0,
totalBytesReceived: 0,
requestHistory: [],
// Percentile tracking
percentiles: {
p50: 0,
p95: 0,
p99: 0,
},
};
this.crawler = new WebsiteCrawler();
this.charts = {
rps: null,
responseTime: null,
userError: null,
};
// Test presets
this.presets = {
light: { userCount: 10, duration: 30, trafficPattern: "steady" },
medium: { userCount: 100, duration: 60, trafficPattern: "random" },
heavy: { userCount: 500, duration: 120, trafficPattern: "rampup" },
spike: { userCount: 200, duration: 60, trafficPattern: "burst" },
};
this.init();
}
init() {
this.bindElements();
this.attachEventListeners();
this.initializeCharts();
this.loadTheme();
this.loadSavedConfigs();
this.setupKeyboardShortcuts();
this.fetchGitInfo();
}
bindElements() {
// Form inputs
this.elements = {
targetUrl: document.getElementById("targetUrl"),
userCount: document.getElementById("userCount"),
userCountValue: document.getElementById("userCountValue"),
duration: document.getElementById("duration"),
durationValue: document.getElementById("durationValue"),
trafficPattern: document.getElementById("trafficPattern"),
httpMethod: document.getElementById("httpMethod"),
customHeaders: document.getElementById("customHeaders"),
requestBody: document.getElementById("requestBody"),
thinkTime: document.getElementById("thinkTime"),
thinkTimeValue: document.getElementById("thinkTimeValue"),
// Crawler controls
crawlerEnabled: document.getElementById("crawlerEnabled"),
crawlDepth: document.getElementById("crawlDepth"),
crawlDepthValue: document.getElementById("crawlDepthValue"),
linksPerPage: document.getElementById("linksPerPage"),
linksPerPageValue: document.getElementById("linksPerPageValue"),
// Controls
startBtn: document.getElementById("startBtn"),
pauseBtn: document.getElementById("pauseBtn"),
stopBtn: document.getElementById("stopBtn"),
statusBadge: document.getElementById("statusBadge"),
progressBar: document.getElementById("progressBar"),
// Statistics
elapsedTime: document.getElementById("elapsedTime"),
remainingTime: document.getElementById("remainingTime"),
activeUsers: document.getElementById("activeUsers"),
totalRequests: document.getElementById("totalRequests"),
requestsPerSec: document.getElementById("requestsPerSec"),
successRate: document.getElementById("successRate"),
failedRequests: document.getElementById("failedRequests"),
avgResponseTime: document.getElementById("avgResponseTime"),
// Enhanced metrics
p50ResponseTime: document.getElementById("p50ResponseTime"),
p95ResponseTime: document.getElementById("p95ResponseTime"),
p99ResponseTime: document.getElementById("p99ResponseTime"),
errors4xx: document.getElementById("errors4xx"),
errors5xx: document.getElementById("errors5xx"),
errorsTimeout: document.getElementById("errorsTimeout"),
errorsNetwork: document.getElementById("errorsNetwork"),
totalBandwidth: document.getElementById("totalBandwidth"),
// Request history
requestHistoryBody: document.getElementById("requestHistoryBody"),
// Results
resultsPanel: document.getElementById("resultsPanel"),
resultsTableBody: document.getElementById("resultsTableBody"),
exportJsonBtn: document.getElementById("exportJsonBtn"),
exportCsvBtn: document.getElementById("exportCsvBtn"),
// Advanced options
advancedToggle: document.getElementById("advancedToggle"),
advancedContent: document.getElementById("advancedContent"),
// Theme & presets
themeToggle: document.getElementById("themeToggle"),
presetSelect: document.getElementById("presetSelect"),
saveConfigBtn: document.getElementById("saveConfigBtn"),
// Git Info
gitInfo: document.getElementById("gitInfo"),
gitCommit: document.getElementById("gitCommit"),
gitDate: document.getElementById("gitDate"),
};
}
attachEventListeners() {
// Range inputs
this.elements.userCount.addEventListener("input", (e) => {
this.elements.userCountValue.textContent = e.target.value;
});
this.elements.duration.addEventListener("input", (e) => {
this.elements.durationValue.textContent = e.target.value;
});
this.elements.thinkTime.addEventListener("input", (e) => {
this.elements.thinkTimeValue.textContent = e.target.value;
});
if (this.elements.crawlDepth) {
this.elements.crawlDepth.addEventListener("input", (e) => {
this.elements.crawlDepthValue.textContent = e.target.value;
});
}
if (this.elements.linksPerPage) {
this.elements.linksPerPage.addEventListener("input", (e) => {
this.elements.linksPerPageValue.textContent = e.target.value;
});
}
// Control buttons
this.elements.startBtn.addEventListener("click", () => this.startTest());
this.elements.pauseBtn.addEventListener("click", () => this.pauseTest());
this.elements.stopBtn.addEventListener("click", () => this.stopTest());
// Export buttons
this.elements.exportJsonBtn.addEventListener("click", () =>
this.exportResults("json")
);
this.elements.exportCsvBtn.addEventListener("click", () =>
this.exportResults("csv")
);
// Advanced options accordion
this.elements.advancedToggle.addEventListener("click", () => {
this.elements.advancedToggle.classList.toggle("active");
this.elements.advancedContent.classList.toggle("active");
});
// Theme toggle
if (this.elements.themeToggle) {
this.elements.themeToggle.addEventListener("click", () =>
this.toggleTheme()
);
}
// Preset selector
if (this.elements.presetSelect) {
this.elements.presetSelect.addEventListener("change", (e) =>
this.loadPreset(e.target.value)
);
}
// Save config
if (this.elements.saveConfigBtn) {
this.elements.saveConfigBtn.addEventListener("click", () =>
this.saveConfig()
);
}
}
setupKeyboardShortcuts() {
document.addEventListener("keydown", (e) => {
// Don't trigger if user is typing in an input
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return;
switch (e.key.toLowerCase()) {
case "s":
if (this.state.status === "idle") this.startTest();
break;
case "p":
if (this.state.status === "running" || this.state.status === "paused")
this.pauseTest();
break;
case "x":
if (this.state.status === "running" || this.state.status === "paused")
this.stopTest();
break;
}
});
}
loadTheme() {
const savedTheme = localStorage.getItem("stressTestTheme") || "dark";
document.documentElement.setAttribute("data-theme", savedTheme);
}
toggleTheme() {
const currentTheme =
document.documentElement.getAttribute("data-theme") || "dark";
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("stressTestTheme", newTheme);
// Update chart colors
this.updateChartTheme();
}
updateChartTheme() {
const isDark =
document.documentElement.getAttribute("data-theme") === "dark";
const textColor = isDark ? "#94a3b8" : "#475569";
const gridColor = isDark
? "rgba(148, 163, 184, 0.1)"
: "rgba(148, 163, 184, 0.2)";
Object.values(this.charts).forEach((chart) => {
if (chart) {
chart.options.scales.x.ticks.color = textColor;
chart.options.scales.x.grid.color = gridColor;
chart.options.scales.y.ticks.color = textColor;
chart.options.scales.y.grid.color = gridColor;
if (chart.options.scales.y1) {
chart.options.scales.y1.ticks.color = textColor;
}
chart.update("none");
}
});
}
loadSavedConfigs() {
const saved = localStorage.getItem("stressTestConfigs");
if (saved) {
try {
const configs = JSON.parse(saved);
// Add to preset select if exists
if (this.elements.presetSelect) {
Object.keys(configs).forEach((name) => {
const option = document.createElement("option");
option.value = `saved_${name}`;
option.textContent = `💾 ${name}`;
this.elements.presetSelect.appendChild(option);
});
}
} catch (e) {
console.error("Failed to load saved configs:", e);
}
}
}
loadPreset(presetName) {
if (!presetName) return;
let config;
if (presetName.startsWith("saved_")) {
const saved = JSON.parse(
localStorage.getItem("stressTestConfigs") || "{}"
);
config = saved[presetName.replace("saved_", "")];
} else {
config = this.presets[presetName];
}
if (config) {
this.elements.userCount.value = config.userCount;
this.elements.userCountValue.textContent = config.userCount;
this.elements.duration.value = config.duration;
this.elements.durationValue.textContent = config.duration;
this.elements.trafficPattern.value = config.trafficPattern;
}
}
saveConfig() {
const name = prompt("Enter a name for this configuration:");
if (!name) return;
const config = {
userCount: parseInt(this.elements.userCount.value),
duration: parseInt(this.elements.duration.value),
trafficPattern: this.elements.trafficPattern.value,
targetUrl: this.elements.targetUrl.value,
};
const saved = JSON.parse(localStorage.getItem("stressTestConfigs") || "{}");
saved[name] = config;
localStorage.setItem("stressTestConfigs", JSON.stringify(saved));
alert(`Configuration "${name}" saved!`);
location.reload(); // Reload to update preset list
}
async fetchGitInfo() {
try {
// Ensure we don't have double slashes if proxyUrl ends with slash (it shouldn't based on init logic)
const url = `${this.config.proxyUrl}/git-info`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
if (data.commit && data.date && data.commit !== 'Unknown') {
if (this.elements.gitCommit) this.elements.gitCommit.textContent = data.commit;
if (this.elements.gitDate) {
let dateStr = data.date;
// Shorten to match screenshot style (approximate)
dateStr = dateStr.replace(/ days? ago/, 'd ago')
.replace(/ hours? ago/, 'h ago')
.replace(/ minutes? ago/, 'm ago')
.replace(/ seconds? ago/, 's ago');
this.elements.gitDate.textContent = dateStr;
}
if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex';
}
}
} catch (e) {
console.error('Failed to fetch git info:', e);
}
}
initializeCharts() {
const isDark =
document.documentElement.getAttribute("data-theme") === "dark";
const textColor = isDark ? "#94a3b8" : "#475569";
const gridColor = isDark
? "rgba(148, 163, 184, 0.1)"
: "rgba(148, 163, 184, 0.2)";
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
grid: {
color: gridColor,
},
ticks: {
color: textColor,
},
},
y: {
grid: {
color: gridColor,
},
ticks: {
color: textColor,
},
beginAtZero: true,
},
},
};
// RPS Chart
const rpsCtx = document.getElementById("rpsChart").getContext("2d");
this.charts.rps = new Chart(rpsCtx, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Requests per Second",
data: [],
borderColor: "#6366f1",
backgroundColor: "rgba(99, 102, 241, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
},
],
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "Requests per Second",
color: textColor,
font: {
size: 14,
weight: 600,
},
},
},
},
});
// Response Time Chart
const responseTimeCtx = document
.getElementById("responseTimeChart")
.getContext("2d");
this.charts.responseTime = new Chart(responseTimeCtx, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Average Response Time (ms)",
data: [],
borderColor: "#f59e0b",
backgroundColor: "rgba(245, 158, 11, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
},
],
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "Average Response Time",
color: textColor,
font: {
size: 14,
weight: 600,
},
},
},
},
});
// User/Error Correlation Chart
const userErrorCtx = document
.getElementById("userErrorChart")
.getContext("2d");
this.charts.userError = new Chart(userErrorCtx, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Active Users",
data: [],
borderColor: "#3b82f6",
backgroundColor: "rgba(59, 130, 246, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
yAxisID: "y",
},
{
label: "Error Rate (%)",
data: [],
borderColor: "#ef4444",
backgroundColor: "rgba(239, 68, 68, 0.2)",
borderWidth: 2,
fill: true,
tension: 0.4,
yAxisID: "y1",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
display: true,
labels: {
color: textColor,
font: {
size: 12,
weight: 600,
},
},
},
title: {
display: true,
text: "User Load vs Error Rate",
color: textColor,
font: {
size: 14,
weight: 600,
},
},
},
scales: {
x: {
grid: {
color: gridColor,
},
ticks: {
color: textColor,
},
},
y: {
type: "linear",
display: true,
position: "left",
title: {
display: true,
text: "Active Users",
color: "#3b82f6",
font: {
size: 12,
weight: 600,
},
},
grid: {
color: gridColor,
},
ticks: {
color: textColor,
},
beginAtZero: true,
},
y1: {
type: "linear",
display: true,
position: "right",
title: {
display: true,
text: "Error Rate (%)",
color: "#ef4444",
font: {
size: 12,
weight: 600,
},
},
grid: {
drawOnChartArea: false,
},
ticks: {
color: textColor,
},
beginAtZero: true,
max: 100,
},
},
},
});
}
async startTest() {
if (!this.validateConfig()) {
return;
}
this.gatherConfig();
this.resetState();
this.updateStatus("running");
this.state.startTime = Date.now();
// Update UI
this.elements.startBtn.disabled = true;
this.elements.pauseBtn.disabled = false;
this.elements.stopBtn.disabled = false;
// Start workers
this.startWorkers();
// Start update intervals
this.state.updateInterval = setInterval(() => this.updateStatistics(), 100);
this.state.chartUpdateInterval = setInterval(
() => this.updateCharts(),
1000
);
}
pauseTest() {
if (this.state.status === "running") {
this.updateStatus("paused");
this.state.pauseTime = Date.now();
this.stopWorkers();
this.elements.pauseBtn.textContent = "▶️ Resume";
} else if (this.state.status === "paused") {
this.updateStatus("running");
const pauseDuration = Date.now() - this.state.pauseTime;
this.state.startTime += pauseDuration;
this.startWorkers();
this.elements.pauseBtn.textContent = "⏸️ Pause";
}
}
stopTest() {
this.updateStatus("stopped");
this.stopWorkers();
clearInterval(this.state.updateInterval);
clearInterval(this.state.chartUpdateInterval);
// Update UI
this.elements.startBtn.disabled = false;
this.elements.pauseBtn.disabled = true;
this.elements.stopBtn.disabled = true;
this.elements.pauseBtn.textContent = "⏸️ Pause";
// Calculate final percentiles
this.calculatePercentiles();
// Show results
this.displayResults();
}
validateConfig() {
const url = this.elements.targetUrl.value.trim();
if (!url) {
alert("Please enter a target URL");
return false;
}
try {
new URL(url);
} catch (e) {
alert("Please enter a valid URL");
return false;
}
const headersText = this.elements.customHeaders.value.trim();
if (headersText) {
try {
JSON.parse(headersText);
} catch (e) {
alert("Custom headers must be valid JSON");
return false;
}
}
const bodyText = this.elements.requestBody.value.trim();
if (bodyText) {
try {
JSON.parse(bodyText);
} catch (e) {
alert("Request body must be valid JSON");
return false;
}
}
return true;
}
gatherConfig() {
this.config.targetUrl = this.elements.targetUrl.value.trim();
this.config.userCount = parseInt(this.elements.userCount.value);
this.config.duration = parseInt(this.elements.duration.value);
this.config.trafficPattern = this.elements.trafficPattern.value;
this.config.httpMethod = this.elements.httpMethod.value;
this.config.thinkTime = parseInt(this.elements.thinkTime.value);
const headersText = this.elements.customHeaders.value.trim();
this.config.customHeaders = headersText ? JSON.parse(headersText) : {};
const bodyText = this.elements.requestBody.value.trim();
this.config.requestBody = bodyText ? JSON.parse(bodyText) : null;
// Crawler config
if (this.elements.crawlerEnabled) {
this.config.crawlerEnabled = this.elements.crawlerEnabled.checked;
this.config.crawlDepth = parseInt(this.elements.crawlDepth?.value || 2);
this.config.linksPerPage = parseInt(
this.elements.linksPerPage?.value || 10
);
}
}
resetState() {
this.state.elapsedTime = 0;
this.state.activeUsers = 0;
this.state.totalRequests = 0;
this.state.successfulRequests = 0;
this.state.failedRequests = 0;
this.state.responseTimes = [];
this.state.requestsPerSecond = [];
this.state.workers = [];
this.state.userErrorData = [];
this.state.errorThreshold = null;
this.state.errorsByCategory = {
"4xx": 0,
"5xx": 0,
timeout: 0,
network: 0,
};
this.state.totalBytesSent = 0;
this.state.totalBytesReceived = 0;
this.state.requestHistory = [];
this.state.percentiles = { p50: 0, p95: 0, p99: 0 };
// Reset crawler
this.crawler.reset();
if (this.config.crawlerEnabled) {
this.crawler.urlQueue.push(this.config.targetUrl);
}
// Reset charts
this.charts.rps.data.labels = [];
this.charts.rps.data.datasets[0].data = [];
this.charts.responseTime.data.labels = [];
this.charts.responseTime.data.datasets[0].data = [];
this.charts.userError.data.labels = [];
this.charts.userError.data.datasets[0].data = [];
this.charts.userError.data.datasets[1].data = [];
this.charts.rps.update("none");
this.charts.responseTime.update("none");
this.charts.userError.update("none");
// Clear request history table
if (this.elements.requestHistoryBody) {
this.elements.requestHistoryBody.innerHTML = "";
}
// Hide results panel
this.elements.resultsPanel.style.display = "none";
}
startWorkers() {
const pattern = this.config.trafficPattern;
switch (pattern) {
case "steady":
this.startSteadyLoad();
break;
case "burst":
this.startBurstLoad();
break;
case "rampup":
this.startRampUpLoad();
break;
case "random":
this.startRandomLoad();
break;
}
}
startSteadyLoad() {
const delayBetweenUsers = 100;
for (let i = 0; i < this.config.userCount; i++) {
setTimeout(() => {
if (this.state.status === "running") {
this.createWorker(i);
}
}, i * delayBetweenUsers);
}
}
startBurstLoad() {
const burstSize = Math.ceil(this.config.userCount / 5);
const burstInterval = (this.config.duration * 1000) / 5;
for (let burst = 0; burst < 5; burst++) {
setTimeout(() => {
if (this.state.status === "running") {
for (let i = 0; i < burstSize; i++) {
this.createWorker(burst * burstSize + i);
}
}
}, burst * burstInterval);
}
}
startRampUpLoad() {
const totalTime = this.config.duration * 1000;
const timePerUser = totalTime / this.config.userCount;
for (let i = 0; i < this.config.userCount; i++) {
setTimeout(() => {
if (this.state.status === "running") {
this.createWorker(i);
}
}, i * timePerUser);
}
}
startRandomLoad() {
const maxDelay = (this.config.duration * 1000) / 2;
for (let i = 0; i < this.config.userCount; i++) {
const randomDelay = Math.random() * maxDelay;
setTimeout(() => {
if (this.state.status === "running") {
this.createWorker(i);
}
}, randomDelay);
}
}
createWorker(id) {
const worker = {
id: id,
active: true,
requestCount: 0,
currentUrl: this.config.targetUrl,
crawlDepth: 0,
};
this.state.workers.push(worker);
this.state.activeUsers++;
this.runWorker(worker);
}
async runWorker(worker) {
const endTime = this.state.startTime + this.config.duration * 1000;
while (
worker.active &&
this.state.status === "running" &&
Date.now() < endTime
) {
await this.makeRequest(worker);
// Think time
if (this.config.thinkTime > 0) {
await this.sleep(this.config.thinkTime);
}
}
// Worker finished
worker.active = false;
this.state.activeUsers--;
// Check if all workers are done
if (this.state.activeUsers === 0 && this.state.status === "running") {
this.stopTest();
}
}
async makeRequest(worker) {
const startTime = performance.now();
const requestUrl = worker.currentUrl;
try {
const proxyPayload = {
targetUrl: requestUrl,
method: this.config.httpMethod,
headers: this.config.customHeaders,
body: this.config.requestBody,
};
// Estimate request size
const requestSize = JSON.stringify(proxyPayload).length;
this.state.totalBytesSent += requestSize;
const response = await fetch(this.config.proxyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(proxyPayload),
});
const endTime = performance.now();
const responseTime = endTime - startTime;
this.state.totalRequests++;
worker.requestCount++;
const proxyResponse = await response.json();
// Track response size
if (proxyResponse.body) {
this.state.totalBytesReceived += proxyResponse.body.length;
}
const isSuccess =
proxyResponse.success &&
proxyResponse.statusCode >= 200 &&
proxyResponse.statusCode < 400;
if (isSuccess) {
this.state.successfulRequests++;
} else {
this.state.failedRequests++;
const category = categorizeError(
proxyResponse.statusCode,
proxyResponse.error
);
this.state.errorsByCategory[category]++;
}
const actualResponseTime = proxyResponse.responseTime || responseTime;
this.state.responseTimes.push(actualResponseTime);
// Keep only last 1000 response times
if (this.state.responseTimes.length > 1000) {
this.state.responseTimes.shift();
}
// Add to request history
this.addToRequestHistory({
url: requestUrl,
status: proxyResponse.statusCode,
responseTime: Math.round(actualResponseTime),
success: isSuccess,
timestamp: new Date().toLocaleTimeString(),
});
// Crawler: Get next URL if enabled
if (
this.config.crawlerEnabled &&
isSuccess &&
proxyResponse.body &&
worker.crawlDepth < this.config.crawlDepth
) {
const nextUrl = this.crawler.getNextUrl(
requestUrl,
proxyResponse.body,
this.config
);
if (nextUrl) {
worker.currentUrl = nextUrl;
worker.crawlDepth++;
}
}
} catch (error) {
const endTime = performance.now();
const responseTime = endTime - startTime;
this.state.totalRequests++;
this.state.failedRequests++;
this.state.responseTimes.push(responseTime);
this.state.errorsByCategory["network"]++;
worker.requestCount++;
this.addToRequestHistory({
url: requestUrl,
status: 0,
responseTime: Math.round(responseTime),
success: false,
timestamp: new Date().toLocaleTimeString(),
error: error.message,
});
}
}
addToRequestHistory(request) {
this.state.requestHistory.unshift(request);
// Keep only last 100
if (this.state.requestHistory.length > 100) {
this.state.requestHistory.pop();
}
// Update UI table
if (this.elements.requestHistoryBody) {
const row = document.createElement("tr");
row.className = request.success ? "success-row" : "error-row";
row.innerHTML = `
<td>${request.timestamp}</td>
<td class="url-cell" title="${request.url}">${this.truncateUrl(
request.url
)}</td>
<td><span class="status-code ${
request.success ? "success" : "error"
}">${request.status}</span></td>
<td>${request.responseTime}ms</td>
`;
this.elements.requestHistoryBody.insertBefore(
row,
this.elements.requestHistoryBody.firstChild
);
// Keep only 100 rows in DOM
while (this.elements.requestHistoryBody.children.length > 100) {
this.elements.requestHistoryBody.removeChild(
this.elements.requestHistoryBody.lastChild
);
}
}
}
truncateUrl(url) {
if (url.length > 50) {
return url.substring(0, 47) + "...";
}
return url;
}
stopWorkers() {
this.state.workers.forEach((worker) => {
worker.active = false;
});
}
calculatePercentiles() {
if (this.state.responseTimes.length > 0) {
this.state.percentiles.p50 = Math.round(
calculatePercentile(this.state.responseTimes, 50)
);
this.state.percentiles.p95 = Math.round(
calculatePercentile(this.state.responseTimes, 95)
);
this.state.percentiles.p99 = Math.round(
calculatePercentile(this.state.responseTimes, 99)
);
}
}
updateStatistics() {
const now = Date.now();
const elapsed = Math.floor((now - this.state.startTime) / 1000);
const remaining = Math.max(0, this.config.duration - elapsed);
const progress = Math.min(100, (elapsed / this.config.duration) * 100);
// Update time displays
this.elements.elapsedTime.textContent = `${elapsed}s`;
this.elements.remainingTime.textContent = `${remaining}s`;
this.elements.progressBar.style.width = `${progress}%`;
// Update statistics
this.elements.activeUsers.textContent = this.state.activeUsers;
this.elements.totalRequests.textContent =
this.state.totalRequests.toLocaleString();
this.elements.failedRequests.textContent =
this.state.failedRequests.toLocaleString();
// Calculate RPS
const rps =
elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0;
this.elements.requestsPerSec.textContent = rps;
// Calculate success rate
const successRate =
this.state.totalRequests > 0
? (
(this.state.successfulRequests / this.state.totalRequests) *
100
).toFixed(1)
: 0;
this.elements.successRate.textContent = `${successRate}%`;
// Calculate average response time
const avgResponseTime =
this.state.responseTimes.length > 0
? Math.round(
this.state.responseTimes.reduce((a, b) => a + b, 0) /
this.state.responseTimes.length
)
: 0;
this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`;
// Update enhanced metrics
if (this.elements.p50ResponseTime) {
const p50 = Math.round(calculatePercentile(this.state.responseTimes, 50));
const p95 = Math.round(calculatePercentile(this.state.responseTimes, 95));
const p99 = Math.round(calculatePercentile(this.state.responseTimes, 99));
this.elements.p50ResponseTime.textContent = `${p50}ms`;
this.elements.p95ResponseTime.textContent = `${p95}ms`;
this.elements.p99ResponseTime.textContent = `${p99}ms`;
}
if (this.elements.errors4xx) {
this.elements.errors4xx.textContent = this.state.errorsByCategory["4xx"];
this.elements.errors5xx.textContent = this.state.errorsByCategory["5xx"];
this.elements.errorsTimeout.textContent =
this.state.errorsByCategory["timeout"];
this.elements.errorsNetwork.textContent =
this.state.errorsByCategory["network"];
}
if (this.elements.totalBandwidth) {
const totalBytes =
this.state.totalBytesSent + this.state.totalBytesReceived;
this.elements.totalBandwidth.textContent = formatBytes(totalBytes);
}
}
updateCharts() {
const now = Date.now();
const elapsed = Math.floor((now - this.state.startTime) / 1000);
// Calculate current RPS
const currentRps =
this.state.totalRequests > 0 && elapsed > 0
? Math.round(this.state.totalRequests / elapsed)
: 0;
// Calculate current average response time
const recentResponseTimes = this.state.responseTimes.slice(-100);
const currentAvgResponseTime =
recentResponseTimes.length > 0
? Math.round(
recentResponseTimes.reduce((a, b) => a + b, 0) /
recentResponseTimes.length
)
: 0;
// Update RPS chart
this.charts.rps.data.labels.push(`${elapsed}s`);
this.charts.rps.data.datasets[0].data.push(currentRps);
if (this.charts.rps.data.labels.length > 60) {
this.charts.rps.data.labels.shift();
this.charts.rps.data.datasets[0].data.shift();
}
this.charts.rps.update("none");
// Update Response Time chart
this.charts.responseTime.data.labels.push(`${elapsed}s`);
this.charts.responseTime.data.datasets[0].data.push(currentAvgResponseTime);
if (this.charts.responseTime.data.labels.length > 60) {
this.charts.responseTime.data.labels.shift();
this.charts.responseTime.data.datasets[0].data.shift();
}
this.charts.responseTime.update("none");
// Calculate current error rate
const currentErrorRate =
this.state.totalRequests > 0
? (
(this.state.failedRequests / this.state.totalRequests) *
100
).toFixed(1)
: 0;
// Update User/Error chart
this.charts.userError.data.labels.push(`${elapsed}s`);
this.charts.userError.data.datasets[0].data.push(this.state.activeUsers);
this.charts.userError.data.datasets[1].data.push(
parseFloat(currentErrorRate)
);
// Track user/error data
this.state.userErrorData.push({
time: elapsed,
users: this.state.activeUsers,
errorRate: parseFloat(currentErrorRate),
failedRequests: this.state.failedRequests,
});
// Detect error threshold
if (
this.state.errorThreshold === null &&
this.state.failedRequests > 0 &&
this.state.activeUsers > 0
) {
this.state.errorThreshold = {
users: this.state.activeUsers,
time: elapsed,
errorRate: parseFloat(currentErrorRate),
};
}
if (this.charts.userError.data.labels.length > 60) {
this.charts.userError.data.labels.shift();
this.charts.userError.data.datasets[0].data.shift();
this.charts.userError.data.datasets[1].data.shift();
}
this.charts.userError.update("none");
}
updateStatus(status) {
this.state.status = status;
const badge = this.elements.statusBadge;
badge.className = "status-badge";
switch (status) {
case "idle":
badge.classList.add("status-idle");
badge.textContent = "Idle";
break;
case "running":
badge.classList.add("status-running");
badge.textContent = "Running";
break;
case "paused":
badge.classList.add("status-paused");
badge.textContent = "Paused";
break;
case "stopped":
badge.classList.add("status-idle");
badge.textContent = "Completed";
break;
}
}
displayResults() {
this.elements.resultsPanel.style.display = "block";
const results = this.calculateResults();
const tbody = this.elements.resultsTableBody;
tbody.innerHTML = "";
// Populate results table
Object.entries(results).forEach(([key, value]) => {
const row = document.createElement("tr");
row.innerHTML = `
<td><strong>${key}</strong></td>
<td><code>${value}</code></td>
`;
tbody.appendChild(row);
});
// Scroll to results
this.elements.resultsPanel.scrollIntoView({ behavior: "smooth" });
}
calculateResults() {
const totalTime =
this.state.elapsedTime ||
Math.floor((Date.now() - this.state.startTime) / 1000);
const successRate =
this.state.totalRequests > 0
? (
(this.state.successfulRequests / this.state.totalRequests) *
100
).toFixed(2)
: 0;
const avgResponseTime =
this.state.responseTimes.length > 0
? Math.round(
this.state.responseTimes.reduce((a, b) => a + b, 0) /
this.state.responseTimes.length
)
: 0;
const minResponseTime =
this.state.responseTimes.length > 0
? Math.round(Math.min(...this.state.responseTimes))
: 0;
const maxResponseTime =
this.state.responseTimes.length > 0
? Math.round(Math.max(...this.state.responseTimes))
: 0;
const rps =
totalTime > 0 ? (this.state.totalRequests / totalTime).toFixed(2) : 0;
const results = {
"Target URL": this.config.targetUrl,
"Test Duration": `${totalTime} seconds`,
"Concurrent Users": this.config.userCount,
"Traffic Pattern": this.config.trafficPattern,
"Crawler Mode": this.config.crawlerEnabled ? "Enabled" : "Disabled",
"Total Requests": this.state.totalRequests.toLocaleString(),
"Successful Requests": this.state.successfulRequests.toLocaleString(),
"Failed Requests": this.state.failedRequests.toLocaleString(),
"Success Rate": `${successRate}%`,
"Requests per Second": rps,
"Average Response Time": `${avgResponseTime}ms`,
"Min Response Time": `${minResponseTime}ms`,
"Max Response Time": `${maxResponseTime}ms`,
"P50 Response Time": `${this.state.percentiles.p50}ms`,
"P95 Response Time": `${this.state.percentiles.p95}ms`,
"P99 Response Time": `${this.state.percentiles.p99}ms`,
"4xx Errors": this.state.errorsByCategory["4xx"],
"5xx Errors": this.state.errorsByCategory["5xx"],
"Timeout Errors": this.state.errorsByCategory["timeout"],
"Network Errors": this.state.errorsByCategory["network"],
"Total Bandwidth": formatBytes(
this.state.totalBytesSent + this.state.totalBytesReceived
),
"Data Sent": formatBytes(this.state.totalBytesSent),
"Data Received": formatBytes(this.state.totalBytesReceived),
"HTTP Method": this.config.httpMethod,
"Think Time": `${this.config.thinkTime}ms`,
"Error Threshold": this.state.errorThreshold
? `${this.state.errorThreshold.users} users at ${this.state.errorThreshold.time}s (${this.state.errorThreshold.errorRate}% error rate)`
: "No errors detected",
};
if (this.config.crawlerEnabled) {
results["Unique URLs Visited"] = this.crawler.visitedUrls.size;
}
return results;
}
exportResults(format) {
const results = this.calculateResults();
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
if (format === "json") {
const data = {
config: this.config,
results: results,
requestHistory: this.state.requestHistory.slice(0, 100),
timestamp: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
this.downloadFile(blob, `stress-test-results-${timestamp}.json`);
} else if (format === "csv") {
let csv = "Metric,Value\n";
Object.entries(results).forEach(([key, value]) => {
csv += `"${key}","${value}"\n`;
});
const blob = new Blob([csv], { type: "text/csv" });
this.downloadFile(blob, `stress-test-results-${timestamp}.csv`);
}
}
downloadFile(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Initialize the application
document.addEventListener("DOMContentLoaded", () => {
new StressTestingTool();
});