UNPKG

straydog-js

Version:

Drop-in API monitoring for any Node.js backend

216 lines (215 loc) 9.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Api = void 0; const request_model_1 = require("../../config/data/request.model"); const response_model_1 = require("../../config/data/response.model"); const metrics_utils_1 = require("../../utils/metrics.utils"); const express_utils_1 = require("../../utils/express.utils"); /** * Sample response structure: * { "id": 5, "method": "GET", "path": "/zama", "query": "{}", "body": null, "headers": "[object Object]", "start_time": "2025-08-14T06:36:04.070Z", "request_id": 5, "end_time": "2025-08-14T06:36:04.088Z", "status_code": 304, "error": null, "latency": 24.9306, "error_stack": null }, */ class Api { constructor(days) { this.requestModel = new request_model_1.RequestModel('request'); this.responseModel = new response_model_1.ResponseModel('response'); this.days = days; } requests() { const requests = this.getRequests(); return { status: "OK", data: { requests, stats: this.getStats(requests), } }; } getErrorRequests() { const apiRequests = this.getRequests().filter(r => !this.isStaticFileOrIgnored(r.path)); return apiRequests.filter((r) => r.status_code >= 400); } getRequests() { const requestLogs = []; const responses = this.responseModel.getAll(); const requests = this.requestModel.getLastNDays('start_time', this.days); const responseMap = new Map(); for (const resp of responses) { responseMap.set(resp.request_id, Object.assign({}, resp)); } for (const req of requests) { const resp = responseMap.get(req.id); if (resp) { delete resp.id; requestLogs.push(Object.assign(Object.assign({}, req), resp)); } } return requestLogs; } // Helper function to check if request is a static file or should be ignored isStaticFileOrIgnored(path) { if (!path) return false; // Ignore Chrome DevTools requests if (path.includes('.well-known/appspecific/com.chrome.devtools')) return true; // Common static file extensions const staticExtensions = ['.js', '.css', '.html', '.htm', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.pdf', '.zip', '.txt']; const lowerPath = path.toLowerCase(); return staticExtensions.some(ext => lowerPath.endsWith(ext)); } /* Requests analytics are generated here. */ getStats(requestLogs = null) { if (!requestLogs) { requestLogs = this.getRequests(); } // Filter for API requests only (exclude static files and ignored requests) const apiRequests = requestLogs.filter(r => !this.isStaticFileOrIgnored(r.path)); // Filter out requests with zero latency (errors) from API requests const validLatencyRequests = apiRequests.filter(r => r.latency > 0); return { count: apiRequests.length, failedCount: apiRequests.filter((r) => r.status_code >= 400).length, // successCount: apiRequests.filter((r) => r.status_code < 400).length, averageLatency: validLatencyRequests.length > 0 ? (validLatencyRequests.reduce((acc, r) => acc + r.latency, 0) / validLatencyRequests.length).toFixed(2) : 0, successRate: apiRequests.length > 0 ? ((apiRequests.filter((r) => r.status_code < 400).length / apiRequests.length) * 100).toFixed(2) : 0, // failureRate: apiRequests.length > 0 ? ((apiRequests.filter((r) => r.status_code >= 400).length / apiRequests.length) * 100).toFixed(2) : 0, mostFailedEndpoint: (() => { const failuresByEndpoint = apiRequests.reduce((acc, r) => { const key = `${r.method} ${r.path}`; acc[key] = (acc[key] || 0) + (r.status_code >= 400 ? 1 : 0); return acc; }, {}); let maxEndpoint = null; let maxFailures = -1; for (const [endpoint, failures] of Object.entries(failuresByEndpoint)) { if (failures > maxFailures) { maxFailures = failures; maxEndpoint = endpoint; } } return maxEndpoint; })(), highestTraffic: (() => { const requestsByEndpoint = apiRequests.reduce((acc, r) => { const key = `${r.method} ${r.path}`; acc[key] = (acc[key] || 0) + 1; return acc; }, {}); let mostRequestedEndpoint = null; let maxRequests = -1; for (const [endpoint, count] of Object.entries(requestsByEndpoint)) { if (count > maxRequests) { maxRequests = count; mostRequestedEndpoint = endpoint; } } return mostRequestedEndpoint ? { endpoint: mostRequestedEndpoint, count: maxRequests } : null; })(), mostSuccessfulEndpoint: (() => { const successesByEndpoint = apiRequests.reduce((acc, r) => { const key = `${r.method} ${r.path}`; acc[key] = (acc[key] || 0) + (r.status_code < 400 ? 1 : 0); return acc; }, {}); let maxEndpoint = null; let maxSuccesses = -1; for (const [endpoint, successes] of Object.entries(successesByEndpoint)) { if (successes > maxSuccesses) { maxSuccesses = successes; maxEndpoint = endpoint; } } return maxEndpoint; })(), slowestEndpoint: (() => { const latencyByEndpoint = validLatencyRequests.reduce((acc, r) => { const key = `${r.method} ${r.path}`; if (!acc[key]) { acc[key] = { total: 0, count: 0 }; } acc[key].total += r.latency; acc[key].count += 1; return acc; }, {}); let slowestEndpoint = null; let maxAvgLatency = -1; for (const [endpoint, data] of Object.entries(latencyByEndpoint)) { const avgLatency = data.total / data.count; if (avgLatency > maxAvgLatency) { maxAvgLatency = avgLatency; slowestEndpoint = endpoint; } } return slowestEndpoint ? { endpoint: slowestEndpoint, averageLatency: maxAvgLatency.toFixed(2) } : null; })(), fastestEndpoint: (() => { const latencyByEndpoint = validLatencyRequests.reduce((acc, r) => { const key = `${r.method} ${r.path}`; if (!acc[key]) { acc[key] = { total: 0, count: 0 }; } acc[key].total += r.latency; acc[key].count += 1; return acc; }, {}); let fastestEndpoint = null; let minAvgLatency = Infinity; for (const [endpoint, data] of Object.entries(latencyByEndpoint)) { const avgLatency = data.total / data.count; if (avgLatency < minAvgLatency) { minAvgLatency = avgLatency; fastestEndpoint = endpoint; } } return fastestEndpoint ? { endpoint: fastestEndpoint, averageLatency: minAvgLatency.toFixed(2) } : null; })(), latencyStats: { average: validLatencyRequests.length > 0 ? validLatencyRequests.reduce((acc, r) => acc + r.latency, 0) / validLatencyRequests.length : 0, min: validLatencyRequests.length > 0 ? Math.min(...validLatencyRequests.map(r => r.latency)) : 0, max: validLatencyRequests.length > 0 ? Math.max(...validLatencyRequests.map(r => r.latency)) : 0, } }; } metrics() { return (0, metrics_utils_1.getMetrics)(); } getEndpoints(app, requests = undefined) { if (!requests) { requests = this.getRequests(); } let endpoints = (0, express_utils_1.expressListEndpoints)(app, ''); endpoints = endpoints.filter((e) => e.path != '/straydog/api'); return endpoints.map((e) => { return Object.assign(Object.assign({}, e), { total: requests.filter((r) => r.path == e.path).length, failed: requests.filter((r) => r.path == e.path && r.status_code >= 400).length, success: requests.filter((r) => r.path == e.path && r.status_code < 400).length }); }); } } exports.Api = Api;