perfstack 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial public release.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aftab Ahmad Khan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # perfstack
2
+
3
+ **Topics:** `dashboard` · `express` · `memory` · `mern-packages` · `merndev` · `mongodb` · `nodejs` · `npm-pm` · `observability` · `performance` · `perfstack` · `profiling` · `tracing` · `typescript`
4
+
5
+ Lightweight fullstack performance profiler for Node.js. One package gives you HTTP latency histograms, slow-query tracking (Mongoose), memory snapshots, async-context tracing, and a built-in dashboard endpoint — no agents, no SaaS, no dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install perfstack
11
+ ```
12
+
13
+ ## One-call setup
14
+
15
+ ```ts
16
+ import express from "express";
17
+ import { init } from "perfstack";
18
+
19
+ const app = express();
20
+ const { profiler, mongoosePlugin } = init(app, {
21
+ slowRequestThreshold: 500,
22
+ slowQueryThreshold: 200,
23
+ enableMemorySampling: true,
24
+ dashboardPath: "/__perf",
25
+ });
26
+
27
+ // Optional: track every Mongoose query
28
+ import mongoose from "mongoose";
29
+ mongoose.plugin(mongoosePlugin);
30
+
31
+ app.get("/", (_req, res) => res.json({ hi: true }));
32
+ app.listen(3000);
33
+ ```
34
+
35
+ Now open:
36
+
37
+ - `http://localhost:3000/__perf` — minimal HTML dashboard
38
+ - `http://localhost:3000/__perf.json` — full report as JSON
39
+
40
+ ## What you get
41
+
42
+ ```jsonc
43
+ {
44
+ "uptimeMs": 124562,
45
+ "totalRequests": 1820,
46
+ "totalErrors": 3,
47
+ "totalQueries": 4001,
48
+ "http": { "count": 1820, "avg": 41.2, "p50": 18, "p95": 220, "p99": 612, "max": 1820 },
49
+ "routes": [
50
+ { "method": "GET", "path": "/users/:id", "count": 720, "p95": 188, "errorCount": 0 },
51
+ ...
52
+ ],
53
+ "slowestRoutes": [ /* sorted by p95 */ ],
54
+ "queries": { "count": 4001, "p95": 30, "p99": 102 },
55
+ "slowestQueries": [ { "collection": "users", "op": "find", "durationMs": 612 } ],
56
+ "memory": { "current": { "rss": 134217728, "heapUsed": 41943040 }, "peakRss": 156000000 }
57
+ }
58
+ ```
59
+
60
+ ## Pieces
61
+
62
+ ```ts
63
+ import {
64
+ Profiler,
65
+ expressMiddleware,
66
+ dashboardMiddleware,
67
+ mongoosePlugin,
68
+ Histogram,
69
+ Tracer,
70
+ } from "perfstack";
71
+
72
+ const profiler = new Profiler({ slowRequestThreshold: 300 });
73
+
74
+ app.use(expressMiddleware(profiler));
75
+ app.get("/__perf", dashboardMiddleware(profiler));
76
+ app.get("/__perf.json", dashboardMiddleware(profiler));
77
+ mongoose.plugin(mongoosePlugin(profiler));
78
+ ```
79
+
80
+ ### Custom spans
81
+
82
+ ```ts
83
+ await profiler.tracer.withSpan("send-email", async () => {
84
+ await mailer.send(...);
85
+ });
86
+
87
+ const span = profiler.tracer.start("billing.charge", "external");
88
+ try {
89
+ await stripe.charge(...);
90
+ span.end({ status: "ok" });
91
+ } catch (err) {
92
+ span.end({ error: err });
93
+ }
94
+ ```
95
+
96
+ Spans inherit the request trace id automatically (`AsyncLocalStorage`).
97
+
98
+ ### Memory sampling
99
+
100
+ ```ts
101
+ const profiler = new Profiler({
102
+ enableMemorySampling: true,
103
+ memorySampleIntervalMs: 5_000,
104
+ memorySamplesMax: 120, // last 10 minutes at 5s intervals
105
+ });
106
+ ```
107
+
108
+ Disable any time with `profiler.stopMemorySampling()`.
109
+
110
+ ### Histogram percentiles
111
+
112
+ `perfstack` keeps a rolling window of the last 1000 samples per metric and computes `min/avg/p50/p90/p95/p99/max` on demand.
113
+
114
+ ## Why not Datadog / New Relic / Sentry?
115
+
116
+ You may still want them. `perfstack` is the *first* thing you reach for — when you want zero-config insight in dev, in tests, or in a small prod app. It runs in the same process, no auth, no UI to log into, no $$/month.
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,491 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Histogram: () => Histogram,
24
+ Profiler: () => Profiler,
25
+ Tracer: () => Tracer,
26
+ dashboardMiddleware: () => dashboardMiddleware,
27
+ expressMiddleware: () => expressMiddleware,
28
+ init: () => init,
29
+ mongoosePlugin: () => mongoosePlugin,
30
+ percentile: () => percentile
31
+ });
32
+ module.exports = __toCommonJS(src_exports);
33
+
34
+ // src/histogram.ts
35
+ var Histogram = class {
36
+ samples = [];
37
+ max;
38
+ constructor(max = 1e3) {
39
+ this.max = max;
40
+ }
41
+ record(value) {
42
+ this.samples.push(value);
43
+ if (this.samples.length > this.max) {
44
+ this.samples.shift();
45
+ }
46
+ }
47
+ summary() {
48
+ const count = this.samples.length;
49
+ if (count === 0) {
50
+ return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p90: 0, p95: 0, p99: 0 };
51
+ }
52
+ const sorted = [...this.samples].sort((a, b) => a - b);
53
+ const sum = sorted.reduce((s, v) => s + v, 0);
54
+ return {
55
+ count,
56
+ min: sorted[0],
57
+ max: sorted[sorted.length - 1],
58
+ avg: round(sum / count),
59
+ p50: percentile(sorted, 0.5),
60
+ p90: percentile(sorted, 0.9),
61
+ p95: percentile(sorted, 0.95),
62
+ p99: percentile(sorted, 0.99)
63
+ };
64
+ }
65
+ reset() {
66
+ this.samples = [];
67
+ }
68
+ };
69
+ function percentile(sortedAscending, p) {
70
+ if (sortedAscending.length === 0) return 0;
71
+ const idx = Math.min(sortedAscending.length - 1, Math.floor(p * sortedAscending.length));
72
+ return round(sortedAscending[idx]);
73
+ }
74
+ function round(n) {
75
+ return Math.round(n * 100) / 100;
76
+ }
77
+
78
+ // src/tracer.ts
79
+ var import_node_async_hooks = require("async_hooks");
80
+ var import_node_crypto = require("crypto");
81
+ var Tracer = class {
82
+ storage = new import_node_async_hooks.AsyncLocalStorage();
83
+ spans = [];
84
+ max;
85
+ constructor(options = {}) {
86
+ this.max = options.maxSpans ?? 500;
87
+ }
88
+ start(name, kind = "custom", metadata = {}) {
89
+ const startedAt = Date.now();
90
+ const id = `s_${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
91
+ const parent = this.storage.getStore();
92
+ const traceId = parent?.traceId ?? `t_${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
93
+ const parentId = parent?.spanId;
94
+ return {
95
+ end: (info = {}) => {
96
+ const status = info.error ? "error" : info.status ?? "ok";
97
+ const errorMsg = info.error instanceof Error ? info.error.message : info.error ? String(info.error) : void 0;
98
+ const span = {
99
+ id,
100
+ parentId,
101
+ traceId,
102
+ name,
103
+ kind,
104
+ startedAt,
105
+ durationMs: Date.now() - startedAt,
106
+ metadata: { ...metadata, ...info.extra ?? {} },
107
+ status,
108
+ error: errorMsg
109
+ };
110
+ this.push(span);
111
+ return span;
112
+ }
113
+ };
114
+ }
115
+ async withSpan(name, fn, options = {}) {
116
+ const startedAt = Date.now();
117
+ const parent = this.storage.getStore();
118
+ const traceId = parent?.traceId ?? `t_${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
119
+ const spanId = `s_${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
120
+ return this.storage.run({ traceId, spanId }, async () => {
121
+ try {
122
+ const result = await fn();
123
+ this.push({
124
+ id: spanId,
125
+ parentId: parent?.spanId,
126
+ traceId,
127
+ name,
128
+ kind: options.kind ?? "custom",
129
+ startedAt,
130
+ durationMs: Date.now() - startedAt,
131
+ metadata: options.metadata ?? {},
132
+ status: "ok"
133
+ });
134
+ return result;
135
+ } catch (err) {
136
+ this.push({
137
+ id: spanId,
138
+ parentId: parent?.spanId,
139
+ traceId,
140
+ name,
141
+ kind: options.kind ?? "custom",
142
+ startedAt,
143
+ durationMs: Date.now() - startedAt,
144
+ metadata: options.metadata ?? {},
145
+ status: "error",
146
+ error: err instanceof Error ? err.message : String(err)
147
+ });
148
+ throw err;
149
+ }
150
+ });
151
+ }
152
+ runInContext(traceId, fn) {
153
+ const spanId = `s_${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
154
+ return this.storage.run({ traceId, spanId }, fn);
155
+ }
156
+ currentTraceId() {
157
+ return this.storage.getStore()?.traceId;
158
+ }
159
+ recent(limit = 100) {
160
+ return this.spans.slice(-limit);
161
+ }
162
+ byTrace(traceId) {
163
+ return this.spans.filter((s) => s.traceId === traceId);
164
+ }
165
+ reset() {
166
+ this.spans = [];
167
+ }
168
+ push(span) {
169
+ this.spans.push(span);
170
+ if (this.spans.length > this.max) this.spans.shift();
171
+ }
172
+ };
173
+
174
+ // src/profiler.ts
175
+ var Profiler = class {
176
+ startedAt;
177
+ httpHistogram = new Histogram();
178
+ queryHistogram = new Histogram();
179
+ routes = /* @__PURE__ */ new Map();
180
+ slowestQueries = [];
181
+ memorySamples = [];
182
+ peakRss = 0;
183
+ peakHeapUsed = 0;
184
+ totalRequests = 0;
185
+ totalErrors = 0;
186
+ totalQueries = 0;
187
+ memoryInterval;
188
+ tracer;
189
+ opts;
190
+ constructor(options = {}) {
191
+ this.startedAt = options.startedAt ?? Date.now();
192
+ this.opts = {
193
+ slowRequestThreshold: options.slowRequestThreshold ?? 500,
194
+ slowQueryThreshold: options.slowQueryThreshold ?? 200,
195
+ memorySampleIntervalMs: options.memorySampleIntervalMs ?? 5e3,
196
+ memorySamplesMax: options.memorySamplesMax ?? 120,
197
+ routeBucketsMax: options.routeBucketsMax ?? 200,
198
+ enableMemorySampling: options.enableMemorySampling ?? false
199
+ };
200
+ this.tracer = new Tracer();
201
+ if (this.opts.enableMemorySampling) this.startMemorySampling();
202
+ }
203
+ recordHttp(record) {
204
+ this.totalRequests += 1;
205
+ if (record.statusCode >= 500) this.totalErrors += 1;
206
+ this.httpHistogram.record(record.durationMs);
207
+ const key = `${record.method} ${record.path}`;
208
+ let bucket = this.routes.get(key);
209
+ if (!bucket) {
210
+ if (this.routes.size >= this.opts.routeBucketsMax) {
211
+ const oldest = this.routes.keys().next().value;
212
+ if (oldest !== void 0) this.routes.delete(oldest);
213
+ }
214
+ bucket = { method: record.method, path: record.path, histogram: new Histogram(), errors: 0 };
215
+ this.routes.set(key, bucket);
216
+ }
217
+ bucket.histogram.record(record.durationMs);
218
+ if (record.statusCode >= 500) bucket.errors += 1;
219
+ }
220
+ recordQuery(record) {
221
+ this.totalQueries += 1;
222
+ this.queryHistogram.record(record.durationMs);
223
+ if (record.durationMs >= this.opts.slowQueryThreshold) {
224
+ this.slowestQueries.push(record);
225
+ this.slowestQueries.sort((a, b) => b.durationMs - a.durationMs);
226
+ if (this.slowestQueries.length > 50) this.slowestQueries.length = 50;
227
+ }
228
+ }
229
+ takeMemorySample() {
230
+ const m = process.memoryUsage();
231
+ const snap = {
232
+ takenAt: Date.now(),
233
+ rss: m.rss,
234
+ heapTotal: m.heapTotal,
235
+ heapUsed: m.heapUsed,
236
+ external: m.external,
237
+ arrayBuffers: m.arrayBuffers
238
+ };
239
+ this.memorySamples.push(snap);
240
+ if (this.memorySamples.length > this.opts.memorySamplesMax) this.memorySamples.shift();
241
+ if (snap.rss > this.peakRss) this.peakRss = snap.rss;
242
+ if (snap.heapUsed > this.peakHeapUsed) this.peakHeapUsed = snap.heapUsed;
243
+ return snap;
244
+ }
245
+ startMemorySampling() {
246
+ if (this.memoryInterval) return;
247
+ this.takeMemorySample();
248
+ const interval = setInterval(() => this.takeMemorySample(), this.opts.memorySampleIntervalMs);
249
+ interval.unref?.();
250
+ this.memoryInterval = interval;
251
+ }
252
+ stopMemorySampling() {
253
+ if (this.memoryInterval) {
254
+ clearInterval(this.memoryInterval);
255
+ this.memoryInterval = void 0;
256
+ }
257
+ }
258
+ report() {
259
+ if (this.memorySamples.length === 0) this.takeMemorySample();
260
+ const routes = Array.from(this.routes.values()).map((bucket) => ({
261
+ method: bucket.method,
262
+ path: bucket.path,
263
+ errorCount: bucket.errors,
264
+ ...bucket.histogram.summary()
265
+ }));
266
+ const slowestRoutes = [...routes].sort((a, b) => b.p95 - a.p95).slice(0, 10);
267
+ return {
268
+ uptimeMs: Date.now() - this.startedAt,
269
+ startedAt: this.startedAt,
270
+ takenAt: Date.now(),
271
+ totalRequests: this.totalRequests,
272
+ totalErrors: this.totalErrors,
273
+ totalQueries: this.totalQueries,
274
+ http: this.httpHistogram.summary(),
275
+ routes,
276
+ slowestRoutes,
277
+ queries: this.queryHistogram.summary(),
278
+ slowestQueries: [...this.slowestQueries],
279
+ memory: {
280
+ current: this.memorySamples.at(-1) ?? null,
281
+ samples: [...this.memorySamples],
282
+ peakRss: this.peakRss,
283
+ peakHeapUsed: this.peakHeapUsed
284
+ }
285
+ };
286
+ }
287
+ reset() {
288
+ this.httpHistogram.reset();
289
+ this.queryHistogram.reset();
290
+ this.routes.clear();
291
+ this.slowestQueries = [];
292
+ this.memorySamples = [];
293
+ this.peakRss = 0;
294
+ this.peakHeapUsed = 0;
295
+ this.totalRequests = 0;
296
+ this.totalErrors = 0;
297
+ this.totalQueries = 0;
298
+ this.tracer.reset();
299
+ }
300
+ isSlowRequest(durationMs) {
301
+ return durationMs >= this.opts.slowRequestThreshold;
302
+ }
303
+ };
304
+
305
+ // src/express.ts
306
+ var import_node_crypto2 = require("crypto");
307
+ function expressMiddleware(profiler, options = {}) {
308
+ const { ignorePaths = [], traceHeader = "x-trace-id", exposeTraceHeader = true } = options;
309
+ return function perfstackMiddleware(req, res, next) {
310
+ const path = req.originalUrl ?? req.url ?? "/";
311
+ if (ignorePaths.some((p) => path === p || path.startsWith(`${p}/`))) {
312
+ next();
313
+ return;
314
+ }
315
+ const traceHeaderVal = req.headers[traceHeader.toLowerCase()];
316
+ const traceId = typeof traceHeaderVal === "string" && traceHeaderVal || Array.isArray(traceHeaderVal) && traceHeaderVal[0] || `req_${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`;
317
+ if (exposeTraceHeader) res.setHeader(traceHeader, traceId);
318
+ const startedAt = Date.now();
319
+ profiler.tracer.runInContext(traceId, () => {
320
+ res.on("finish", () => {
321
+ profiler.recordHttp({
322
+ method: req.method ?? "GET",
323
+ path: req.route?.path ?? simplifyPath(path),
324
+ statusCode: res.statusCode,
325
+ durationMs: Date.now() - startedAt,
326
+ startedAt,
327
+ traceId
328
+ });
329
+ });
330
+ next();
331
+ });
332
+ };
333
+ }
334
+ function dashboardMiddleware(profiler) {
335
+ return function perfstackDashboard(req, res) {
336
+ const path = req.originalUrl ?? req.url ?? "/";
337
+ if (path.endsWith(".json")) {
338
+ res.setHeader("content-type", "application/json");
339
+ res.statusCode = 200;
340
+ res.end(JSON.stringify(profiler.report(), null, 2));
341
+ return;
342
+ }
343
+ res.setHeader("content-type", "text/html; charset=utf-8");
344
+ res.statusCode = 200;
345
+ res.end(renderDashboard(profiler));
346
+ };
347
+ }
348
+ function simplifyPath(p) {
349
+ return p.split("?")[0].replace(/\/[0-9a-f]{24}\b/gi, "/:id").replace(/\/\d+\b/g, "/:id");
350
+ }
351
+ function renderDashboard(profiler) {
352
+ const report = profiler.report();
353
+ return `<!doctype html>
354
+ <html><head><meta charset="utf-8" />
355
+ <title>perfstack</title>
356
+ <style>
357
+ body{font:14px/1.4 ui-monospace,Menlo,monospace;background:#0b1020;color:#e6e6e6;margin:0;padding:24px}
358
+ h1,h2{font-weight:600;margin:0 0 12px}h2{margin-top:24px}
359
+ table{border-collapse:collapse;width:100%;margin-top:8px}
360
+ th,td{padding:6px 10px;text-align:left;border-bottom:1px solid #1d2440}
361
+ th{color:#8aa6ff;font-weight:600}
362
+ .bad{color:#ff8a8a}.good{color:#8aff9c}
363
+ .card{background:#121838;border-radius:8px;padding:16px;margin-bottom:16px}
364
+ .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
365
+ .k{color:#8a93b3}.v{font-size:20px;font-weight:600}
366
+ </style></head>
367
+ <body>
368
+ <h1>perfstack</h1>
369
+ <div class="grid">
370
+ <div class="card"><div class="k">uptime</div><div class="v">${Math.round(report.uptimeMs / 1e3)}s</div></div>
371
+ <div class="card"><div class="k">requests</div><div class="v">${report.totalRequests}</div></div>
372
+ <div class="card"><div class="k">errors</div><div class="v ${report.totalErrors ? "bad" : "good"}">${report.totalErrors}</div></div>
373
+ <div class="card"><div class="k">queries</div><div class="v">${report.totalQueries}</div></div>
374
+ </div>
375
+
376
+ <h2>HTTP latency (ms)</h2>
377
+ <table><tr><th>count</th><th>min</th><th>avg</th><th>p50</th><th>p90</th><th>p95</th><th>p99</th><th>max</th></tr>
378
+ <tr><td>${report.http.count}</td><td>${report.http.min}</td><td>${report.http.avg}</td><td>${report.http.p50}</td><td>${report.http.p90}</td><td>${report.http.p95}</td><td>${report.http.p99}</td><td>${report.http.max}</td></tr>
379
+ </table>
380
+
381
+ <h2>Slowest routes (p95)</h2>
382
+ <table><tr><th>route</th><th>count</th><th>p95</th><th>p99</th><th>max</th><th>errors</th></tr>
383
+ ${report.slowestRoutes.map((r) => `<tr><td>${escapeHtml(r.method)} ${escapeHtml(r.path)}</td><td>${r.count}</td><td>${r.p95}</td><td>${r.p99}</td><td>${r.max}</td><td>${r.errorCount}</td></tr>`).join("\n")}
384
+ </table>
385
+
386
+ <h2>Slowest queries</h2>
387
+ <table><tr><th>collection</th><th>op</th><th>duration</th></tr>
388
+ ${report.slowestQueries.map((q) => `<tr><td>${escapeHtml(q.collection)}</td><td>${escapeHtml(q.op)}</td><td>${q.durationMs}ms</td></tr>`).join("\n")}
389
+ </table>
390
+
391
+ <h2>Memory</h2>
392
+ <table><tr><th>rss</th><th>heap used</th><th>heap total</th><th>peak rss</th><th>peak heap</th></tr>
393
+ <tr>
394
+ <td>${fmtBytes(report.memory.current?.rss ?? 0)}</td>
395
+ <td>${fmtBytes(report.memory.current?.heapUsed ?? 0)}</td>
396
+ <td>${fmtBytes(report.memory.current?.heapTotal ?? 0)}</td>
397
+ <td>${fmtBytes(report.memory.peakRss)}</td>
398
+ <td>${fmtBytes(report.memory.peakHeapUsed)}</td>
399
+ </tr>
400
+ </table>
401
+
402
+ </body></html>`;
403
+ }
404
+ function fmtBytes(n) {
405
+ if (n < 1024) return `${n} B`;
406
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
407
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
408
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
409
+ }
410
+ function escapeHtml(s) {
411
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
412
+ }
413
+
414
+ // src/mongoose.ts
415
+ var HOOKS = [
416
+ "find",
417
+ "findOne",
418
+ "findOneAndUpdate",
419
+ "findOneAndDelete",
420
+ "count",
421
+ "countDocuments",
422
+ "updateOne",
423
+ "updateMany",
424
+ "deleteOne",
425
+ "deleteMany",
426
+ "distinct"
427
+ ];
428
+ function mongoosePlugin(profiler) {
429
+ return function perfstackMongoose(schema) {
430
+ for (const hook of HOOKS) {
431
+ schema.pre(hook, function() {
432
+ this._perfstackStart = Date.now();
433
+ this._perfstackOp = hook;
434
+ this._perfstackCollection = this.model?.collection?.name ?? this.model?.modelName ?? "unknown";
435
+ });
436
+ schema.post(hook, function() {
437
+ const startedAt = this._perfstackStart ?? Date.now();
438
+ profiler.recordQuery({
439
+ collection: this._perfstackCollection ?? "unknown",
440
+ op: this._perfstackOp ?? hook,
441
+ durationMs: Date.now() - startedAt,
442
+ startedAt,
443
+ traceId: profiler.tracer.currentTraceId()
444
+ });
445
+ });
446
+ }
447
+ schema.pre("aggregate", function() {
448
+ this._perfstackStart = Date.now();
449
+ const model = typeof this.model === "function" ? this.model() : void 0;
450
+ this._perfstackCollection = model?.collection?.name ?? model?.modelName ?? "unknown";
451
+ });
452
+ schema.post("aggregate", function() {
453
+ const startedAt = this._perfstackStart ?? Date.now();
454
+ profiler.recordQuery({
455
+ collection: this._perfstackCollection ?? "unknown",
456
+ op: "aggregate",
457
+ durationMs: Date.now() - startedAt,
458
+ startedAt,
459
+ traceId: profiler.tracer.currentTraceId()
460
+ });
461
+ });
462
+ };
463
+ }
464
+
465
+ // src/index.ts
466
+ function init(app, options = {}) {
467
+ const profiler = new Profiler(options);
468
+ const middleware = expressMiddleware(profiler);
469
+ const dashboard = dashboardMiddleware(profiler);
470
+ const plugin = mongoosePlugin(profiler);
471
+ if (app) {
472
+ app.use(middleware);
473
+ if (typeof app.get === "function") {
474
+ app.get(options.dashboardPath ?? "/__perf", dashboard);
475
+ app.get((options.dashboardPath ?? "/__perf") + ".json", dashboard);
476
+ }
477
+ }
478
+ return { profiler, middleware, dashboard, mongoosePlugin: plugin };
479
+ }
480
+ // Annotate the CommonJS export names for ESM import in node:
481
+ 0 && (module.exports = {
482
+ Histogram,
483
+ Profiler,
484
+ Tracer,
485
+ dashboardMiddleware,
486
+ expressMiddleware,
487
+ init,
488
+ mongoosePlugin,
489
+ percentile
490
+ });
491
+ //# sourceMappingURL=index.cjs.map