#!/usr/bin/env node /** * metrics-coverage.js — Static analysis script for metrics/logging instrumentation coverage. * * Scans all Express route files in server/src/routes/*.js and app.js to measure * how many route handlers contain logging calls (console.error/console.warn/console.log). * * Usage: * node scripts/metrics-coverage.js # JSON output (default) * node scripts/metrics-coverage.js --format=text # Human-readable table * * Sample output (captured 2026-03-20): * { * "files": [ * { "file": "actuals.js", "total": 5, "logged": 5, "unlogged": 0, "coverage": 100 }, * { "file": "bills.js", "total": 6, "logged": 6, "unlogged": 0, "coverage": 100 }, * { "file": "config.js", "total": 2, "logged": 2, "unlogged": 0, "coverage": 100 }, * { "file": "financing.js", "total": 6, "logged": 6, "unlogged": 0, "coverage": 100 }, * { "file": "health.js", "total": 1, "logged": 0, "unlogged": 1, "coverage": 0 }, * { "file": "one-time-expenses.js", "total": 3, "logged": 3, "unlogged": 0, "coverage": 100 }, * { "file": "paychecks.js", "total": 6, "logged": 6, "unlogged": 0, "coverage": 100 }, * { "file": "summary.js", "total": 2, "logged": 2, "unlogged": 0, "coverage": 100 } * ], * "app": { * "has_request_timing_middleware": false, * "has_error_handling_middleware": false, * "middleware_count": 11 * }, * "aggregate": { * "total_handlers": 31, * "logged_handlers": 30, * "unlogged_handlers": 1, * "coverage_pct": 96.77 * } * } */ 'use strict'; const fs = require('fs'); const path = require('path'); const ROUTES_DIR = path.resolve(__dirname, '../server/src/routes'); const APP_FILE = path.resolve(__dirname, '../server/src/app.js'); // Regex patterns for route handler definitions. // Matches: router.get/post/put/patch/delete( and app.get/post/put/patch/delete( const ROUTE_DEF_RE = /\b(?:router|app)\.(get|post|put|patch|delete)\s*\(/g; // Logging call patterns const LOG_RE = /\bconsole\.(error|warn|log)\s*\(/; /** * Extract individual route handler bodies from source. * Strategy: find each route definition, then walk forward counting * braces to find the closing of the outermost async/function callback. */ function extractHandlerBodies(src) { const handlers = []; let match; ROUTE_DEF_RE.lastIndex = 0; while ((match = ROUTE_DEF_RE.exec(src)) !== null) { const startIdx = match.index; // Find the opening paren of the route call const parenOpen = src.indexOf('(', startIdx); if (parenOpen === -1) continue; // Walk from the paren open, tracking paren depth to find the matching close. // The handler callback body will be inside the outer parens. let depth = 0; let bodyStart = -1; let bodyEnd = -1; let inString = false; let stringChar = ''; let i = parenOpen; while (i < src.length) { const ch = src[i]; // Basic string tracking (skip contents of string literals) if (!inString && (ch === '"' || ch === "'" || ch === '`')) { inString = true; stringChar = ch; i++; continue; } if (inString) { if (ch === '\\') { i += 2; continue; } // skip escape if (ch === stringChar) inString = false; i++; continue; } if (ch === '(') { depth++; if (depth === 1) { // This is the opening of the route call args } } else if (ch === ')') { depth--; if (depth === 0) { bodyEnd = i; break; } } else if (ch === '{' && depth >= 1 && bodyStart === -1) { // First brace inside the outer parens — start of the handler body bodyStart = i; } i++; } if (bodyStart !== -1 && bodyEnd !== -1) { handlers.push(src.slice(bodyStart, bodyEnd)); } } return handlers; } /** * Analyse a single route file. */ function analyseRouteFile(filePath) { const src = fs.readFileSync(filePath, 'utf8'); const handlers = extractHandlerBodies(src); const logged = handlers.filter(body => LOG_RE.test(body)); return { file: path.basename(filePath), total: handlers.length, logged: logged.length, unlogged: handlers.length - logged.length, coverage: handlers.length === 0 ? null : Math.round((logged.length / handlers.length) * 10000) / 100, }; } /** * Analyse app.js for middleware-level instrumentation. */ function analyseApp(filePath) { const src = fs.readFileSync(filePath, 'utf8'); // Request timing: morgan, custom middleware checking req.method, Date.now() at top-level use() const hasRequestTiming = /\brequire\s*\(\s*['"]morgan['"]\s*\)/.test(src) || /app\.use\s*\(.*Date\.now\(\)/.test(src) || /app\.use\s*\(.*req,\s*res,\s*next/.test(src) && /Date\.now|performance\.now/.test(src); // Error handling middleware: app.use((err, req, res, next) => ...) const hasErrorHandling = /app\.use\s*\(\s*(?:\S+\s*,\s*)?\(\s*err\s*,/.test(src); // Count top-level app.use() calls (middleware registrations) const middlewareMatches = src.match(/app\.use\s*\(/g) || []; return { has_request_timing_middleware: hasRequestTiming, has_error_handling_middleware: hasErrorHandling, middleware_count: middlewareMatches.length, }; } function run() { const format = process.argv.includes('--format=text') ? 'text' : 'json'; // Analyse all route files const routeFiles = fs.readdirSync(ROUTES_DIR) .filter(f => f.endsWith('.js')) .sort(); const fileResults = routeFiles.map(f => analyseRouteFile(path.join(ROUTES_DIR, f)) ); // Aggregate const totalHandlers = fileResults.reduce((s, r) => s + r.total, 0); const loggedHandlers = fileResults.reduce((s, r) => s + r.logged, 0); const aggregate = { total_handlers: totalHandlers, logged_handlers: loggedHandlers, unlogged_handlers: totalHandlers - loggedHandlers, coverage_pct: totalHandlers === 0 ? null : Math.round((loggedHandlers / totalHandlers) * 10000) / 100, }; const appInfo = analyseApp(APP_FILE); const result = { files: fileResults, app: appInfo, aggregate, }; if (format === 'json') { console.log(JSON.stringify(result, null, 2)); return; } // Text table const COL_FILE = 28; const COL_TOTAL = 7; const COL_LOGGED = 8; const COL_COVER = 10; const pad = (s, n) => String(s).padEnd(n); const lpad = (s, n) => String(s).padStart(n); const hr = '-'.repeat(COL_FILE + COL_TOTAL + COL_LOGGED + COL_COVER + 6); console.log('\nMetrics Instrumentation Coverage\n'); console.log( pad('Route File', COL_FILE) + lpad('Handlers', COL_TOTAL) + lpad('Logged', COL_LOGGED) + lpad('Coverage', COL_COVER) ); console.log(hr); for (const r of fileResults) { const cov = r.coverage === null ? 'N/A' : `${r.coverage}%`; console.log( pad(r.file, COL_FILE) + lpad(r.total, COL_TOTAL) + lpad(r.logged, COL_LOGGED) + lpad(cov, COL_COVER) ); } console.log(hr); const aggCov = aggregate.coverage_pct === null ? 'N/A' : `${aggregate.coverage_pct}%`; console.log( pad('TOTAL', COL_FILE) + lpad(aggregate.total_handlers, COL_TOTAL) + lpad(aggregate.logged_handlers, COL_LOGGED) + lpad(aggCov, COL_COVER) ); console.log('\napp.js middleware:'); console.log(` Request timing middleware : ${appInfo.has_request_timing_middleware}`); console.log(` Error handling middleware : ${appInfo.has_error_handling_middleware}`); console.log(` app.use() call count : ${appInfo.middleware_count}`); console.log(''); } run();