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,86 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { MetricsStore } from "./store";
3
+ import { RateLimitOptions } from "./types";
4
+
5
+ interface RateLimitTracker {
6
+ count: number;
7
+ resetTime: number;
8
+ }
9
+
10
+ export function createRateLimiter(
11
+ store: MetricsStore,
12
+ options: RateLimitOptions | boolean | undefined
13
+ ) {
14
+ const enabled = typeof options === "boolean" ? options : options?.enabled !== false;
15
+ if (!enabled) {
16
+ return (req: Request, res: Response, next: NextFunction) => next();
17
+ }
18
+
19
+ const opts = typeof options === "object" ? options : {};
20
+ const windowMs = opts.windowMs || 60000;
21
+ const max = opts.max || 100;
22
+ const statusCode = opts.statusCode || 429;
23
+ const message = opts.message || "Too many requests, please try again later.";
24
+
25
+ // In-memory store mapping IP addresses to tracking objects
26
+ const hits = new Map<string, RateLimitTracker>();
27
+
28
+ // Cleanup interval to prevent memory leaks from inactive IPs
29
+ setInterval(() => {
30
+ const now = Date.now();
31
+ for (const [ip, tracker] of hits.entries()) {
32
+ if (now > tracker.resetTime) {
33
+ hits.delete(ip);
34
+ }
35
+ }
36
+ }, windowMs).unref(); // .unref() ensures this interval doesn't keep the Node process alive
37
+
38
+ const exclude = opts.exclude || [];
39
+
40
+ return (req: Request, res: Response, next: NextFunction) => {
41
+ // Check exclusions
42
+ const path = req.path;
43
+ const isExcluded = exclude.some((pattern) => {
44
+ if (typeof pattern === "string") return path.startsWith(pattern);
45
+ if (pattern instanceof RegExp) return pattern.test(path);
46
+ return false;
47
+ });
48
+
49
+ if (isExcluded) return next();
50
+
51
+ const ip = req.ip || req.connection.remoteAddress || "unknown";
52
+ const now = Date.now();
53
+ const routeKey = `${req.method} ${req.route ? req.route.path : req.path}`;
54
+
55
+ let tracker = hits.get(ip);
56
+
57
+ if (!tracker || now > tracker.resetTime) {
58
+ // Create new tracker or reset expired tracker
59
+ tracker = {
60
+ count: 1,
61
+ resetTime: now + windowMs,
62
+ };
63
+ hits.set(ip, tracker);
64
+ return next();
65
+ }
66
+
67
+ tracker.count++;
68
+
69
+ if (tracker.count > max) {
70
+ store.recordRateLimitHit(routeKey, ip, req.method, req.path);
71
+
72
+ res.setHeader("Retry-After", Math.ceil((tracker.resetTime - now) / 1000));
73
+ res.setHeader("X-RateLimit-Limit", max);
74
+ res.setHeader("X-RateLimit-Remaining", 0);
75
+ res.setHeader("X-RateLimit-Reset", Math.ceil(tracker.resetTime / 1000));
76
+
77
+ res.status(statusCode).send(message);
78
+ return;
79
+ }
80
+
81
+ res.setHeader("X-RateLimit-Limit", max);
82
+ res.setHeader("X-RateLimit-Remaining", max - tracker.count);
83
+ res.setHeader("X-RateLimit-Reset", Math.ceil(tracker.resetTime / 1000));
84
+ next();
85
+ };
86
+ }
package/src/store.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { LogEntry, Metrics, RouteStats } from './types';
1
+ import { LogEntry, Metrics, RouteStats, BlockedEvent } from "./types";
2
+ import { monitorEventLoopDelay } from "perf_hooks";
3
+ import { analyzeMetrics } from "./analyzer";
4
+ import * as v8 from "v8";
2
5
 
3
6
  /**
4
7
  * In-memory metrics store — shared state between all middleware components.
@@ -6,13 +9,19 @@ import { LogEntry, Metrics, RouteStats } from './types';
6
9
  */
7
10
  export class MetricsStore {
8
11
  private maxLogs: number;
12
+ private maxRoutes: number = 200; // Cap unique routes to prevent memory leaks
9
13
  private logs: LogEntry[];
14
+ private blockedEvents: BlockedEvent[] = [];
15
+ private histogram: ReturnType<typeof monitorEventLoopDelay>;
10
16
  private stats: {
11
17
  totalRequests: number;
12
18
  totalResponseTime: number;
13
19
  slowRequests: number;
20
+ highQueryRequests: number;
21
+ rateLimitHits: number;
14
22
  cacheHits: number;
15
23
  cacheMisses: number;
24
+ totalBytesSent: number;
16
25
  cacheSize: number;
17
26
  statusCodes: Record<number, number>;
18
27
  routes: Record<string, RouteStats>;
@@ -26,21 +35,23 @@ export class MetricsStore {
26
35
  totalRequests: 0,
27
36
  totalResponseTime: 0,
28
37
  slowRequests: 0,
38
+ highQueryRequests: 0,
39
+ rateLimitHits: 0,
29
40
  cacheHits: 0,
30
41
  cacheMisses: 0,
42
+ totalBytesSent: 0,
31
43
  cacheSize: 0,
32
44
  statusCodes: {},
33
45
  routes: {},
34
46
  startTime: Date.now(),
35
47
  };
48
+ this.histogram = monitorEventLoopDelay({ resolution: 10 });
49
+ this.histogram.enable();
36
50
  }
37
51
 
38
52
  /** Add a request log entry to the ring buffer. */
39
- addLog(entry: LogEntry): void {
40
- this.logs.push({
41
- ...entry,
42
- timestamp: entry.timestamp || Date.now(),
43
- });
53
+ recordLog(log: LogEntry): void {
54
+ this.logs.push(log);
44
55
 
45
56
  // Ring buffer — drop oldest entries when full
46
57
  if (this.logs.length > this.maxLogs) {
@@ -49,35 +60,101 @@ export class MetricsStore {
49
60
 
50
61
  // Update aggregate stats
51
62
  this.stats.totalRequests++;
52
- this.stats.totalResponseTime += entry.responseTime || 0;
63
+ this.stats.totalResponseTime += log.responseTime || 0;
53
64
 
54
65
  // Track status codes
55
- const code = entry.statusCode || 0;
56
- this.stats.statusCodes[code] = (this.stats.statusCodes[code] || 0) + 1;
66
+ this.stats.totalBytesSent += log.bytesSent;
57
67
 
58
68
  // Track per-route stats
59
- const routeKey = `${entry.method} ${entry.path}`;
69
+ // Use normalized path (routePattern) if available, fallback to actual path
70
+ const routeKey = `${log.method} ${log.routePattern || log.path}`;
71
+
60
72
  if (!this.stats.routes[routeKey]) {
61
- this.stats.routes[routeKey] = {
62
- count: 0,
63
- totalTime: 0,
64
- slowCount: 0,
65
- avgTime: 0,
66
- };
73
+ // Prevent unbounded growth of the routes map
74
+ if (Object.keys(this.stats.routes).length >= this.maxRoutes) {
75
+ // Fallback to a catch-all for excessive new routes
76
+ const othersKey = `${log.method} [Other]`;
77
+ if (!this.stats.routes[othersKey]) {
78
+ this.stats.routes[othersKey] = this.createNewRouteStats();
79
+ }
80
+ this.updateRouteStats(this.stats.routes[othersKey], log);
81
+ } else {
82
+ this.stats.routes[routeKey] = this.createNewRouteStats();
83
+ this.updateRouteStats(this.stats.routes[routeKey], log);
84
+ }
85
+ } else {
86
+ this.updateRouteStats(this.stats.routes[routeKey], log);
67
87
  }
68
- const route = this.stats.routes[routeKey];
88
+
89
+ const code = log.statusCode;
90
+ this.stats.statusCodes[code] = (this.stats.statusCodes[code] || 0) + 1;
91
+ }
92
+
93
+ private createNewRouteStats(): RouteStats {
94
+ return {
95
+ count: 0,
96
+ totalTime: 0,
97
+ slowCount: 0,
98
+ highQueryCount: 0,
99
+ rateLimitHits: 0,
100
+ avgTime: 0,
101
+ totalBytes: 0,
102
+ avgSize: 0,
103
+ };
104
+ }
105
+
106
+ private updateRouteStats(route: RouteStats, log: LogEntry): void {
69
107
  route.count++;
70
- route.totalTime += entry.responseTime || 0;
108
+ route.totalTime += log.responseTime;
109
+ route.totalBytes += log.bytesSent;
71
110
  route.avgTime = Math.round(route.totalTime / route.count);
72
- if (entry.slow) {
111
+ route.avgSize = Math.round(route.totalBytes / route.count);
112
+
113
+ if (log.slow) {
114
+ this.stats.slowRequests++;
73
115
  route.slowCount++;
74
116
  }
117
+
118
+ if (log.highQueries) {
119
+ this.stats.highQueryRequests++;
120
+ route.highQueryCount++;
121
+ }
75
122
  }
76
123
 
77
124
  recordSlowRequest(): void {
78
125
  this.stats.slowRequests++;
79
126
  }
80
127
 
128
+ recordRateLimitHit(
129
+ routeKey: string,
130
+ ip: string,
131
+ method: string,
132
+ path: string,
133
+ ): void {
134
+ this.stats.rateLimitHits++;
135
+
136
+ // Track blocked event details
137
+ this.blockedEvents.push({
138
+ ip,
139
+ path,
140
+ method,
141
+ timestamp: Date.now(),
142
+ });
143
+
144
+ // Keep only last 100 blocked events
145
+ if (this.blockedEvents.length > 100) {
146
+ this.blockedEvents.shift();
147
+ }
148
+
149
+ if (!this.stats.routes[routeKey]) {
150
+ if (Object.keys(this.stats.routes).length >= this.maxRoutes) {
151
+ return; // Don't track rate limits for new routes if at capacity
152
+ }
153
+ this.stats.routes[routeKey] = this.createNewRouteStats();
154
+ }
155
+ this.stats.routes[routeKey].rateLimitHits++;
156
+ }
157
+
81
158
  recordCacheHit(): void {
82
159
  this.stats.cacheHits++;
83
160
  }
@@ -90,6 +167,13 @@ export class MetricsStore {
90
167
  this.stats.cacheSize = size;
91
168
  }
92
169
 
170
+ private calculateCacheHitRate(): number {
171
+ const cacheTotal = this.stats.cacheHits + this.stats.cacheMisses;
172
+ return cacheTotal > 0
173
+ ? Math.round((this.stats.cacheHits / cacheTotal) * 100)
174
+ : 0;
175
+ }
176
+
93
177
  /** Get all metrics data for the dashboard. */
94
178
  getMetrics(): Metrics {
95
179
  const avgResponseTime =
@@ -97,25 +181,44 @@ export class MetricsStore {
97
181
  ? Math.round(this.stats.totalResponseTime / this.stats.totalRequests)
98
182
  : 0;
99
183
 
100
- const cacheTotal = this.stats.cacheHits + this.stats.cacheMisses;
101
- const cacheHitRate =
102
- cacheTotal > 0
103
- ? Math.round((this.stats.cacheHits / cacheTotal) * 100)
104
- : 0;
184
+ const mem = process.memoryUsage();
185
+ const heapStats = v8.getHeapStatistics();
105
186
 
106
- return {
187
+ const metrics: Metrics = {
107
188
  uptime: Date.now() - this.stats.startTime,
108
189
  totalRequests: this.stats.totalRequests,
109
190
  avgResponseTime,
110
191
  slowRequests: this.stats.slowRequests,
192
+ highQueryRequests: this.stats.highQueryRequests,
193
+ rateLimitHits: this.stats.rateLimitHits,
111
194
  cacheHits: this.stats.cacheHits,
112
195
  cacheMisses: this.stats.cacheMisses,
113
- cacheHitRate,
114
- cacheSize: this.stats.cacheSize,
196
+ cacheHitRate: this.calculateCacheHitRate(),
197
+ cacheSize: 0, // Will be populated in index.ts if cache is enabled
198
+ totalBytesSent: this.stats.totalBytesSent,
199
+ avgResponseSize:
200
+ this.stats.totalRequests > 0
201
+ ? Math.round(this.stats.totalBytesSent / this.stats.totalRequests)
202
+ : 0,
203
+ insights: [], // Placeholder, filled below
204
+ eventLoopLag: Math.round(this.histogram.mean / 1e6),
205
+ memoryUsage: {
206
+ rss: mem.rss,
207
+ heapTotal: mem.heapTotal,
208
+ heapUsed: mem.heapUsed,
209
+ heapLimit: heapStats.heap_size_limit,
210
+ external: mem.external,
211
+ },
115
212
  statusCodes: { ...this.stats.statusCodes },
116
213
  routes: { ...this.stats.routes },
117
214
  recentLogs: this.logs.slice(-100),
215
+ blockedEvents: [...this.blockedEvents],
118
216
  };
217
+
218
+ // Generate insights
219
+ metrics.insights = analyzeMetrics(metrics);
220
+
221
+ return metrics;
119
222
  }
120
223
 
121
224
  /** Reset all metrics. */
@@ -124,11 +227,17 @@ export class MetricsStore {
124
227
  this.stats.totalRequests = 0;
125
228
  this.stats.totalResponseTime = 0;
126
229
  this.stats.slowRequests = 0;
230
+ this.stats.highQueryRequests = 0;
231
+ this.stats.rateLimitHits = 0;
127
232
  this.stats.cacheHits = 0;
128
233
  this.stats.cacheMisses = 0;
129
234
  this.stats.cacheSize = 0;
130
235
  this.stats.statusCodes = {};
131
236
  this.stats.routes = {};
132
237
  this.stats.startTime = Date.now();
238
+ this.blockedEvents = [];
239
+ this.histogram.disable();
240
+ this.histogram = monitorEventLoopDelay({ resolution: 10 });
241
+ this.histogram.enable();
133
242
  }
134
243
  }
package/src/types.ts CHANGED
@@ -43,6 +43,12 @@ export interface LoggerOptions {
43
43
  slowThreshold?: number;
44
44
  /** Log to console (default: true) */
45
45
  console?: boolean;
46
+ /** Log to file path (optional) */
47
+ file?: string;
48
+ /** Enable automatic daily log rotation (appends YYYY-MM-DD to filename) (default: false) */
49
+ rotation?: boolean;
50
+ /** Delete log files older than this many days (default: 7). Requires rotation: true */
51
+ maxDays?: number;
46
52
  /** Custom log formatter */
47
53
  formatter?: (entry: LogEntry) => string;
48
54
  }
@@ -54,11 +60,37 @@ export interface QueryHelperOptions {
54
60
  threshold?: number;
55
61
  }
56
62
 
63
+ export interface DashboardAuthOptions {
64
+ /** Admin username (default: 'admin') */
65
+ username?: string;
66
+ /** Admin password (default: 'perf-toolkit') */
67
+ password?: string;
68
+ /** Secret key for session cookie (default: 'toolkit-secret') */
69
+ secret?: string;
70
+ }
71
+
57
72
  export interface DashboardOptions {
58
73
  /** Enable dashboard (default: true) */
59
74
  enabled?: boolean;
60
75
  /** Dashboard mount path (default: '/__perf') */
61
76
  path?: string;
77
+ /** Authentication settings. If provided, user must login to see dashboard. */
78
+ auth?: DashboardAuthOptions;
79
+ }
80
+
81
+ export interface RateLimitOptions {
82
+ /** Enable rate limiting (default: false) */
83
+ enabled?: boolean;
84
+ /** Time window in milliseconds (default: 60000 - 1 minute) */
85
+ windowMs?: number;
86
+ /** Maximum number of requests per windowMs (default: 100) */
87
+ max?: number;
88
+ /** Response status code (default: 429) */
89
+ statusCode?: number;
90
+ /** Response message string or object */
91
+ message?: string | object;
92
+ /** URL patterns to exclude from rate limiting (e.g. ['/__perf*']) */
93
+ exclude?: (string | RegExp)[];
62
94
  }
63
95
 
64
96
  export interface ToolkitOptions {
@@ -72,6 +104,8 @@ export interface ToolkitOptions {
72
104
  queryHelper?: boolean | QueryHelperOptions;
73
105
  /** Performance dashboard */
74
106
  dashboard?: boolean | DashboardOptions;
107
+ /** Rate limiting */
108
+ rateLimit?: boolean | RateLimitOptions;
75
109
  /** Max log entries to keep in memory (default: 1000) */
76
110
  maxLogs?: number;
77
111
  }
@@ -81,22 +115,71 @@ export interface ToolkitOptions {
81
115
  export interface LogEntry {
82
116
  method: string;
83
117
  path: string;
118
+ routePattern?: string;
84
119
  statusCode: number;
85
120
  responseTime: number;
86
121
  timestamp: number;
122
+ bytesSent: number;
87
123
  slow: boolean;
88
124
  cached: boolean;
89
125
  queryCount?: number;
126
+ highQueries?: boolean;
90
127
  contentLength?: number;
91
128
  userAgent?: string;
92
129
  ip?: string;
93
130
  }
94
131
 
132
+ export interface BlockedEvent {
133
+ ip: string;
134
+ path: string;
135
+ timestamp: number;
136
+ method: string;
137
+ }
138
+
95
139
  export interface RouteStats {
96
140
  count: number;
97
141
  totalTime: number;
98
142
  slowCount: number;
143
+ highQueryCount: number;
144
+ rateLimitHits: number;
99
145
  avgTime: number;
146
+ totalBytes: number;
147
+ avgSize: number;
148
+ }
149
+
150
+ export interface Insight {
151
+ type: "info" | "warning" | "error";
152
+ title: string;
153
+ message: string;
154
+ action?: string;
155
+ }
156
+
157
+ export interface MetricSnapshot {
158
+ uptime: number;
159
+ totalRequests: number;
160
+ avgResponseTime: number;
161
+ slowRequests: number;
162
+ highQueryRequests: number;
163
+ rateLimitHits: number;
164
+ cacheHits: number;
165
+ cacheMisses: number;
166
+ cacheHitRate: number;
167
+ cacheSize: number;
168
+ totalBytesSent: number;
169
+ avgResponseSize: number;
170
+ insights: Insight[];
171
+ eventLoopLag: number;
172
+ memoryUsage: {
173
+ rss: number;
174
+ heapTotal: number;
175
+ heapUsed: number;
176
+ heapLimit: number; // Added v8 heap limit
177
+ external: number;
178
+ };
179
+ statusCodes: Record<number, number>;
180
+ routes: Record<string, RouteStats>;
181
+ recentLogs: LogEntry[];
182
+ blockedEvents: BlockedEvent[];
100
183
  }
101
184
 
102
185
  export interface Metrics {
@@ -104,13 +187,27 @@ export interface Metrics {
104
187
  totalRequests: number;
105
188
  avgResponseTime: number;
106
189
  slowRequests: number;
190
+ highQueryRequests: number;
191
+ rateLimitHits: number;
107
192
  cacheHits: number;
108
193
  cacheMisses: number;
109
194
  cacheHitRate: number;
110
195
  cacheSize: number;
196
+ totalBytesSent: number;
197
+ avgResponseSize: number;
198
+ insights: Insight[];
199
+ eventLoopLag: number;
200
+ memoryUsage: {
201
+ rss: number;
202
+ heapTotal: number;
203
+ heapUsed: number;
204
+ heapLimit: number; // Added v8 heap limit
205
+ external: number;
206
+ };
111
207
  statusCodes: Record<number, number>;
112
208
  routes: Record<string, RouteStats>;
113
209
  recentLogs: LogEntry[];
210
+ blockedEvents: BlockedEvent[];
114
211
  }
115
212
 
116
213
  export interface CacheEntry {
@@ -148,6 +245,7 @@ declare global {
148
245
  perfToolkit?: {
149
246
  startTime: number;
150
247
  queryCount: number;
248
+ highQueries?: boolean;
151
249
  trackQuery: (label?: string) => void;
152
250
  };
153
251
  }
@@ -0,0 +1,108 @@
1
+ import { analyzeMetrics } from "../src/analyzer";
2
+ import { Metrics } from "../src/types";
3
+
4
+ describe("Smart Insights Engine", () => {
5
+ const mockMetrics: Metrics = {
6
+ uptime: 1000,
7
+ totalRequests: 10,
8
+ avgResponseTime: 100,
9
+ slowRequests: 0,
10
+ highQueryRequests: 0,
11
+ rateLimitHits: 0,
12
+ cacheHits: 0,
13
+ cacheMisses: 10,
14
+ cacheHitRate: 0,
15
+ cacheSize: 0,
16
+ totalBytesSent: 1000,
17
+ avgResponseSize: 100,
18
+ insights: [],
19
+ eventLoopLag: 10,
20
+ memoryUsage: {
21
+ rss: 100,
22
+ heapTotal: 200,
23
+ heapUsed: 50,
24
+ external: 10,
25
+ },
26
+ statusCodes: { 200: 10 },
27
+ routes: {},
28
+ recentLogs: [],
29
+ blockedEvents: [],
30
+ };
31
+
32
+ it("should suggest caching for slow routes", () => {
33
+ const metrics: Metrics = {
34
+ ...mockMetrics,
35
+ routes: {
36
+ "GET /api/slow": {
37
+ count: 10,
38
+ totalTime: 8000,
39
+ avgTime: 800,
40
+ slowCount: 10,
41
+ highQueryCount: 0,
42
+ rateLimitHits: 0,
43
+ totalBytes: 1000,
44
+ avgSize: 100,
45
+ },
46
+ },
47
+ };
48
+
49
+ const insights = analyzeMetrics(metrics);
50
+ expect(insights.some((i) => i.title.includes("Caching Opportunity"))).toBe(
51
+ true
52
+ );
53
+ });
54
+
55
+ it("should warn about N+1 queries", () => {
56
+ const metrics: Metrics = {
57
+ ...mockMetrics,
58
+ routes: {
59
+ "GET /api/n-plus-one": {
60
+ count: 10,
61
+ totalTime: 1000,
62
+ avgTime: 100,
63
+ slowCount: 0,
64
+ highQueryCount: 8, // 80%
65
+ rateLimitHits: 0,
66
+ totalBytes: 1000,
67
+ avgSize: 100,
68
+ },
69
+ },
70
+ };
71
+
72
+ const insights = analyzeMetrics(metrics);
73
+ expect(insights.some((i) => i.title.includes("N+1 Query"))).toBe(true);
74
+ });
75
+
76
+ it("should warn about heavy payloads", () => {
77
+ const metrics: Metrics = {
78
+ ...mockMetrics,
79
+ routes: {
80
+ "GET /api/heavy": {
81
+ count: 10,
82
+ totalTime: 1000,
83
+ avgTime: 100,
84
+ slowCount: 0,
85
+ highQueryCount: 0,
86
+ rateLimitHits: 0,
87
+ totalBytes: 20 * 1024 * 1024,
88
+ avgSize: 2 * 1024 * 1024, // 2MB
89
+ },
90
+ },
91
+ };
92
+
93
+ const insights = analyzeMetrics(metrics);
94
+ expect(insights.some((i) => i.title.includes("Heavy Payload"))).toBe(true);
95
+ });
96
+
97
+ it("should warn about event loop lag", () => {
98
+ const metrics: Metrics = {
99
+ ...mockMetrics,
100
+ eventLoopLag: 150,
101
+ };
102
+
103
+ const insights = analyzeMetrics(metrics);
104
+ expect(insights.some((i) => i.title.includes("Event Loop Lagging"))).toBe(
105
+ true
106
+ );
107
+ });
108
+ });
@@ -0,0 +1,79 @@
1
+ import request from "supertest";
2
+ import express from "express";
3
+ import { performanceToolkit } from "../src/index";
4
+
5
+ describe("Dashboard Authentication", () => {
6
+ it("should require login if auth is enabled", async () => {
7
+ const app = express();
8
+ const toolkit = performanceToolkit({
9
+ dashboard: {
10
+ enabled: true,
11
+ auth: {
12
+ username: "testuser",
13
+ password: "testpassword",
14
+ },
15
+ },
16
+ });
17
+
18
+ app.use(toolkit.middleware);
19
+ app.use('/__perf', toolkit.dashboardRouter);
20
+
21
+ // Should return 401 for protected data
22
+ const resp = await request(app).get("/__perf/api/metrics");
23
+ expect(resp.status).toBe(401);
24
+
25
+ // Try login
26
+ const loginResp = await request(app)
27
+ .post("/__perf/api/login")
28
+ .send({ username: "testuser", password: "testpassword" });
29
+
30
+ expect(loginResp.status).toBe(200);
31
+ expect(loginResp.headers["set-cookie"]).toBeDefined();
32
+
33
+ // With cookie, should work
34
+ const cookie = loginResp.headers["set-cookie"];
35
+ const metricsResp = await request(app)
36
+ .get("/__perf/api/metrics")
37
+ .set("Cookie", cookie);
38
+
39
+ expect(metricsResp.status).toBe(200);
40
+ });
41
+
42
+ it("should handle invalid credentials", async () => {
43
+ const app = express();
44
+ const toolkit = performanceToolkit({
45
+ dashboard: {
46
+ enabled: true,
47
+ auth: {
48
+ username: "admin",
49
+ password: "password",
50
+ },
51
+ },
52
+ });
53
+
54
+ app.use(toolkit.middleware);
55
+ app.use('/__perf', toolkit.dashboardRouter);
56
+
57
+ const loginResp = await request(app)
58
+ .post("/__perf/api/login")
59
+ .send({ username: "admin", password: "wrong" });
60
+
61
+ expect(loginResp.status).toBe(401);
62
+ });
63
+
64
+ it("should allow access if auth is disabled", async () => {
65
+ const app = express();
66
+ const toolkit = performanceToolkit({
67
+ dashboard: {
68
+ enabled: true,
69
+ auth: undefined,
70
+ },
71
+ });
72
+
73
+ app.use(toolkit.middleware);
74
+ app.use("/__perf", toolkit.dashboardRouter);
75
+
76
+ const resp = await request(app).get("/__perf/api/metrics");
77
+ expect(resp.status).toBe(200);
78
+ });
79
+ });