express-performance-toolkit 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +119 -76
  2. package/dashboard-ui/README.md +73 -0
  3. package/dashboard-ui/eslint.config.js +23 -0
  4. package/dashboard-ui/index.html +13 -0
  5. package/dashboard-ui/package-lock.json +3382 -0
  6. package/dashboard-ui/package.json +32 -0
  7. package/dashboard-ui/src/App.css +184 -0
  8. package/dashboard-ui/src/App.tsx +182 -0
  9. package/dashboard-ui/src/components/BlockedModal.tsx +108 -0
  10. package/dashboard-ui/src/components/CachePanel.tsx +45 -0
  11. package/dashboard-ui/src/components/HealthCharts.tsx +142 -0
  12. package/dashboard-ui/src/components/InsightsPanel.tsx +49 -0
  13. package/dashboard-ui/src/components/KpiGrid.tsx +178 -0
  14. package/dashboard-ui/src/components/LiveLogs.tsx +76 -0
  15. package/dashboard-ui/src/components/Login.tsx +83 -0
  16. package/dashboard-ui/src/components/RoutesTable.tsx +110 -0
  17. package/dashboard-ui/src/hooks/useMetrics.ts +131 -0
  18. package/dashboard-ui/src/index.css +652 -0
  19. package/dashboard-ui/src/main.tsx +10 -0
  20. package/dashboard-ui/src/pages/InsightsPage.tsx +42 -0
  21. package/dashboard-ui/src/pages/LogsPage.tsx +26 -0
  22. package/dashboard-ui/src/pages/OverviewPage.tsx +32 -0
  23. package/dashboard-ui/src/pages/RoutesPage.tsx +26 -0
  24. package/dashboard-ui/src/utils/formatters.ts +27 -0
  25. package/dashboard-ui/tsconfig.app.json +28 -0
  26. package/dashboard-ui/tsconfig.json +7 -0
  27. package/dashboard-ui/tsconfig.node.json +26 -0
  28. package/dashboard-ui/vite.config.ts +12 -0
  29. package/dist/analyzer.d.ts +6 -0
  30. package/dist/analyzer.d.ts.map +1 -0
  31. package/dist/analyzer.js +70 -0
  32. package/dist/analyzer.js.map +1 -0
  33. package/dist/dashboard/dashboardRouter.d.ts +4 -4
  34. package/dist/dashboard/dashboardRouter.d.ts.map +1 -1
  35. package/dist/dashboard/dashboardRouter.js +67 -21
  36. package/dist/dashboard/dashboardRouter.js.map +1 -1
  37. package/dist/dashboard-ui/assets/index-CX-zE-Qy.css +1 -0
  38. package/dist/dashboard-ui/assets/index-Q9TGkd8n.js +41 -0
  39. package/dist/dashboard-ui/index.html +14 -0
  40. package/dist/index.d.ts +11 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +35 -11
  43. package/dist/index.js.map +1 -1
  44. package/dist/logger.d.ts +3 -3
  45. package/dist/logger.d.ts.map +1 -1
  46. package/dist/logger.js +167 -9
  47. package/dist/logger.js.map +1 -1
  48. package/dist/queryHelper.d.ts.map +1 -1
  49. package/dist/queryHelper.js +1 -0
  50. package/dist/queryHelper.js.map +1 -1
  51. package/dist/rateLimit.d.ts +5 -0
  52. package/dist/rateLimit.d.ts.map +1 -0
  53. package/dist/rateLimit.js +67 -0
  54. package/dist/rateLimit.js.map +1 -0
  55. package/dist/store.d.ts +9 -2
  56. package/dist/store.d.ts.map +1 -1
  57. package/dist/store.js +147 -25
  58. package/dist/store.js.map +1 -1
  59. package/dist/types.d.ts +93 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/example/server.ts +68 -37
  62. package/package.json +9 -6
  63. package/src/analyzer.ts +78 -0
  64. package/src/dashboard/dashboardRouter.ts +88 -23
  65. package/src/index.ts +70 -30
  66. package/src/logger.ts +177 -13
  67. package/src/queryHelper.ts +2 -0
  68. package/src/rateLimit.ts +86 -0
  69. package/src/store.ts +136 -27
  70. package/src/types.ts +98 -0
  71. package/tests/analyzer.test.ts +108 -0
  72. package/tests/auth.test.ts +79 -0
  73. package/tests/bandwidth.test.ts +72 -0
  74. package/tests/integration.test.ts +51 -54
  75. package/tests/rateLimit.test.ts +57 -0
  76. package/tests/store.test.ts +37 -18
  77. package/tsconfig.json +1 -0
  78. package/src/dashboard/dashboard.html +0 -756
@@ -0,0 +1,49 @@
1
+ import type { Insight } from "../hooks/useMetrics";
2
+ import { Lightbulb, AlertTriangle, AlertCircle, Info, ArrowRight } from "lucide-react";
3
+
4
+ export function InsightsPanel({ insights }: { insights: Insight[] }) {
5
+ if (!insights || insights.length === 0) {
6
+ return (
7
+ <div className="panel animate-in delay-3" style={{ marginTop: '20px' }}>
8
+ <div className="panel-header">
9
+ <div className="panel-title">✨ Smart Recommendations</div>
10
+ </div>
11
+ <div className="panel-body" style={{ textAlign: 'center', padding: '40px', color: 'var(--text-400)' }}>
12
+ <Lightbulb size={24} style={{ marginBottom: '12px', opacity: 0.5 }} />
13
+ <p>No performance issues detected. Everything looks optimal!</p>
14
+ </div>
15
+ </div>
16
+ );
17
+ }
18
+
19
+ return (
20
+ <div className="panel animate-in delay-3" style={{ marginTop: '20px' }}>
21
+ <div className="panel-header">
22
+ <div className="panel-title">✨ Smart Recommendations ({insights.length})</div>
23
+ </div>
24
+ <div className="panel-body" style={{ padding: '0px' }}>
25
+ <div className="insights-list">
26
+ {insights.map((insight, idx) => (
27
+ <div key={idx} className={`insight-item type-${insight.type}`}>
28
+ <div className="insight-icon">
29
+ {insight.type === 'error' && <AlertCircle size={18} className="val-rose" />}
30
+ {insight.type === 'warning' && <AlertTriangle size={18} className="val-amber" />}
31
+ {insight.type === 'info' && <Info size={18} className="val-emerald" />}
32
+ </div>
33
+ <div className="insight-content">
34
+ <div className="insight-title">{insight.title}</div>
35
+ <div className="insight-message">{insight.message}</div>
36
+ {insight.action && (
37
+ <div className="insight-action">
38
+ <ArrowRight size={12} style={{ marginRight: 6 }} />
39
+ {insight.action}
40
+ </div>
41
+ )}
42
+ </div>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,178 @@
1
+ import { Activity, Zap, AlertTriangle, Database, ShieldAlert, Server, HardDrive } from "lucide-react";
2
+ import type { MetricsData } from "../hooks/useMetrics";
3
+ import { fNum, fPct, fBytes } from "../utils/formatters";
4
+ import { useState } from "react";
5
+ import { BlockedModal } from "./BlockedModal";
6
+
7
+ export function KpiGrid({ data }: { data: MetricsData }) {
8
+ const [isModalOpen, setModalOpen] = useState(false);
9
+
10
+ const rps =
11
+ data.uptime > 0
12
+ ? (data.totalRequests / (data.uptime / 1000)).toFixed(1)
13
+ : "0";
14
+ return (
15
+ <div className="kpi-grid">
16
+ <div className="kpi-card grad-1 animate-in delay-1">
17
+ <div className="kpi-title">
18
+ <Activity
19
+ size={14}
20
+ style={{
21
+ display: "inline",
22
+ marginRight: 6,
23
+ verticalAlign: "text-bottom",
24
+ }}
25
+ />{" "}
26
+ Total Requests
27
+ </div>
28
+ <div className="kpi-value">{fNum(data.totalRequests)}</div>
29
+ <div className="kpi-subtext">{rps} req/s</div>
30
+ </div>
31
+ <div className="kpi-card grad-2 animate-in delay-2">
32
+ <div className="kpi-title">
33
+ <Zap
34
+ size={14}
35
+ style={{
36
+ display: "inline",
37
+ marginRight: 6,
38
+ verticalAlign: "text-bottom",
39
+ }}
40
+ />{" "}
41
+ Avg Response Time
42
+ </div>
43
+ <div className="kpi-value">
44
+ {data.avgResponseTime}
45
+ <span style={{ fontSize: "1.25rem", color: "var(--text-300)" }}>
46
+ ms
47
+ </span>
48
+ </div>
49
+ <div className="kpi-subtext" style={{ color: "var(--accent-emerald)" }}>
50
+ Optimal
51
+ </div>
52
+ </div>
53
+ <div className="kpi-card grad-4 animate-in delay-3">
54
+ <div className="kpi-title">
55
+ <AlertTriangle
56
+ size={14}
57
+ style={{
58
+ display: "inline",
59
+ marginRight: 6,
60
+ verticalAlign: "text-bottom",
61
+ }}
62
+ />{" "}
63
+ Slow Requests
64
+ </div>
65
+ <div className="kpi-value val-rose">{fNum(data.slowRequests)}</div>
66
+ <div className="kpi-subtext">
67
+ {fPct(data.slowRequests, data.totalRequests)}% of total
68
+ </div>
69
+ </div>
70
+ <div className="kpi-card grad-1 animate-in delay-4">
71
+ <div className="kpi-title">Cache Hit Rate</div>
72
+ <div className="kpi-value val-emerald">
73
+ {data.cacheHitRate}
74
+ <span style={{ fontSize: "1.25rem", color: "var(--text-300)" }}>
75
+ %
76
+ </span>
77
+ </div>
78
+ <div className="kpi-subtext">{data.cacheSize} entries active</div>
79
+ </div>
80
+ <div className="kpi-card grad-3 animate-in delay-5">
81
+ <div className="kpi-title">
82
+ <Database
83
+ size={14}
84
+ style={{
85
+ display: "inline",
86
+ marginRight: 6,
87
+ verticalAlign: "text-bottom",
88
+ }}
89
+ />{" "}
90
+ N+1 Query Alerts
91
+ </div>
92
+ <div className="kpi-value val-amber">
93
+ {fNum(data.highQueryRequests)}
94
+ </div>
95
+ <div className="kpi-subtext">
96
+ <span style={{ color: "var(--text-300)" }}>
97
+ {fPct(data.highQueryRequests, data.totalRequests)}% of total
98
+ </span>
99
+ </div>
100
+ </div>
101
+ <div className="kpi-card grad-1 animate-in delay-5">
102
+ <div className="kpi-title">
103
+ <Server
104
+ size={14}
105
+ style={{
106
+ display: "inline",
107
+ marginRight: 6,
108
+ verticalAlign: "text-bottom",
109
+ }}
110
+ />{" "}
111
+ Network Egress
112
+ </div>
113
+ <div className="kpi-value val-emerald">
114
+ {fBytes(data.totalBytesSent)}
115
+ </div>
116
+ <div className="kpi-subtext">Total bytes sent</div>
117
+ </div>
118
+
119
+ <div className="kpi-card grad-1 animate-in delay-6">
120
+ <div className="kpi-title">
121
+ <HardDrive
122
+ size={14}
123
+ style={{
124
+ display: "inline",
125
+ marginRight: 6,
126
+ verticalAlign: "text-bottom",
127
+ }}
128
+ />{" "}
129
+ Avg Payload
130
+ </div>
131
+ <div className="kpi-value">
132
+ {fBytes(data.avgResponseSize)}
133
+ </div>
134
+ <div className="kpi-subtext">Per request average</div>
135
+ </div>
136
+
137
+ <div className="kpi-card grad-4 animate-in delay-6" style={{ position: 'relative' }}>
138
+ <div className="kpi-title">
139
+ <ShieldAlert
140
+ size={14}
141
+ style={{
142
+ display: "inline",
143
+ marginRight: 6,
144
+ verticalAlign: "text-bottom",
145
+ }}
146
+ />{" "}
147
+ Blocked Traffic
148
+ </div>
149
+ <div className="kpi-value val-rose">{fNum(data.rateLimitHits)}</div>
150
+ <div className="kpi-subtext" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
151
+ <span>Rate limit 429</span>
152
+ {data.rateLimitHits > 0 && (
153
+ <button
154
+ onClick={() => setModalOpen(true)}
155
+ style={{
156
+ background: 'rgba(244, 63, 94, 0.1)',
157
+ border: '1px solid rgba(244, 63, 94, 0.2)',
158
+ borderRadius: '4px',
159
+ color: 'var(--accent-rose)',
160
+ padding: '2px 8px',
161
+ fontSize: '0.7rem',
162
+ cursor: 'pointer'
163
+ }}
164
+ >
165
+ View Details
166
+ </button>
167
+ )}
168
+ </div>
169
+ </div>
170
+
171
+ <BlockedModal
172
+ isOpen={isModalOpen}
173
+ onClose={() => setModalOpen(false)}
174
+ events={data.blockedEvents || []}
175
+ />
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,76 @@
1
+ import { useState } from 'react';
2
+ import type { LogEntry } from '../hooks/useMetrics';
3
+ import { fTime, getStatusClass, getTimeClass } from '../utils/formatters';
4
+
5
+ export function LiveLogs({ logs }: { logs: LogEntry[] }) {
6
+ const [filter, setFilter] = useState<'all' | 'slow' | 'cached' | 'errors'>('all');
7
+
8
+ let filtered = logs;
9
+ if (filter === "slow") filtered = logs.filter((l) => l.slow);
10
+ else if (filter === "cached") filtered = logs.filter((l) => l.cached);
11
+ else if (filter === "errors") filtered = logs.filter((l) => l.statusCode >= 400);
12
+
13
+ return (
14
+ <div className="panel animate-in delay-5">
15
+ <div className="panel-header">
16
+ <div className="panel-title">📋 Live Request Stream</div>
17
+ <div className="filters">
18
+ <button className={`filter-btn ${filter === 'all' ? 'active' : ''}`} onClick={() => setFilter('all')}>All</button>
19
+ <button className={`filter-btn ${filter === 'slow' ? 'active' : ''}`} onClick={() => setFilter('slow')}>
20
+ <span className="fire-icon">🔥</span> Slow
21
+ </button>
22
+ <button className={`filter-btn ${filter === 'cached' ? 'active' : ''}`} onClick={() => setFilter('cached')}>Cached</button>
23
+ <button className={`filter-btn ${filter === 'errors' ? 'active' : ''}`} onClick={() => setFilter('errors')}>Errors</button>
24
+ </div>
25
+ </div>
26
+ <div className="panel-body" style={{ padding: 0, maxHeight: "500px" }}>
27
+ <div className="table-container">
28
+ <table>
29
+ <thead>
30
+ <tr>
31
+ <th>Timestamp</th>
32
+ <th>Method</th>
33
+ <th style={{ width: "40%" }}>Path</th>
34
+ <th>Status</th>
35
+ <th>LATENCY</th>
36
+ <th>Cache</th>
37
+ <th>Flags</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ {filtered.length === 0 ? (
42
+ <tr>
43
+ <td colSpan={7} className="empty-state">
44
+ <div className="empty-icon">✨</div><p>No matching requests found</p>
45
+ </td>
46
+ </tr>
47
+ ) : (
48
+ filtered.sort((a,b) => b.timestamp - a.timestamp).map((log, i) => {
49
+ const flag = log.slow
50
+ ? <span className="fire-icon" title="Slow Request">🔥</span>
51
+ : log.highQueries
52
+ ? <span style={{color:"var(--accent-amber)"}} title="High DB Queries">⚠️</span>
53
+ : "-";
54
+
55
+ return (
56
+ <tr key={i} style={log.slow ? { background: "rgba(244,63,94,0.03)" } : {}}>
57
+ <td style={{ color: "var(--text-300)", fontFamily: "var(--font-mono)" }}>{fTime(log.timestamp)}</td>
58
+ <td><span className={`badge badge-${log.method}`}>{log.method}</span></td>
59
+ <td className="route-path">{log.path}</td>
60
+ <td className={`status-code ${getStatusClass(log.statusCode)}`} style={{ textAlign: "left" }}>{log.statusCode}</td>
61
+ <td className={getTimeClass(log.responseTime)} style={{ fontWeight: 600 }}>{log.responseTime}ms</td>
62
+ <td>
63
+ {log.cached ? <span className="badge cache-hit">HIT</span> : <span className="badge cache-miss">MISS</span>}
64
+ </td>
65
+ <td>{flag}</td>
66
+ </tr>
67
+ )
68
+ })
69
+ )}
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,83 @@
1
+ import React, { useState } from "react";
2
+ import { Zap, Lock, User, Loader2 } from "lucide-react";
3
+
4
+ interface LoginProps {
5
+ onLoginSuccess: () => void;
6
+ }
7
+
8
+ export function Login({ onLoginSuccess }: LoginProps) {
9
+ const [username, setUsername] = useState("");
10
+ const [password, setPassword] = useState("");
11
+ const [error, setError] = useState("");
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setLoading(true);
17
+ setError("");
18
+
19
+ try {
20
+ const resp = await fetch("./api/login", {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({ username, password }),
24
+ });
25
+
26
+ const data = await resp.json();
27
+
28
+ if (data.success) {
29
+ onLoginSuccess();
30
+ } else {
31
+ setError(data.message || "Invalid credentials");
32
+ }
33
+ } catch (err) {
34
+ setError("Failed to connect to server");
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="login-wrapper animate-in">
42
+ <div className="login-card panel">
43
+ <div className="login-header">
44
+ <div className="brand-icon" style={{ margin: '0 auto 1.5rem', width: '48px', height: '48px' }}>
45
+ <Zap size={24} />
46
+ </div>
47
+ <h1>Dashboard Access</h1>
48
+ <p>Please enter your credentials to continue</p>
49
+ </div>
50
+
51
+ <form onSubmit={handleSubmit} className="login-form">
52
+ <div className="input-group">
53
+ <User size={18} className="input-icon" />
54
+ <input
55
+ type="text"
56
+ placeholder="Username"
57
+ value={username}
58
+ onChange={(e) => setUsername(e.target.value)}
59
+ required
60
+ />
61
+ </div>
62
+
63
+ <div className="input-group">
64
+ <Lock size={18} className="input-icon" />
65
+ <input
66
+ type="password"
67
+ placeholder="Password"
68
+ value={password}
69
+ onChange={(e) => setPassword(e.target.value)}
70
+ required
71
+ />
72
+ </div>
73
+
74
+ {error && <div className="login-error">{error}</div>}
75
+
76
+ <button type="submit" className="login-button" disabled={loading}>
77
+ {loading ? <Loader2 className="animate-spin" size={20} /> : "Unlock Dashboard"}
78
+ </button>
79
+ </form>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,110 @@
1
+ import type { RouteStats } from "../hooks/useMetrics";
2
+ import { fNum, getTimeClass, fBytes } from "../utils/formatters";
3
+
4
+ export function RoutesTable({
5
+ routes,
6
+ }: {
7
+ routes: Record<string, RouteStats>;
8
+ }) {
9
+ const routeEntries = Object.entries(routes)
10
+ .sort(([, a], [, b]) => b.avgTime - a.avgTime)
11
+ .slice(0, 8); // top 8 slowest
12
+
13
+ return (
14
+ <div className="panel animate-in delay-2" style={{ maxHeight: "500px" }}>
15
+ <div className="panel-header">
16
+ <div className="panel-title">📉 Route Performance</div>
17
+ </div>
18
+ <div className="panel-body" style={{ padding: 0 }}>
19
+ <div className="table-container">
20
+ <table className="routes-table">
21
+ <thead>
22
+ <tr>
23
+ <th>Route Path</th>
24
+ <th>Calls</th>
25
+ <th>Avg Latency</th>
26
+ <th>Payload</th>
27
+ <th>Anomalies</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {routeEntries.length === 0 ? (
32
+ <tr>
33
+ <td colSpan={5} className="empty-state">
34
+ No route data collected yet.
35
+ </td>
36
+ </tr>
37
+ ) : (
38
+ routeEntries.map(([route, stats]) => {
39
+ const anomalies = [];
40
+ if (stats.slowCount > 0)
41
+ anomalies.push(
42
+ <span key="slow">
43
+ <span className="fire-icon">🔥</span>{" "}
44
+ <span style={{ color: "var(--accent-rose)" }}>
45
+ {stats.slowCount} Slow
46
+ </span>
47
+ </span>,
48
+ );
49
+ if (stats.highQueryCount > 0)
50
+ anomalies.push(
51
+ <span key="n1">
52
+ <span style={{ color: "var(--accent-amber)" }}>⚠️</span>{" "}
53
+ <span style={{ color: "var(--accent-amber)" }}>
54
+ {stats.highQueryCount} N+1
55
+ </span>
56
+ </span>,
57
+ );
58
+
59
+ return (
60
+ <tr key={route}>
61
+ <td className="route-path">{route}</td>
62
+ <td style={{ color: "var(--text-300)" }}>
63
+ {fNum(stats.count)}
64
+ </td>
65
+ <td className={getTimeClass(stats.avgTime)}>
66
+ {stats.avgTime}ms
67
+ </td>
68
+ <td style={{ color: "var(--text-300)" }}>
69
+ {fBytes(stats.avgSize)}
70
+ </td>
71
+ <td>
72
+ {anomalies.length > 0 ? (
73
+ <div
74
+ style={{
75
+ display: "flex",
76
+ gap: "8px",
77
+ alignItems: "center",
78
+ }}
79
+ >
80
+ {anomalies.map((a, i) => (
81
+ <span key={i}>
82
+ {i > 0 && (
83
+ <span
84
+ style={{
85
+ color: "var(--border)",
86
+ margin: "0 4px",
87
+ }}
88
+ >
89
+ |
90
+ </span>
91
+ )}
92
+ {a}
93
+ </span>
94
+ ))}
95
+ </div>
96
+ ) : (
97
+ <span style={{ color: "var(--text-400)" }}>-</span>
98
+ )}
99
+ </td>
100
+ </tr>
101
+ );
102
+ })
103
+ )}
104
+ </tbody>
105
+ </table>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,131 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export interface RouteStats {
4
+ count: number;
5
+ totalTime: number;
6
+ slowCount: number;
7
+ highQueryCount: number;
8
+ rateLimitHits: number;
9
+ avgTime: number;
10
+ totalBytes: number;
11
+ avgSize: number;
12
+ }
13
+
14
+ export interface BlockedEvent {
15
+ ip: string;
16
+ path: string;
17
+ timestamp: number;
18
+ method: string;
19
+ }
20
+
21
+ export interface LogEntry {
22
+ method: string;
23
+ path: string;
24
+ statusCode: number;
25
+ responseTime: number;
26
+ timestamp: number;
27
+ slow: boolean;
28
+ cached: boolean;
29
+ queryCount?: number;
30
+ highQueries?: boolean;
31
+ }
32
+
33
+ export interface Insight {
34
+ type: "info" | "warning" | "error";
35
+ title: string;
36
+ message: string;
37
+ action?: string;
38
+ }
39
+
40
+ export interface HistoryData {
41
+ time: string;
42
+ lag: number;
43
+ memory: number;
44
+ }
45
+
46
+ export interface MetricsData {
47
+ uptime: number;
48
+ totalRequests: number;
49
+ avgResponseTime: number;
50
+ slowRequests: number;
51
+ highQueryRequests: number;
52
+ rateLimitHits: number;
53
+ cacheHits: number;
54
+ cacheMisses: number;
55
+ cacheHitRate: number;
56
+ cacheSize: number;
57
+ totalBytesSent: number;
58
+ avgResponseSize: number;
59
+ insights: Insight[];
60
+ eventLoopLag: number;
61
+ memoryUsage: {
62
+ rss: number;
63
+ heapTotal: number;
64
+ heapUsed: number;
65
+ external: number;
66
+ };
67
+ statusCodes: Record<number, number>;
68
+ routes: Record<string, RouteStats>;
69
+ recentLogs: LogEntry[];
70
+ blockedEvents: BlockedEvent[];
71
+ }
72
+
73
+ export function useMetrics(enabled: boolean = true) {
74
+ const [data, setData] = useState<MetricsData | null>(null);
75
+ const [history, setHistory] = useState<HistoryData[]>([]);
76
+ const [error, setError] = useState<Error | null>(null);
77
+
78
+ useEffect(() => {
79
+ if (!enabled) return;
80
+
81
+ let mounted = true;
82
+
83
+ const fetchMetrics = async () => {
84
+ try {
85
+ const response = await fetch("./api/metrics");
86
+ if (!response.ok) {
87
+ if (response.status === 401) {
88
+ throw new Error("unauthorized");
89
+ }
90
+ throw new Error("Failed to fetch metrics");
91
+ }
92
+ const metrics: MetricsData = await response.json();
93
+
94
+ if (mounted) {
95
+ setData(metrics);
96
+ setError(null);
97
+
98
+ // Append to history (keep last 50 data points)
99
+ setHistory((prev) => {
100
+ const timeStr = new Date().toLocaleTimeString([], {
101
+ hour12: false,
102
+ hour: "2-digit",
103
+ minute: "2-digit",
104
+ second: "2-digit",
105
+ });
106
+ const newPoint = {
107
+ time: timeStr,
108
+ lag: metrics.eventLoopLag,
109
+ memory: Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024),
110
+ };
111
+ return [...prev, newPoint].slice(-30);
112
+ });
113
+ }
114
+ } catch (e: unknown) {
115
+ if (mounted) {
116
+ const errorObj = e instanceof Error ? e : new Error(String(e));
117
+ setError(errorObj);
118
+ }
119
+ }
120
+ };
121
+
122
+ fetchMetrics();
123
+ const id = setInterval(fetchMetrics, 3000);
124
+ return () => {
125
+ mounted = false;
126
+ clearInterval(id);
127
+ };
128
+ }, [enabled]);
129
+
130
+ return { data, history, error };
131
+ }