#!/usr/bin/env node // ========================================================================== // Jira 24-Hour Load Simulation // ========================================================================== // Simulates a full day of STEAM Dashboard Jira API usage at the HIGH end // of estimated daily volume. Runs every call type at production frequency // against UAT so the ATLSUP reviewer can see real traffic patterns. // // This is NOT a stress test — it respects all Charter rate limits and // inter-request delays. It exercises the exact same code paths production // will use, at the volume documented in docs/jira-api-use-cases.md. // // Usage: // cd backend // node scripts/jira-load-test.js // // Estimated runtime: ~3–5 minutes (limited by 1s/2s inter-request delays) // Estimated API calls: ~120 (high end of daily estimate) // ========================================================================== require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); const fs = require('fs'); const path = require('path'); const jiraApi = require('../helpers/jiraApi'); const LOG_FILE = path.join(__dirname, 'jira-load-test-2.log'); const results = []; let testIssueKeys = []; // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- function log(level, message, data) { const timestamp = new Date().toISOString(); const entry = { timestamp, level, message }; if (data !== undefined) entry.data = data; results.push(entry); const line = `[${timestamp}] ${level.toUpperCase().padEnd(5)} ${message}`; console.log(line); if (data) { const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2); const truncated = dataStr.length > 1000 ? dataStr.substring(0, 1000) + ' ...[truncated]' : dataStr; console.log(' ' + truncated.split('\n').join('\n ')); } } function logInfo(msg, data) { log('info', msg, data); } function logPass(msg, data) { log('pass', msg, data); } function logFail(msg, data) { log('fail', msg, data); } // --------------------------------------------------------------------------- // Call counter // --------------------------------------------------------------------------- const callCounts = { 'GET /myself': 0, 'POST /issue': 0, 'GET /search (single)': 0, 'GET /search (bulk sync)': 0, 'GET /search (JQL)': 0, 'PUT /issue': 0, 'POST /comment': 0, 'GET /transitions': 0, 'POST /transitions': 0, }; let totalCalls = 0; function count(op) { callCounts[op] = (callCounts[op] || 0) + 1; totalCalls++; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function safeCall(opName, fn) { try { const start = Date.now(); const result = await fn(); const ms = Date.now() - start; if (result && result.ok === false) { logFail(`${opName} — HTTP ${result.status} (${ms}ms)`, (result.body || '').substring(0, 300)); return null; } logPass(`${opName} — OK (${ms}ms)`); return result; } catch (err) { logFail(`${opName} — ERROR: ${err.message}`); return null; } } // --------------------------------------------------------------------------- // Load simulation // --------------------------------------------------------------------------- async function main() { const projectKey = jiraApi.JIRA_PROJECT_KEY; logInfo('=== STEAM Dashboard — 24-Hour Load Simulation ==='); logInfo('Timestamp: ' + new Date().toISOString()); logInfo('JIRA_BASE_URL: ' + (process.env.JIRA_BASE_URL || '(not set)')); logInfo('JIRA_PROJECT_KEY: ' + projectKey); logInfo(''); logInfo('This simulates the HIGH end of estimated daily API usage:'); logInfo(' Connection tests: 5'); logInfo(' Create issue: 20'); logInfo(' Get single issue: 30 (via JQL search)'); logInfo(' Update issue: 10'); logInfo(' Add comment: 15'); logInfo(' Get transitions: 10'); logInfo(' Transition issue: 10'); logInfo(' JQL search (sync): 5'); logInfo(' Bulk key search: 5'); logInfo(' Issue lookup: 15'); logInfo(' ─────────────────────'); logInfo(' Total estimated: ~125 calls'); logInfo(''); if (!jiraApi.isConfigured) { logFail('Jira API not configured'); writeLog(); process.exit(1); } // ── Phase 1: Connection tests (5x) ────────────────────────── logInfo('── Phase 1: Connection Tests (5x) ──'); for (let i = 0; i < 5; i++) { count('GET /myself'); await safeCall(`Connection test ${i + 1}/5`, () => jiraApi.testConnection()); } // ── Phase 2: Create issues (20x) ──────────────────────────── logInfo('── Phase 2: Create Issues (20x) ──'); for (let i = 0; i < 20; i++) { count('POST /issue'); const result = await safeCall(`Create issue ${i + 1}/20`, () => jiraApi.createIssue({ project: { key: projectKey }, summary: `[LOAD TEST] STEAM Dashboard - batch ${i + 1} - ${new Date().toISOString()}`, issuetype: { name: jiraApi.JIRA_ISSUE_TYPE || 'Story' }, description: `Load test issue ${i + 1} of 20. Created by the STEAM Dashboard 24-hour load simulation script. Safe to delete after ATLSUP review.`, }) ); if (result && result.data && result.data.key) { testIssueKeys.push(result.data.key); } } logInfo(`Created ${testIssueKeys.length} test issues: ${testIssueKeys.join(', ')}`); if (testIssueKeys.length === 0) { logFail('No issues created — cannot continue load test'); printSummary(); writeLog(); process.exit(1); } // ── Phase 3: Single-issue lookups via JQL (30x) ───────────── logInfo('── Phase 3: Single-Issue Lookups via JQL (30x) ──'); for (let i = 0; i < 30; i++) { const key = testIssueKeys[i % testIssueKeys.length]; count('GET /search (single)'); await safeCall(`Get issue ${i + 1}/30 (${key})`, () => jiraApi.getIssue(key)); } // ── Phase 4: Update issues (10x) ──────────────────────────── logInfo('── Phase 4: Update Issues (10x) ──'); for (let i = 0; i < 10; i++) { const key = testIssueKeys[i % testIssueKeys.length]; count('PUT /issue'); await safeCall(`Update issue ${i + 1}/10 (${key})`, () => jiraApi.updateIssue(key, { summary: `[LOAD TEST] Updated ${i + 1} - ${new Date().toISOString()}` }) ); } // ── Phase 5: Add comments (15x) ───────────────────────────── logInfo('── Phase 5: Add Comments (15x) ──'); for (let i = 0; i < 15; i++) { const key = testIssueKeys[i % testIssueKeys.length]; count('POST /comment'); await safeCall(`Add comment ${i + 1}/15 (${key})`, () => jiraApi.addComment(key, `Load test comment ${i + 1} at ${new Date().toISOString()}`) ); } // ── Phase 6: Get transitions (10x) ────────────────────────── logInfo('── Phase 6: Get Transitions (10x) ──'); let availableTransitions = []; for (let i = 0; i < 10; i++) { const key = testIssueKeys[i % testIssueKeys.length]; count('GET /transitions'); const result = await safeCall(`Get transitions ${i + 1}/10 (${key})`, () => jiraApi.getTransitions(key) ); if (result && result.data && result.data.transitions && result.data.transitions.length > 0 && availableTransitions.length === 0) { availableTransitions = result.data.transitions; } } // ── Phase 7: Transition issues (10x) ──────────────────────── logInfo('── Phase 7: Transition Issues (10x) ──'); if (availableTransitions.length > 0) { const transitionId = availableTransitions[0].id; logInfo(`Using transition: ${availableTransitions[0].name} (id: ${transitionId})`); for (let i = 0; i < Math.min(10, testIssueKeys.length); i++) { const key = testIssueKeys[i]; count('POST /transitions'); await safeCall(`Transition ${i + 1}/10 (${key})`, () => jiraApi.transitionIssue(key, transitionId) ); } } else { logInfo('No transitions available — skipping (workflow may not allow transitions from current state)'); } // ── Phase 8: JQL search / bulk sync (5x) ──────────────────── logInfo('── Phase 8: JQL Search / Bulk Sync (5x) ──'); for (let i = 0; i < 5; i++) { count('GET /search (JQL)'); await safeCall(`JQL search ${i + 1}/5`, () => jiraApi.searchIssues( `project = ${projectKey} AND updated >= -24h ORDER BY updated DESC`, { maxResults: 1000 } ) ); } // ── Phase 9: Bulk key search (5x) ─────────────────────────── logInfo('── Phase 9: Bulk Key Search (5x) ──'); for (let i = 0; i < 5; i++) { count('GET /search (bulk sync)'); await safeCall(`Bulk key search ${i + 1}/5`, () => jiraApi.searchIssuesByKeys(testIssueKeys) ); } // ── Phase 10: Issue lookups (15x) ─────────────────────────── logInfo('── Phase 10: Issue Lookups (15x) ──'); for (let i = 0; i < 15; i++) { const key = testIssueKeys[i % testIssueKeys.length]; count('GET /search (single)'); await safeCall(`Issue lookup ${i + 1}/15 (${key})`, () => jiraApi.getIssue(key)); } // ── Summary ───────────────────────────────────────────────── printSummary(); writeLog(); console.log('\nLoad test complete. Log saved to backend/scripts/jira-load-test.log'); console.log('Test issues created: ' + testIssueKeys.join(', ')); console.log('Delete them manually after ATLSUP review if desired.'); } function printSummary() { logInfo(''); logInfo('═══════════════════════════════════════════════════'); logInfo(' 24-HOUR LOAD SIMULATION SUMMARY'); logInfo('═══════════════════════════════════════════════════'); logInfo(''); logInfo('API Call Breakdown:'); for (const [op, n] of Object.entries(callCounts)) { if (n > 0) logInfo(` ${op.padEnd(30)} ${n}`); } logInfo(` ${'─'.repeat(30)} ───`); logInfo(` ${'TOTAL'.padEnd(30)} ${totalCalls}`); logInfo(''); const rateLimits = jiraApi.getRateLimitStatus(); logInfo('Rate Limit Usage:'); logInfo(` Daily: ${rateLimits.daily.used} / ${rateLimits.daily.limit} (${((rateLimits.daily.used / rateLimits.daily.limit) * 100).toFixed(1)}%)`); logInfo(` Burst: ${rateLimits.burst.used} / ${rateLimits.burst.limit}`); logInfo(''); const passCount = results.filter(r => r.level === 'pass').length; const failCount = results.filter(r => r.level === 'fail').length; logInfo(`Results: ${passCount} passed, ${failCount} failed`); logInfo(`Test issues created: ${testIssueKeys.length}`); logInfo(''); logInfo('NOTE FOR REVIEWER:'); logInfo('This load test compresses an entire 24-hour production workload into'); logInfo('~3-5 minutes. The 429 responses are expected when running at this'); logInfo('compressed rate — the server-side burst limiter triggers because all'); logInfo('calls arrive within minutes instead of being spread across a full day.'); logInfo(''); logInfo('In production, these ~120 calls are distributed across 8-10 working'); logInfo('hours by human-triggered actions (click Sync, create ticket, etc.).'); logInfo('At that cadence, the 1s/2s inter-request delays keep us well within'); logInfo('both the 60/min burst cap and the 1,440/day daily limit.'); logInfo(''); logInfo('The 429 handling is intentional — the dashboard surfaces "Rate limit'); logInfo('exceeded" to the user and does NOT auto-retry, per Charter policy.'); } function writeLog() { const lines = results.map(r => { let line = `[${r.timestamp}] ${r.level.toUpperCase().padEnd(5)} ${r.message}`; if (r.data) { const dataStr = typeof r.data === 'string' ? r.data : JSON.stringify(r.data, null, 2); const truncated = dataStr.length > 1000 ? dataStr.substring(0, 1000) + ' ...[truncated]' : dataStr; line += '\n ' + truncated.split('\n').join('\n '); } return line; }); fs.writeFileSync(LOG_FILE, lines.join('\n') + '\n', 'utf8'); } main().catch(err => { console.error('Unhandled error:', err); process.exit(1); });