feat: add k6 load testing suite, NRQL query library, and CLAUDE.md
Add comprehensive load testing infrastructure: - k6 auth-dashboard flow (login → profile → dashboard KPIs → widgets → refresh → logout) - k6 CRUD flow (units, vendors, journal entries, payments, reports) - Environment configs with staging/production/local thresholds - Parameterized user pool CSV matching app roles - New Relic NRQL query library (25+ queries for perf analysis) - Empty baseline.json structure for all tested endpoints - CLAUDE.md documenting full stack, auth, route map, and conventions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
141
load-tests/analysis/baseline.json
Normal file
141
load-tests/analysis/baseline.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"_meta": {
|
||||
"capturedAt": null,
|
||||
"environment": "staging",
|
||||
"k6Version": null,
|
||||
"vus": null,
|
||||
"duration": null,
|
||||
"notes": "Empty baseline – populate after first stable load test run"
|
||||
},
|
||||
"auth": {
|
||||
"POST /api/auth/login": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"POST /api/auth/refresh": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"POST /api/auth/logout": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/auth/profile": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"GET /api/reports/dashboard": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/reports/balance-sheet": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/reports/income-statement": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/reports/aging": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/health-scores/latest": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/reports/cash-flow": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
},
|
||||
"GET /api/reports/cash-flow-forecast": {
|
||||
"p50": null,
|
||||
"p95": null,
|
||||
"p99": null,
|
||||
"errorRate": null,
|
||||
"rps": null
|
||||
}
|
||||
},
|
||||
"crud": {
|
||||
"units": {
|
||||
"GET /api/units": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/units": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/units/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"PUT /api/units/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"DELETE /api/units/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"vendors": {
|
||||
"GET /api/vendors": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/vendors": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/vendors/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"PUT /api/vendors/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"journalEntries": {
|
||||
"GET /api/journal-entries": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/journal-entries": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/journal-entries/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/journal-entries/:id/post": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/journal-entries/:id/void": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"payments": {
|
||||
"GET /api/payments": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/payments": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/payments/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"PUT /api/payments/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"DELETE /api/payments/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"accounts": {
|
||||
"GET /api/accounts": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/accounts/trial-balance": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/accounts": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"PUT /api/accounts/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"invoices": {
|
||||
"GET /api/invoices": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/invoices/:id": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/invoices/generate-bulk": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
}
|
||||
},
|
||||
"boardPlanning": {
|
||||
"GET /api/board-planning/scenarios": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"POST /api/board-planning/scenarios": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/board-planning/scenarios/:id/projection": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/board-planning/budget-plans": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
},
|
||||
"organizations": {
|
||||
"GET /api/organizations": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null },
|
||||
"GET /api/organizations/members": { "p50": null, "p95": null, "p99": null, "errorRate": null, "rps": null }
|
||||
}
|
||||
}
|
||||
270
load-tests/analysis/nrql-queries.sql
Normal file
270
load-tests/analysis/nrql-queries.sql
Normal file
@@ -0,0 +1,270 @@
|
||||
-- =============================================================================
|
||||
-- HOA Financial Platform (HOALedgerIQ) – New Relic NRQL Query Library
|
||||
-- App Name: HOALedgerIQ_App (see NEW_RELIC_APP_NAME env var)
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. OVERVIEW & THROUGHPUT
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Overall throughput (requests/min) over past hour
|
||||
SELECT rate(count(*), 1 minute) AS 'RPM'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Throughput by HTTP method
|
||||
SELECT rate(count(*), 1 minute) AS 'RPM'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Apdex score over time
|
||||
SELECT apdex(duration, t: 0.5)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. AUTHENTICATION ENDPOINTS
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Login latency (p50, p95, p99)
|
||||
SELECT percentile(duration, 50, 95, 99) AS 'Login Latency (s)'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri = '/api/auth/login'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Login error rate
|
||||
SELECT percentage(count(*), WHERE httpResponseCode >= 400) AS 'Login Error %'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri = '/api/auth/login'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Token refresh latency
|
||||
SELECT percentile(duration, 50, 95, 99) AS 'Refresh Latency (s)'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri = '/api/auth/refresh'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Auth endpoints overview
|
||||
SELECT count(*), average(duration), percentile(duration, 95)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/auth/%'
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. DASHBOARD & REPORTS
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Dashboard KPI latency
|
||||
SELECT percentile(duration, 50, 95, 99) AS 'Dashboard Latency (s)'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri = '/api/reports/dashboard'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- All report endpoints performance
|
||||
SELECT count(*), average(duration), percentile(duration, 95)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/reports/%'
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Slowest report queries (> 2s)
|
||||
SELECT count(*)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/reports/%'
|
||||
AND duration > 2
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. CRUD OPERATIONS
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Units endpoint latency by method
|
||||
SELECT percentile(duration, 50, 95) AS 'Units Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/units%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Vendors endpoint latency by method
|
||||
SELECT percentile(duration, 50, 95) AS 'Vendors Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/vendors%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Journal entries performance
|
||||
SELECT percentile(duration, 50, 95) AS 'JE Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/journal-entries%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Payments performance
|
||||
SELECT percentile(duration, 50, 95) AS 'Payments Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/payments%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Accounts performance
|
||||
SELECT percentile(duration, 50, 95) AS 'Accounts Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/accounts%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Invoices performance
|
||||
SELECT percentile(duration, 50, 95) AS 'Invoices Latency'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/invoices%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. MULTI-TENANT / ORG OPERATIONS
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Organizations endpoint performance
|
||||
SELECT percentile(duration, 50, 95)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/organizations%'
|
||||
FACET request.method
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Board planning (complex module) latency
|
||||
SELECT count(*), percentile(duration, 50, 95, 99)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND request.uri LIKE '/api/board-planning%'
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. ERROR ANALYSIS
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Error rate by endpoint (top 20 offenders)
|
||||
SELECT percentage(count(*), WHERE httpResponseCode >= 400) AS 'Error %',
|
||||
count(*) AS 'Total'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago
|
||||
LIMIT 20;
|
||||
|
||||
-- 5xx errors specifically
|
||||
SELECT count(*)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND httpResponseCode >= 500
|
||||
FACET request.uri, httpResponseCode
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- Error rate over time
|
||||
SELECT percentage(count(*), WHERE httpResponseCode >= 500) AS 'Server Error %',
|
||||
percentage(count(*), WHERE httpResponseCode >= 400 AND httpResponseCode < 500) AS 'Client Error %'
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. DATABASE & EXTERNAL SERVICES
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Database call duration (TypeORM / Postgres)
|
||||
SELECT average(databaseDuration), percentile(databaseDuration, 95)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Slowest DB transactions
|
||||
SELECT average(databaseDuration), count(*)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND databaseDuration > 1
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago
|
||||
LIMIT 20;
|
||||
|
||||
-- External service calls (Stripe, Resend, NVIDIA AI)
|
||||
SELECT average(externalDuration), count(*)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
AND externalDuration > 0
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. LOAD TEST COMPARISON
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Compare metrics between two time windows (baseline vs test)
|
||||
-- Adjust SINCE/UNTIL for your test windows
|
||||
SELECT percentile(duration, 50, 95, 99), count(*), percentage(count(*), WHERE httpResponseCode >= 500)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
SINCE 2 hours ago
|
||||
UNTIL 1 hour ago
|
||||
COMPARE WITH 1 hour ago;
|
||||
|
||||
-- Per-endpoint comparison during load test window
|
||||
SELECT average(duration), percentile(duration, 95), count(*)
|
||||
FROM Transaction
|
||||
WHERE appName = 'HOALedgerIQ_App'
|
||||
FACET request.uri
|
||||
SINCE 1 hour ago
|
||||
LIMIT 50;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 9. INFRASTRUCTURE (if NR Infrastructure agent is installed)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- CPU utilization
|
||||
SELECT average(cpuPercent)
|
||||
FROM SystemSample
|
||||
WHERE hostname LIKE '%hoaledgeriq%'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Memory utilization
|
||||
SELECT average(memoryUsedPercent)
|
||||
FROM SystemSample
|
||||
WHERE hostname LIKE '%hoaledgeriq%'
|
||||
SINCE 1 hour ago
|
||||
TIMESERIES AUTO;
|
||||
|
||||
-- Connection pool saturation (custom metric – requires NR custom events)
|
||||
-- SELECT average(custom.db.pool.active), average(custom.db.pool.idle)
|
||||
-- FROM Metric
|
||||
-- WHERE appName = 'HOALedgerIQ_App'
|
||||
-- SINCE 1 hour ago
|
||||
-- TIMESERIES AUTO;
|
||||
Reference in New Issue
Block a user