// =================================== // 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(); } 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"), }; } 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 } 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 = `
${value}