opticore-profiler 1.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/assets/css/debug-message.css +16 -0
  4. package/dist/assets/css/home-page.css +177 -0
  5. package/dist/assets/css/profiler-detail.css +215 -0
  6. package/dist/assets/css/profiler-list.css +131 -0
  7. package/dist/assets/css/toolbar-standalone.css +213 -0
  8. package/dist/assets/js/home-particles.js +65 -0
  9. package/dist/assets/js/profiler-detail.js +77 -0
  10. package/dist/assets/js/profiler-list.js +76 -0
  11. package/dist/assets/js/toolbar.js +230 -0
  12. package/dist/index.cjs +1942 -0
  13. package/dist/index.d.cts +488 -0
  14. package/dist/index.d.ts +488 -0
  15. package/dist/index.js +1893 -0
  16. package/dist/utils/translations/message.translation.en.json +190 -0
  17. package/dist/utils/translations/message.translation.fr.json +190 -0
  18. package/dist/views/templates/home.njk +120 -0
  19. package/dist/views/templates/macros/icons.njk +19 -0
  20. package/dist/views/templates/macros/tables.njk +17 -0
  21. package/dist/views/templates/message.njk +14 -0
  22. package/dist/views/templates/panels/configuration.njk +16 -0
  23. package/dist/views/templates/panels/database.njk +36 -0
  24. package/dist/views/templates/panels/exception.njk +26 -0
  25. package/dist/views/templates/panels/logs.njk +44 -0
  26. package/dist/views/templates/panels/performance.njk +56 -0
  27. package/dist/views/templates/panels/request.njk +141 -0
  28. package/dist/views/templates/panels/routes.njk +51 -0
  29. package/dist/views/templates/panels/routing.njk +22 -0
  30. package/dist/views/templates/profiler-detail.njk +114 -0
  31. package/dist/views/templates/profiler-list.njk +163 -0
  32. package/dist/views/templates/toolbar-wdt.njk +161 -0
  33. package/package.json +73 -0
package/dist/index.js ADDED
@@ -0,0 +1,1893 @@
1
+ // node_modules/tsup/assets/esm_shims.js
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ var getFilename = () => fileURLToPath(import.meta.url);
5
+ var getDirname = () => path.dirname(getFilename());
6
+ var __dirname = /* @__PURE__ */ getDirname();
7
+
8
+ // src/middleware/profiler.middleware.ts
9
+ import { EventEmitter } from "events";
10
+
11
+ // src/token.ts
12
+ import { randomBytes } from "crypto";
13
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
14
+ function generateToken(length = 8) {
15
+ if (length < 6 || length > 8) {
16
+ throw new Error("Profiler token length must be between 6 and 8 characters");
17
+ }
18
+ let out = "";
19
+ while (out.length < length) {
20
+ const bytes = randomBytes(length);
21
+ for (let i = 0; i < bytes.length && out.length < length; i++) {
22
+ const byte = bytes[i];
23
+ if (byte < 252) {
24
+ out += ALPHABET[byte % 36];
25
+ }
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ function isValidToken(token) {
31
+ return typeof token === "string" && /^[a-z0-9]{6,8}$/.test(token);
32
+ }
33
+
34
+ // src/context.ts
35
+ import { AsyncLocalStorage } from "async_hooks";
36
+ var profilerContext = new AsyncLocalStorage();
37
+ function runWithProfilerContext(state, fn) {
38
+ return profilerContext.run(state, fn);
39
+ }
40
+ function getActiveProfilingState() {
41
+ return profilerContext.getStore();
42
+ }
43
+ function getActiveCollector(name) {
44
+ return profilerContext.getStore()?.collectors.get(name);
45
+ }
46
+
47
+ // src/registry.ts
48
+ var CollectorRegistry = class {
49
+ factories = /* @__PURE__ */ new Map();
50
+ register(factory) {
51
+ const probe = factory();
52
+ const name = probe.getName();
53
+ if (!name) {
54
+ throw new Error("DataCollector.getName() must return a non-empty string");
55
+ }
56
+ this.factories.set(name, factory);
57
+ }
58
+ unregister(name) {
59
+ this.factories.delete(name);
60
+ }
61
+ has(name) {
62
+ return this.factories.has(name);
63
+ }
64
+ names() {
65
+ return [...this.factories.keys()];
66
+ }
67
+ /** Builds one fresh instance per registered factory, keyed by collector name. */
68
+ createAll() {
69
+ const instances = /* @__PURE__ */ new Map();
70
+ for (const [name, factory] of this.factories) {
71
+ instances.set(name, factory());
72
+ }
73
+ return instances;
74
+ }
75
+ };
76
+
77
+ // src/types.ts
78
+ var DEFAULT_EXCLUDE_PATHS = ["/favicon.ico", "/.well-known/*"];
79
+ var DEFAULT_SENSITIVE_KEYS = [
80
+ "authorization",
81
+ "cookie",
82
+ "set-cookie",
83
+ "x-api-key",
84
+ "session",
85
+ "password",
86
+ "passwd",
87
+ "secret",
88
+ "token",
89
+ "access_token",
90
+ "refresh_token",
91
+ "api_key",
92
+ "apikey",
93
+ "private_key"
94
+ ];
95
+
96
+ // src/security/masker.ts
97
+ var REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
98
+ var Masker = class {
99
+ keys;
100
+ constructor(sensitiveKeys = DEFAULT_SENSITIVE_KEYS) {
101
+ this.keys = new Set(sensitiveKeys.map((k) => k.toLowerCase()));
102
+ }
103
+ isSensitive(key) {
104
+ const k = key.toLowerCase();
105
+ for (const sensitive of this.keys) {
106
+ if (k === sensitive || k.includes(sensitive)) return true;
107
+ }
108
+ return false;
109
+ }
110
+ /** Shallow-masks a flat object (headers, query, cookies, single-level body). */
111
+ maskShallow(obj) {
112
+ const result = {};
113
+ for (const [key, value] of Object.entries(obj ?? {})) {
114
+ result[key] = this.isSensitive(key) ? REDACTED : value;
115
+ }
116
+ return result;
117
+ }
118
+ /** Recursively masks nested objects/arrays (request/response bodies). */
119
+ maskDeep(value, depth = 0) {
120
+ if (depth > 8 || value === null || typeof value !== "object") return value;
121
+ if (Array.isArray(value)) {
122
+ return value.map((v) => this.maskDeep(v, depth + 1));
123
+ }
124
+ const out = {};
125
+ for (const [key, v] of Object.entries(value)) {
126
+ out[key] = this.isSensitive(key) ? REDACTED : this.maskDeep(v, depth + 1);
127
+ }
128
+ return out;
129
+ }
130
+ };
131
+
132
+ // src/collectors/request.collector.ts
133
+ var HTTP_STATUS_MESSAGES = {
134
+ 100: "Continue",
135
+ 101: "Switching Protocols",
136
+ 200: "OK",
137
+ 201: "Created",
138
+ 202: "Accepted",
139
+ 204: "No Content",
140
+ 301: "Moved Permanently",
141
+ 302: "Found",
142
+ 304: "Not Modified",
143
+ 400: "Bad Request",
144
+ 401: "Unauthorized",
145
+ 403: "Forbidden",
146
+ 404: "Not Found",
147
+ 405: "Method Not Allowed",
148
+ 409: "Conflict",
149
+ 422: "Unprocessable Entity",
150
+ 429: "Too Many Requests",
151
+ 500: "Internal Server Error",
152
+ 502: "Bad Gateway",
153
+ 503: "Service Unavailable"
154
+ };
155
+ function statusMessage(code) {
156
+ return HTTP_STATUS_MESSAGES[code] ?? "Unknown";
157
+ }
158
+ var RequestCollector = class {
159
+ constructor(masker = new Masker()) {
160
+ this.masker = masker;
161
+ }
162
+ masker;
163
+ data = null;
164
+ getName() {
165
+ return "request";
166
+ }
167
+ /** Parses the buffered response body as JSON when the response declared a JSON content-type — undefined otherwise (HTML pages, empty bodies, non-JSON payloads), so the detail view falls back to its "no body captured" state instead of dumping raw bytes. */
168
+ parseResponseBody(ctx) {
169
+ if (!ctx.responseBody || !ctx.responseBody.contentType.includes("application/json")) return void 0;
170
+ const text = ctx.responseBody.buffer.toString("utf8").trim();
171
+ if (!text) return void 0;
172
+ try {
173
+ return this.masker.maskDeep(JSON.parse(text));
174
+ } catch {
175
+ return void 0;
176
+ }
177
+ }
178
+ /** Parses Set-Cookie response header(s) into name -> value, mirroring how request cookies are captured — masked per cookie name (Masker), not blanket-redacted like the raw "set-cookie" header. */
179
+ parseResponseCookies(res) {
180
+ const raw = res.getHeader("set-cookie");
181
+ const setCookies = Array.isArray(raw) ? raw.map(String) : raw ? [String(raw)] : [];
182
+ const cookies = {};
183
+ for (const setCookie of setCookies) {
184
+ const pair = setCookie.split(";")[0];
185
+ const eq = pair.indexOf("=");
186
+ if (eq === -1) continue;
187
+ const name = pair.slice(0, eq).trim();
188
+ if (!name) continue;
189
+ try {
190
+ cookies[name] = decodeURIComponent(pair.slice(eq + 1).trim());
191
+ } catch {
192
+ cookies[name] = pair.slice(eq + 1).trim();
193
+ }
194
+ }
195
+ return this.masker.maskShallow(cookies);
196
+ }
197
+ /**
198
+ * Reads `req.session` (express-session or any middleware that assigns a
199
+ * plain, JSON-serializable object there) — own data properties only,
200
+ * methods (save/destroy/reload/...) live on the prototype so a plain
201
+ * Object.entries() already excludes them. Returns undefined (not `{}`)
202
+ * when there's no session object at all, so the Session tab can tell
203
+ * "this app doesn't use sessions" apart from "session exists but empty".
204
+ */
205
+ parseSession(req) {
206
+ const session = req.session;
207
+ if (!session || typeof session !== "object") return void 0;
208
+ const data = {};
209
+ for (const [key, value] of Object.entries(session)) {
210
+ if (typeof value === "function") continue;
211
+ data[key] = value;
212
+ }
213
+ return this.masker.maskDeep(data);
214
+ }
215
+ collect(ctx) {
216
+ const { req, res } = ctx;
217
+ const statusCode = res.statusCode;
218
+ this.data = {
219
+ method: req.method,
220
+ url: req.originalUrl || req.url,
221
+ statusCode,
222
+ statusMessage: statusMessage(statusCode),
223
+ request: {
224
+ method: req.method,
225
+ url: req.originalUrl || req.url,
226
+ headers: this.masker.maskShallow(req.headers),
227
+ query: this.masker.maskDeep(req.query ?? {}),
228
+ body: this.masker.maskDeep(req.body ?? {}),
229
+ params: req.params ?? {},
230
+ ip: req.ip ?? req.socket?.remoteAddress ?? "unknown",
231
+ cookies: this.masker.maskShallow(req.cookies ?? {}),
232
+ protocol: req.protocol,
233
+ hostname: req.hostname,
234
+ session: this.parseSession(req)
235
+ },
236
+ response: {
237
+ statusCode,
238
+ statusMessage: statusMessage(statusCode),
239
+ headers: this.masker.maskShallow(res.getHeaders()),
240
+ contentType: res.getHeader("content-type"),
241
+ cookies: this.parseResponseCookies(res),
242
+ body: this.parseResponseBody(ctx)
243
+ },
244
+ route: {
245
+ path: req.route?.path ?? req.url,
246
+ method: req.method,
247
+ params: req.params ?? {},
248
+ controller: req.route?.stack?.[0]?.name
249
+ }
250
+ };
251
+ }
252
+ getData() {
253
+ if (!this.data) {
254
+ throw new Error("RequestCollector.getData() called before collect()");
255
+ }
256
+ return this.data;
257
+ }
258
+ reset() {
259
+ this.data = null;
260
+ }
261
+ };
262
+
263
+ // src/collectors/time.collector.ts
264
+ var TimeCollector = class {
265
+ data = null;
266
+ phases = [];
267
+ getName() {
268
+ return "time";
269
+ }
270
+ /** Records a named checkpoint at the current elapsed time, relative to the active request's start. */
271
+ mark(name) {
272
+ const startHr = getActiveProfilingState()?.startHrTime;
273
+ if (!startHr) return;
274
+ const elapsedNs = process.hrtime.bigint() - startHr;
275
+ this.phases.push({ name, offset: Number(elapsedNs / 1000000n) });
276
+ }
277
+ collect(ctx) {
278
+ const durationNs = process.hrtime.bigint() - ctx.startHrTime;
279
+ this.data = {
280
+ startedAt: ctx.startTime,
281
+ duration: Number(durationNs / 1000000n),
282
+ phases: [...this.phases]
283
+ };
284
+ }
285
+ getData() {
286
+ if (!this.data) {
287
+ throw new Error("TimeCollector.getData() called before collect()");
288
+ }
289
+ return this.data;
290
+ }
291
+ reset() {
292
+ this.data = null;
293
+ this.phases.length = 0;
294
+ }
295
+ };
296
+
297
+ // src/collectors/memory.collector.ts
298
+ var MemoryCollector = class {
299
+ data = null;
300
+ getName() {
301
+ return "memory";
302
+ }
303
+ collect(ctx) {
304
+ const heapUsedAfter = process.memoryUsage().heapUsed;
305
+ this.data = {
306
+ heapUsedBefore: ctx.heapUsedBefore,
307
+ heapUsedAfter,
308
+ delta: heapUsedAfter - ctx.heapUsedBefore
309
+ };
310
+ }
311
+ getData() {
312
+ if (!this.data) {
313
+ throw new Error("MemoryCollector.getData() called before collect()");
314
+ }
315
+ return this.data;
316
+ }
317
+ reset() {
318
+ this.data = null;
319
+ }
320
+ };
321
+
322
+ // src/collectors/database.collector.ts
323
+ var DatabaseCollector = class {
324
+ queries = [];
325
+ getName() {
326
+ return "database";
327
+ }
328
+ record(query) {
329
+ this.queries.push({ ...query, timestamp: Date.now() });
330
+ }
331
+ collect(_ctx) {
332
+ }
333
+ getData() {
334
+ return [...this.queries];
335
+ }
336
+ reset() {
337
+ this.queries.length = 0;
338
+ }
339
+ };
340
+
341
+ // src/collectors/logger.collector.ts
342
+ var LoggerCollector = class {
343
+ logs = [];
344
+ getName() {
345
+ return "logger";
346
+ }
347
+ record(entry) {
348
+ this.logs.push({ ...entry, timestamp: Date.now() });
349
+ }
350
+ collect(_ctx) {
351
+ }
352
+ getData() {
353
+ return [...this.logs];
354
+ }
355
+ byLevel(level) {
356
+ return this.logs.filter((l) => l.level === level);
357
+ }
358
+ reset() {
359
+ this.logs.length = 0;
360
+ }
361
+ };
362
+
363
+ // src/collectors/exception.collector.ts
364
+ var ExceptionCollector = class {
365
+ error = null;
366
+ getName() {
367
+ return "exception";
368
+ }
369
+ setError(err) {
370
+ if (this.error) return;
371
+ this.error = {
372
+ name: err.name || "Error",
373
+ message: err.message,
374
+ stack: err.stack ?? ""
375
+ };
376
+ }
377
+ collect(_ctx, error) {
378
+ if (error) this.setError(error);
379
+ }
380
+ getData() {
381
+ return { error: this.error };
382
+ }
383
+ reset() {
384
+ this.error = null;
385
+ }
386
+ };
387
+
388
+ // src/collectors/index.ts
389
+ function createBuiltinCollectorFactories(security) {
390
+ const masker = new Masker(security.sensitiveKeys);
391
+ return [
392
+ () => new RequestCollector(masker),
393
+ () => new TimeCollector(),
394
+ () => new MemoryCollector(),
395
+ () => new DatabaseCollector(),
396
+ () => new LoggerCollector(),
397
+ () => new ExceptionCollector()
398
+ ];
399
+ }
400
+
401
+ // src/middleware/htmlInjector.ts
402
+ function installHtmlInjection(res, buildSnippet, onCapture) {
403
+ const chunks = [];
404
+ let intercepting = true;
405
+ const originalWrite = res.write.bind(res);
406
+ const originalEnd = res.end.bind(res);
407
+ function toBuffer(chunk, encoding) {
408
+ if (chunk === void 0 || chunk === null) return null;
409
+ if (Buffer.isBuffer(chunk)) return chunk;
410
+ const enc = typeof encoding === "string" ? encoding : "utf8";
411
+ return Buffer.from(chunk, enc);
412
+ }
413
+ res.write = function(chunk, ...rest) {
414
+ if (!intercepting) return originalWrite(chunk, ...rest);
415
+ const buf = toBuffer(chunk, rest[0]);
416
+ if (buf) chunks.push(buf);
417
+ return true;
418
+ };
419
+ res.end = function(chunk, ...rest) {
420
+ if (!intercepting) return originalEnd(chunk, ...rest);
421
+ intercepting = false;
422
+ const buf = toBuffer(chunk, rest[0]);
423
+ if (buf) chunks.push(buf);
424
+ const contentType = String(res.getHeader("content-type") ?? "");
425
+ const isHtml = contentType.includes("text/html");
426
+ let body = Buffer.concat(chunks);
427
+ if (isHtml && !res.headersSent) {
428
+ const text = body.toString("utf8");
429
+ if (/<\/body>/i.test(text)) {
430
+ const injected = text.replace(/<\/body>/i, `${buildSnippet()}
431
+ </body>`);
432
+ body = Buffer.from(injected, "utf8");
433
+ res.setHeader("Content-Length", Buffer.byteLength(body));
434
+ }
435
+ }
436
+ onCapture?.(body, contentType);
437
+ return originalEnd(body);
438
+ };
439
+ }
440
+
441
+ // src/utils/i18n.ts
442
+ import { readFileSync } from "fs";
443
+ import { join } from "path";
444
+
445
+ // src/security/escape.ts
446
+ function escapeHtml(value) {
447
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
448
+ }
449
+
450
+ // src/utils/i18n.ts
451
+ var SUPPORTED_LANGUAGES = ["en", "fr"];
452
+ var DEFAULT_LANGUAGE = "en";
453
+ var LANG_COOKIE = "opticore_profiler_lang";
454
+ var LANG_LABELS = { en: "English", fr: "Fran\xE7ais" };
455
+ function isSupportedLanguage(value) {
456
+ return typeof value === "string" && SUPPORTED_LANGUAGES.includes(value);
457
+ }
458
+ var dictionaryCache = /* @__PURE__ */ new Map();
459
+ function loadDictionary(lang) {
460
+ const cached = dictionaryCache.get(lang);
461
+ if (cached) return cached;
462
+ const path2 = join(__dirname, "utils", "translations", `message.translation.${lang}.json`);
463
+ let dict = {};
464
+ try {
465
+ dict = JSON.parse(readFileSync(path2, "utf8"));
466
+ } catch {
467
+ dict = {};
468
+ }
469
+ dictionaryCache.set(lang, dict);
470
+ return dict;
471
+ }
472
+ var SafeHtml = class {
473
+ constructor(value) {
474
+ this.value = value;
475
+ }
476
+ value;
477
+ };
478
+ function safeHtml(value) {
479
+ return new SafeHtml(value);
480
+ }
481
+ function createTranslator(lang) {
482
+ const dict = loadDictionary(lang);
483
+ const fallback = lang === DEFAULT_LANGUAGE ? dict : loadDictionary(DEFAULT_LANGUAGE);
484
+ return function t(key, vars) {
485
+ let str = dict[key] ?? fallback[key] ?? key;
486
+ if (vars) {
487
+ for (const [name, value] of Object.entries(vars)) {
488
+ const rendered = value instanceof SafeHtml ? value.value : escapeHtml(value);
489
+ str = str.split(`{${name}}`).join(rendered);
490
+ }
491
+ }
492
+ return str;
493
+ };
494
+ }
495
+ function parseCookie(header, name) {
496
+ if (!header) return void 0;
497
+ for (const part of header.split(";")) {
498
+ const eq = part.indexOf("=");
499
+ if (eq === -1) continue;
500
+ if (part.slice(0, eq).trim() === name) {
501
+ try {
502
+ return decodeURIComponent(part.slice(eq + 1).trim());
503
+ } catch {
504
+ return part.slice(eq + 1).trim();
505
+ }
506
+ }
507
+ }
508
+ return void 0;
509
+ }
510
+ function resolveLanguage(req, res) {
511
+ const queryLang = req.query?.lang;
512
+ if (isSupportedLanguage(queryLang)) {
513
+ res.setHeader("Set-Cookie", `${LANG_COOKIE}=${queryLang}; Path=/; Max-Age=31536000; SameSite=Lax`);
514
+ return queryLang;
515
+ }
516
+ const cookieLang = parseCookie(req.headers?.cookie, LANG_COOKIE);
517
+ if (isSupportedLanguage(cookieLang)) return cookieLang;
518
+ return DEFAULT_LANGUAGE;
519
+ }
520
+ function languageOptions(current) {
521
+ return SUPPORTED_LANGUAGES.map((code) => ({
522
+ code,
523
+ label: LANG_LABELS[code],
524
+ active: code === current
525
+ }));
526
+ }
527
+
528
+ // src/middleware/snippet.ts
529
+ function buildToolbarSnippet(token, routePrefix, lang) {
530
+ const t = createTranslator(lang);
531
+ const i18n = {
532
+ lastNRequests: t("toolbar.last_n_requests"),
533
+ viewAll: t("toolbar.view_all")
534
+ };
535
+ return `<link rel="stylesheet" href="${routePrefix}/assets/css/toolbar-standalone.css">
536
+ <div id="opticore-wdt-mount"></div>
537
+ <script>
538
+ (function () {
539
+ window.__opticoreProfilerToken = "${token}";
540
+ window.__opticoreProfilerRoutePrefix = "${routePrefix}";
541
+ window.__opticoreProfilerI18n = ${JSON.stringify(i18n)};
542
+ fetch("${routePrefix}/wdt/${token}", { credentials: "same-origin" })
543
+ .then(function (r) { return r.text(); })
544
+ .then(function (html) {
545
+ document.getElementById("opticore-wdt-mount").outerHTML = html;
546
+ let s = document.createElement("script");
547
+ s.src = "${routePrefix}/assets/js/toolbar.js";
548
+ document.body.appendChild(s);
549
+ })
550
+ .catch(function () {});
551
+ })();
552
+ </script>`;
553
+ }
554
+
555
+ // src/middleware/errorHandler.middleware.ts
556
+ function profilerErrorHandler() {
557
+ return function(err, _req, _res, next) {
558
+ getActiveCollector("exception")?.setError(err);
559
+ next(err);
560
+ };
561
+ }
562
+ var processHandlersRegistered = false;
563
+ function registerProcessErrorCapture() {
564
+ if (processHandlersRegistered) return;
565
+ processHandlersRegistered = true;
566
+ process.on("uncaughtExceptionMonitor", (err) => {
567
+ try {
568
+ getActiveCollector("exception")?.setError(err);
569
+ } catch {
570
+ }
571
+ });
572
+ process.on("unhandledRejection", (reason) => {
573
+ try {
574
+ const err = reason instanceof Error ? reason : new Error(String(reason));
575
+ getActiveCollector("exception")?.setError(err);
576
+ } catch {
577
+ }
578
+ });
579
+ }
580
+
581
+ // src/middleware/profiler.middleware.ts
582
+ var DEFAULT_ROUTE_PREFIX = "/_profiler";
583
+ var DEFAULT_MAX_REQUESTS = 10;
584
+ var DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
585
+ function isUnderPrefix(path2, prefix) {
586
+ return path2 === prefix || path2.startsWith(`${prefix}/`);
587
+ }
588
+ function isExcludedPath(path2, excludePaths) {
589
+ return excludePaths.some(
590
+ (pattern) => pattern.endsWith("/*") ? path2.startsWith(pattern.slice(0, -1)) : path2 === pattern
591
+ );
592
+ }
593
+ function opticoreProfiler(options) {
594
+ const enabled = options.enabled ?? false;
595
+ const routePrefix = options.routePrefix ?? DEFAULT_ROUTE_PREFIX;
596
+ const storage = options.storage;
597
+ const retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;
598
+ const maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
599
+ const excludePaths = options.excludePaths ?? [...DEFAULT_EXCLUDE_PATHS];
600
+ const security = {
601
+ sensitiveKeys: options.security?.sensitiveKeys ?? [...DEFAULT_SENSITIVE_KEYS],
602
+ maxUrlLength: options.security?.maxUrlLength ?? 100
603
+ };
604
+ const events = new EventEmitter();
605
+ events.setMaxListeners(0);
606
+ const registry = new CollectorRegistry();
607
+ if (options.collectors) {
608
+ for (const factory of options.collectors) registry.register(factory);
609
+ } else {
610
+ for (const factory of createBuiltinCollectorFactories(security)) registry.register(factory);
611
+ }
612
+ if (enabled) registerProcessErrorCapture();
613
+ const middleware = function opticoreProfilerMiddleware(req, res, next) {
614
+ if (!enabled || isUnderPrefix(req.path, routePrefix) || isExcludedPath(req.path, excludePaths)) {
615
+ next();
616
+ return;
617
+ }
618
+ const token = generateToken(8);
619
+ const startTime = Date.now();
620
+ const startHrTime = process.hrtime.bigint();
621
+ const heapUsedBefore = process.memoryUsage().heapUsed;
622
+ const collectors = registry.createAll();
623
+ res.setHeader("X-Debug-Token", token);
624
+ res.setHeader("X-Debug-Token-Link", `${routePrefix}/${token}`);
625
+ let responseBody;
626
+ const lang = resolveLanguage(req, res);
627
+ installHtmlInjection(res, () => buildToolbarSnippet(token, routePrefix, lang), (buffer, contentType) => {
628
+ responseBody = { buffer, contentType };
629
+ });
630
+ res.on("finish", () => {
631
+ void (async () => {
632
+ for (const collector of collectors.values()) {
633
+ try {
634
+ await collector.collect({ token, req, res, startTime, startHrTime, heapUsedBefore, responseBody });
635
+ } catch {
636
+ }
637
+ }
638
+ const requestData = collectors.get("request")?.getData();
639
+ const timeData = collectors.get("time")?.getData();
640
+ const profile = {
641
+ token,
642
+ ip: requestData?.request.ip ?? req.ip ?? "unknown",
643
+ method: requestData?.method ?? req.method,
644
+ url: requestData?.url ?? req.originalUrl ?? req.url,
645
+ statusCode: res.statusCode,
646
+ time: startTime,
647
+ duration: timeData?.duration ?? Date.now() - startTime,
648
+ contentType: res.getHeader("content-type"),
649
+ collectors: Object.fromEntries([...collectors].map(([name, c]) => [name, c.getData()]))
650
+ };
651
+ try {
652
+ await storage.write(profile);
653
+ events.emit("profile", profile);
654
+ if (retentionMs > 0) await storage.purge(retentionMs);
655
+ } catch {
656
+ }
657
+ })();
658
+ });
659
+ runWithProfilerContext({ token, collectors, startTime, startHrTime }, () => next());
660
+ };
661
+ middleware.registry = registry;
662
+ middleware.options = Object.freeze({ enabled, routePrefix, storage, retentionMs, maxRequests, excludePaths, security });
663
+ middleware.clear = () => storage.clear();
664
+ middleware.events = events;
665
+ return middleware;
666
+ }
667
+
668
+ // src/module.ts
669
+ import { join as join3 } from "path";
670
+ import { express } from "opticore-express";
671
+ import { OpticoreRoutingFactory } from "opticore-router";
672
+
673
+ // src/core/viewEngine.config.ts
674
+ import { join as join2 } from "path";
675
+ import nunjucks from "nunjucks";
676
+ var configured = false;
677
+ function configureProfilerViewEngine(app) {
678
+ if (configured) return;
679
+ configured = true;
680
+ const viewsDir = join2(__dirname, "views", "templates");
681
+ nunjucks.configure(viewsDir, {
682
+ autoescape: true,
683
+ express: app,
684
+ noCache: process.env.NODE_ENV !== "production"
685
+ });
686
+ app.set("views", viewsDir);
687
+ }
688
+
689
+ // src/core/routeIntrospection.ts
690
+ var _expressApp = null;
691
+ function setIntrospectedApp(app) {
692
+ _expressApp = app;
693
+ }
694
+ function getLayerPrefix(layer) {
695
+ try {
696
+ const src = layer.regexp?.source ?? "";
697
+ if (!src) return "";
698
+ if (src === "^\\/?(?=\\/|$)" || src === "^\\/(?=\\/|$)" || src === "^\\/?$" || src === "^\\/$") return "";
699
+ let s = src.startsWith("^") ? src.slice(1) : src;
700
+ const markers = ["\\/?(?=", "(?=\\/", "\\/?$", "(?=$)"];
701
+ let endIdx = s.length;
702
+ for (const m of markers) {
703
+ const i = s.indexOf(m);
704
+ if (i !== -1 && i < endIdx) endIdx = i;
705
+ }
706
+ s = s.slice(0, endIdx);
707
+ s = s.replace(/\\\//g, "/").replace(/\\\./g, ".");
708
+ if (s.endsWith("/?")) s = s.slice(0, -2);
709
+ return s && s !== "/" ? s : "";
710
+ } catch {
711
+ return "";
712
+ }
713
+ }
714
+ function extractRoutesFromApp(app) {
715
+ const result = [];
716
+ function traverse(stack, prefix) {
717
+ for (const layer of stack ?? []) {
718
+ if (!layer) continue;
719
+ if (layer.route) {
720
+ const methods = Object.keys(layer.route.methods ?? {}).filter((m) => layer.route.methods[m]).map((m) => m.toUpperCase());
721
+ const routePath = String(layer.route.path ?? "");
722
+ const fullPath = (prefix + routePath).replace(/\/+/g, "/") || "/";
723
+ for (const method of methods) {
724
+ result.push({
725
+ method,
726
+ path: fullPath,
727
+ middlewareCount: layer.route.stack?.length ?? 1
728
+ });
729
+ }
730
+ } else if (layer.handle && typeof layer.handle === "function" && layer.handle.stack) {
731
+ const seg = getLayerPrefix(layer);
732
+ traverse(layer.handle.stack, prefix + seg);
733
+ }
734
+ }
735
+ }
736
+ const router = app._router ?? app.router;
737
+ traverse(router?.stack ?? [], "");
738
+ return result;
739
+ }
740
+ function getExpressRoutes() {
741
+ if (!_expressApp) return [];
742
+ return extractRoutesFromApp(_expressApp);
743
+ }
744
+
745
+ // src/controllers/profiler.controller.ts
746
+ import { execSync } from "child_process";
747
+
748
+ // src/view/legacyAdapter.ts
749
+ function toLegacyProfile(profile) {
750
+ const requestData = profile.collectors.request;
751
+ const timeData = profile.collectors.time;
752
+ const memoryData = profile.collectors.memory;
753
+ const queries = profile.collectors.database ?? [];
754
+ const logs = [...profile.collectors.logger ?? []];
755
+ const exceptionData = profile.collectors.exception;
756
+ if (exceptionData?.error) {
757
+ logs.push({
758
+ level: "critical",
759
+ message: `${exceptionData.error.name}: ${exceptionData.error.message}`,
760
+ timestamp: profile.time,
761
+ context: { stack: exceptionData.error.stack }
762
+ });
763
+ }
764
+ return {
765
+ token: profile.token,
766
+ timestamp: profile.time,
767
+ method: requestData?.method ?? profile.method,
768
+ url: requestData?.url ?? profile.url,
769
+ statusCode: requestData?.statusCode ?? profile.statusCode,
770
+ statusMessage: requestData?.statusMessage ?? "",
771
+ duration: timeData?.duration ?? profile.duration,
772
+ memoryUsage: memoryData?.heapUsedAfter ?? 0,
773
+ queries,
774
+ logs,
775
+ route: requestData?.route ?? { path: profile.url, method: profile.method, params: {} },
776
+ request: requestData?.request ?? {
777
+ method: profile.method,
778
+ url: profile.url,
779
+ headers: {},
780
+ query: {},
781
+ body: {},
782
+ params: {},
783
+ ip: profile.ip
784
+ },
785
+ response: requestData?.response ?? { statusCode: profile.statusCode, statusMessage: "", headers: {}, contentType: profile.contentType },
786
+ performance: [],
787
+ nodeVersion: process.version,
788
+ appVersion: process.env.npm_package_version ?? "1.0.0",
789
+ environment: process.env.NODE_ENV ?? "development"
790
+ };
791
+ }
792
+
793
+ // src/core/render.util.ts
794
+ function renderView(req, res, view, data = {}) {
795
+ const lang = resolveLanguage(req, res);
796
+ const t = createTranslator(lang);
797
+ const fullData = { ...data, t, lang, languages: languageOptions(lang), safeHtml };
798
+ return new Promise((resolve, reject) => {
799
+ res.render(view, fullData, (err, html) => {
800
+ if (err) {
801
+ reject(err);
802
+ return;
803
+ }
804
+ res.setHeader("Cache-Control", "no-store");
805
+ res.send(html);
806
+ resolve();
807
+ });
808
+ });
809
+ }
810
+
811
+ // src/view/helpers.view.ts
812
+ function statusClass(code) {
813
+ if (code >= 500) return "s-error";
814
+ if (code >= 400) return "s-warn";
815
+ if (code >= 300) return "s-redirect";
816
+ return "s-ok";
817
+ }
818
+ function formatDuration(ms) {
819
+ return ms < 1e3 ? `${ms} ms` : `${(ms / 1e3).toFixed(2)} s`;
820
+ }
821
+ function formatMemory(bytes) {
822
+ return `${(Math.max(0, bytes) / 1024 / 1024).toFixed(1)} MiB`;
823
+ }
824
+ function sqlTotalTime(profile) {
825
+ return profile.queries.reduce((sum, q) => sum + q.duration, 0);
826
+ }
827
+ function highlightJson(value) {
828
+ const pretty = JSON.stringify(value, null, 2);
829
+ return pretty.split("\n").map((line) => {
830
+ const m = line.match(/^(\s*)"((?:[^"\\]|\\.)*)":\s*(.*)$/);
831
+ if (!m) return escapeHtml(line);
832
+ const [, indent, key, rest] = m;
833
+ return `${indent}<span class="json-key">"${escapeHtml(key)}"</span>: <span class="json-val">${escapeHtml(rest)}</span>`;
834
+ }).join("\n");
835
+ }
836
+
837
+ // src/view/toolbar.view.ts
838
+ function computeMetrics(profile, httpCount) {
839
+ return {
840
+ sqlTotalTime: sqlTotalTime(profile),
841
+ sqlCount: profile.queries.length,
842
+ logCount: profile.logs.length,
843
+ logErrors: profile.logs.filter((l) => l.level === "error" || l.level === "critical").length,
844
+ logWarnings: profile.logs.filter((l) => l.level === "warning").length,
845
+ logDeprecations: profile.logs.filter((l) => l.level === "deprecation").length,
846
+ httpCount,
847
+ memoryFormatted: formatMemory(profile.memoryUsage),
848
+ durationFormatted: formatDuration(profile.duration),
849
+ statusClass: statusClass(profile.statusCode)
850
+ };
851
+ }
852
+ function truncateUrl(url, maxLen) {
853
+ if (url.length <= maxLen) return url;
854
+ return url.slice(0, maxLen - 1) + "\u2026";
855
+ }
856
+ function isApiRequest(profile) {
857
+ return !(profile.response.contentType ?? "").toLowerCase().startsWith("text/html");
858
+ }
859
+ function buildToolbarBarViewModel(profile, recentProfiles, security, routePrefix, maxRequests) {
860
+ const apiProfiles = recentProfiles.filter(isApiRequest);
861
+ const metrics = computeMetrics(profile, apiProfiles.length);
862
+ const base = `${routePrefix}/${profile.token}`;
863
+ const httpRows = apiProfiles.slice(0, maxRequests).map((p) => ({
864
+ isCurrent: p.token === profile.token,
865
+ method: p.method,
866
+ url: truncateUrl(p.url, security.maxUrlLength),
867
+ fullUrl: p.url,
868
+ token: p.token,
869
+ statusCode: p.statusCode,
870
+ statusClass: p.statusCode >= 400 ? "tip-s-err" : p.statusCode >= 300 ? "tip-s-redir" : "tip-s-ok",
871
+ duration: formatDuration(p.duration)
872
+ }));
873
+ return {
874
+ statusCode: profile.statusCode,
875
+ statusMessage: profile.statusMessage,
876
+ profilerUrl: base,
877
+ statusCls: metrics.statusClass,
878
+ appVersion: profile.appVersion,
879
+ nodeVersion: profile.nodeVersion,
880
+ environment: profile.environment,
881
+ method: profile.method,
882
+ url: profile.url,
883
+ token: profile.token,
884
+ base,
885
+ duration: {
886
+ formatted: metrics.durationFormatted,
887
+ barWidth: Math.min(metrics.sqlTotalTime / 2, 100),
888
+ appFormatted: metrics.durationFormatted,
889
+ dbFormatted: metrics.sqlCount > 0 ? formatDuration(metrics.sqlTotalTime) : "0 ms"
890
+ },
891
+ memory: { formatted: metrics.memoryFormatted },
892
+ logs: { count: metrics.logCount, errors: metrics.logErrors, warnings: metrics.logWarnings, deprecations: metrics.logDeprecations },
893
+ route: { label: profile.route.path || profile.url, method: profile.route.method || profile.method },
894
+ http: { count: metrics.httpCount, rows: httpRows }
895
+ };
896
+ }
897
+
898
+ // src/view/profilerList.view.ts
899
+ function statusClassOf(code) {
900
+ if (code >= 400) return "error";
901
+ if (code >= 300) return "redirect";
902
+ return "ok";
903
+ }
904
+ function fmtDate(ts) {
905
+ return new Date(ts).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
906
+ }
907
+ function fmtClock(ts) {
908
+ return new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
909
+ }
910
+ function toProfilerListRow(p) {
911
+ return {
912
+ token: p.token,
913
+ tokenShort: p.token.slice(0, 6),
914
+ statusCode: p.statusCode,
915
+ statusClass: statusClassOf(p.statusCode),
916
+ method: p.method,
917
+ url: p.url,
918
+ dateStr: fmtDate(p.timestamp),
919
+ timeStr: fmtClock(p.timestamp),
920
+ duration: p.duration,
921
+ contentType: p.response.contentType
922
+ };
923
+ }
924
+ function tabHref(tab, search, method, status, ip, token, from, until, limit) {
925
+ const params = new URLSearchParams();
926
+ if (search) params.set("search", search);
927
+ if (method) params.set("method", method);
928
+ if (status) params.set("status", status);
929
+ if (ip) params.set("ip", ip);
930
+ if (token) params.set("token", token);
931
+ if (from) params.set("from", from);
932
+ if (until) params.set("until", until);
933
+ if (limit !== 10) params.set("limit", String(limit));
934
+ if (tab !== "requests") params.set("tab", tab);
935
+ const qs = params.toString();
936
+ return `/_profiler${qs ? "?" + qs : ""}`;
937
+ }
938
+ function buildProfilerListViewModel(profiles, search = "", method = "", status = "", limit = 10, ip = "", token = "", from = "", until = "", tab = "requests") {
939
+ let filtered = [...profiles];
940
+ if (ip) {
941
+ filtered = filtered.filter((p) => (p.request?.ip ?? "").toLowerCase().includes(ip.toLowerCase()));
942
+ }
943
+ if (token) {
944
+ filtered = filtered.filter((p) => p.token.toLowerCase().startsWith(token.toLowerCase()));
945
+ }
946
+ if (search) {
947
+ const q = search.toLowerCase();
948
+ filtered = filtered.filter(
949
+ (p) => p.url.toLowerCase().includes(q) || p.method.toLowerCase().includes(q) || p.token.toLowerCase().includes(q) || (p.request?.ip ?? "").toLowerCase().includes(q) || String(p.statusCode).includes(q)
950
+ );
951
+ }
952
+ if (method) {
953
+ filtered = filtered.filter((p) => p.method === method.toUpperCase());
954
+ }
955
+ if (status) {
956
+ const code = parseInt(status, 10);
957
+ if (code >= 100) {
958
+ filtered = filtered.filter((p) => p.statusCode === code);
959
+ } else if (code >= 1 && code <= 9) {
960
+ const prefix = code * 100;
961
+ filtered = filtered.filter((p) => p.statusCode >= prefix && p.statusCode < prefix + 100);
962
+ }
963
+ }
964
+ if (from) {
965
+ const ts = new Date(from).getTime();
966
+ if (!isNaN(ts)) filtered = filtered.filter((p) => p.timestamp >= ts);
967
+ }
968
+ if (until) {
969
+ const ts = new Date(until).getTime() + 86399999;
970
+ if (!isNaN(ts)) filtered = filtered.filter((p) => p.timestamp <= ts);
971
+ }
972
+ filtered = filtered.slice(0, limit);
973
+ const isFiltered = !!(search || method || status || ip || token || from || until);
974
+ const rows = filtered.map(toProfilerListRow);
975
+ return {
976
+ tab,
977
+ resultCount: filtered.length,
978
+ rows,
979
+ autoReload: !isFiltered && tab === "requests",
980
+ filters: {
981
+ ip,
982
+ status,
983
+ search,
984
+ token,
985
+ from,
986
+ until,
987
+ hasProfiles: profiles.length > 0,
988
+ methodOptions: ["Any", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"].map((m) => {
989
+ const value = m === "Any" ? "" : m;
990
+ return { value, label: m, selected: method === value };
991
+ }),
992
+ limitOptions: [10, 25, 50, 100].map((n) => ({ value: n, selected: limit === n }))
993
+ },
994
+ tabs: {
995
+ requests: { href: tabHref("requests", search, method, status, ip, token, from, until, limit), active: tab === "requests" },
996
+ commands: { href: tabHref("commands", search, method, status, ip, token, from, until, limit), active: tab === "commands" }
997
+ }
998
+ };
999
+ }
1000
+
1001
+ // src/view/profilerDetail.view.ts
1002
+ function toKvRows(obj) {
1003
+ return Object.entries(obj).map(([key, v]) => ({
1004
+ key,
1005
+ value: typeof v === "object" && v !== null ? JSON.stringify(v, null, 2) : String(v)
1006
+ }));
1007
+ }
1008
+ function headersToRows(headers) {
1009
+ return Object.entries(headers ?? {}).map(([key, value]) => ({ key, value }));
1010
+ }
1011
+ function buildCurlCommand(profile) {
1012
+ const headers = profile.request.headers ?? {};
1013
+ const host = headers.host ?? profile.request.hostname ?? "localhost";
1014
+ const protocol = profile.request.protocol ?? "http";
1015
+ const url = `${protocol}://${host}${profile.url}`;
1016
+ const lines = ["curl \\", " --compressed \\", ` --url '${url}' \\`];
1017
+ for (const [name, value] of Object.entries(headers)) {
1018
+ const k = name.toLowerCase();
1019
+ if (k === "host" || k === "content-length" || k === "cookie") continue;
1020
+ lines.push(` --header '${name}: ${value}' \\`);
1021
+ }
1022
+ if (headers.cookie) lines.push(` --cookie '${headers.cookie}' \\`);
1023
+ const body = profile.request.body ?? {};
1024
+ if (Object.keys(body).length > 0) {
1025
+ lines.push(` --data '${JSON.stringify(body)}' \\`);
1026
+ }
1027
+ const last = lines.pop();
1028
+ lines.push(last.replace(/ \\$/, ""));
1029
+ return lines.join("\n");
1030
+ }
1031
+ function buildRequestPanel(profile) {
1032
+ const cookies = profile.request.cookies ?? {};
1033
+ const responseCookies = profile.response.cookies ?? {};
1034
+ const body = profile.request.body ?? {};
1035
+ const hasBody = Object.keys(body).length > 0;
1036
+ const headers = profile.request.headers ?? {};
1037
+ return {
1038
+ statusOk: profile.statusCode < 400,
1039
+ statusCode: profile.statusCode,
1040
+ durationFormatted: formatDuration(profile.duration),
1041
+ memoryFormatted: formatMemory(profile.memoryUsage),
1042
+ sqlQueryCount: profile.queries.length,
1043
+ getParams: toKvRows(profile.request.query ?? {}),
1044
+ postParamsJson: hasBody ? highlightJson(body) : null,
1045
+ uploadedFiles: [],
1046
+ routeParams: toKvRows(profile.request.params ?? {}),
1047
+ attributes: [
1048
+ { key: "_method", value: profile.method },
1049
+ { key: "_route", value: profile.route.path },
1050
+ { key: "_controller", value: profile.route.controller ?? "unknown" },
1051
+ { key: "_ip", value: profile.request.ip },
1052
+ { key: "_hostname", value: profile.request.hostname ?? "localhost" },
1053
+ { key: "_protocol", value: profile.request.protocol ?? "http" }
1054
+ ],
1055
+ requestHeaders: headersToRows(headers),
1056
+ responseHeaders: headersToRows(profile.response.headers),
1057
+ hasCookies: Object.keys(cookies).length > 0,
1058
+ cookies: toKvRows(cookies),
1059
+ hasResponseCookies: Object.keys(responseCookies).length > 0,
1060
+ responseCookies: toKvRows(responseCookies),
1061
+ requestContentJson: hasBody ? highlightJson(body) : null,
1062
+ responseBodyJson: profile.response.body ? highlightJson(profile.response.body) : null,
1063
+ retryCommandCurl: buildCurlCommand(profile),
1064
+ serverParams: [
1065
+ { key: "HTTP_HOST", value: headers.host ?? profile.request.hostname ?? "localhost" },
1066
+ { key: "HTTP_USER_AGENT", value: headers["user-agent"] ?? "" },
1067
+ { key: "REQUEST_METHOD", value: profile.method },
1068
+ { key: "REQUEST_URI", value: profile.url },
1069
+ { key: "SERVER_PROTOCOL", value: (profile.request.protocol ?? "http").toUpperCase() },
1070
+ { key: "REMOTE_ADDR", value: profile.request.ip },
1071
+ { key: "NODE_ENV", value: profile.environment },
1072
+ { key: "NODE_VERSION", value: profile.nodeVersion },
1073
+ { key: "APP_VERSION", value: profile.appVersion }
1074
+ ],
1075
+ hasSession: profile.request.session !== void 0,
1076
+ session: toKvRows(profile.request.session ?? {})
1077
+ };
1078
+ }
1079
+ function buildPerformancePanel(profile) {
1080
+ const total = profile.duration;
1081
+ const sqlTime = sqlTotalTime(profile);
1082
+ const appTime = Math.max(0, total - sqlTime);
1083
+ const rawEvents = [
1084
+ { label: "kernel.request (Application)", duration: appTime, color: "#C87A3C" },
1085
+ ...sqlTime > 0 ? [{ label: "kernel.response (Database SQL)", duration: sqlTime, color: "#2D6A4A" }] : []
1086
+ ].filter((e) => e.duration > 0);
1087
+ const events = rawEvents.map((e) => {
1088
+ const pct = total > 0 ? Math.min(e.duration / total * 100, 100) : 0;
1089
+ return {
1090
+ label: e.label,
1091
+ widthPct: Math.max(pct, 0.5).toFixed(1),
1092
+ color: e.color,
1093
+ durationFormatted: formatDuration(e.duration)
1094
+ };
1095
+ });
1096
+ return {
1097
+ total,
1098
+ appTime,
1099
+ memoryMiB: (Math.max(0, profile.memoryUsage) / 1024 / 1024).toFixed(2),
1100
+ hasEvents: events.length > 0,
1101
+ events,
1102
+ hasSqlTime: sqlTime > 0,
1103
+ processInfo: [
1104
+ { key: "Node.js version", value: profile.nodeVersion },
1105
+ { key: "App version", value: profile.appVersion },
1106
+ { key: "Environment", badge: { text: profile.environment, variant: profile.environment === "production" ? "warn" : "info" } },
1107
+ { key: "Process PID", value: String(process.pid) },
1108
+ { key: "Platform", value: process.platform },
1109
+ { key: "Heap used", value: formatMemory(process.memoryUsage().heapUsed) },
1110
+ { key: "Heap total", value: formatMemory(process.memoryUsage().heapTotal) },
1111
+ { key: "RSS", value: formatMemory(process.memoryUsage().rss) },
1112
+ { key: "Uptime", value: `${Math.round(process.uptime())}s` }
1113
+ ]
1114
+ };
1115
+ }
1116
+ function sqlType(sql) {
1117
+ const t = sql.trimStart().toUpperCase().slice(0, 6);
1118
+ if (t.startsWith("SELECT")) return "SELECT";
1119
+ if (t.startsWith("INSERT")) return "INSERT";
1120
+ if (t.startsWith("UPDATE")) return "UPDATE";
1121
+ if (t.startsWith("DELETE")) return "DELETE";
1122
+ return "OTHER";
1123
+ }
1124
+ function buildDatabasePanel(profile) {
1125
+ const queries = profile.queries;
1126
+ const totalTime = sqlTotalTime(profile);
1127
+ return {
1128
+ hasQueries: queries.length > 0,
1129
+ totalCount: queries.length,
1130
+ totalTimeFormatted: formatDuration(totalTime),
1131
+ avgFormatted: queries.length > 0 ? formatDuration(Math.round(totalTime / queries.length)) : "0 ms",
1132
+ queries: queries.map((q, i) => ({
1133
+ type: sqlType(q.sql),
1134
+ index: i + 1,
1135
+ hasError: !!q.error,
1136
+ durationFormatted: formatDuration(q.duration),
1137
+ sql: q.sql,
1138
+ bindingsJson: q.bindings && q.bindings.length > 0 ? JSON.stringify(q.bindings) : null,
1139
+ error: q.error ?? null
1140
+ }))
1141
+ };
1142
+ }
1143
+ function buildLogsPanel(profile) {
1144
+ const logs = profile.logs;
1145
+ if (logs.length === 0) {
1146
+ return { hasLogs: false, errCount: 0, warnCount: 0, deprCount: 0, rows: [] };
1147
+ }
1148
+ const errCount = logs.filter((l) => l.level === "error" || l.level === "critical").length;
1149
+ const warnCount = logs.filter((l) => l.level === "warning").length;
1150
+ const deprCount = logs.filter((l) => l.level === "deprecation").length;
1151
+ function levelCls(level) {
1152
+ if (level === "critical") return "log-error";
1153
+ if (level === "deprecation") return "log-deprecation";
1154
+ return `log-${level}`;
1155
+ }
1156
+ function rowBorderCls(level) {
1157
+ if (level === "error" || level === "critical") return "log-row-error";
1158
+ if (level === "warning") return "log-row-warning";
1159
+ if (level === "deprecation") return "log-row-deprecation";
1160
+ return "";
1161
+ }
1162
+ function logType(level) {
1163
+ if (level === "error" || level === "critical") return "error";
1164
+ if (level === "warning") return "warning";
1165
+ if (level === "deprecation") return "deprecation";
1166
+ return "other";
1167
+ }
1168
+ const rows = logs.map((l, i) => {
1169
+ const ctx = l.context ?? {};
1170
+ const hasCtx = Object.keys(ctx).length > 0;
1171
+ const hasStack = hasCtx && typeof ctx.stack === "string" && ctx.stack.length > 0;
1172
+ const source = hasCtx && typeof ctx.source === "string" ? ctx.source : "";
1173
+ const ctxId = `lctx${i}`;
1174
+ const stackId = `lstk${i}`;
1175
+ const ms = String(l.timestamp % 1e3).padStart(3, "0");
1176
+ const timeStr = new Date(l.timestamp).toLocaleTimeString("en-US", {
1177
+ hour: "2-digit",
1178
+ minute: "2-digit",
1179
+ second: "2-digit",
1180
+ hour12: true
1181
+ }) + "." + ms;
1182
+ let extra = null;
1183
+ if (hasStack) {
1184
+ const stack = ctx.stack;
1185
+ const ctxWithoutStack = {};
1186
+ for (const [k, v] of Object.entries(ctx)) {
1187
+ if (k !== "stack") ctxWithoutStack[k] = v;
1188
+ }
1189
+ const hasOtherCtx = Object.keys(ctxWithoutStack).length > 0;
1190
+ extra = {
1191
+ showContextBtn: hasOtherCtx,
1192
+ showTraceBtn: true,
1193
+ ctxId,
1194
+ stackId,
1195
+ ctxJson: hasOtherCtx ? JSON.stringify(ctxWithoutStack, null, 2) : null,
1196
+ stackText: stack
1197
+ };
1198
+ } else if (hasCtx) {
1199
+ extra = {
1200
+ showContextBtn: true,
1201
+ showTraceBtn: false,
1202
+ ctxId,
1203
+ stackId,
1204
+ ctxJson: JSON.stringify(ctx, null, 2),
1205
+ stackText: null
1206
+ };
1207
+ }
1208
+ return {
1209
+ rowClass: rowBorderCls(l.level),
1210
+ logType: logType(l.level),
1211
+ timeStr,
1212
+ levelCls: levelCls(l.level),
1213
+ level: l.level,
1214
+ message: l.message,
1215
+ source,
1216
+ extra
1217
+ };
1218
+ });
1219
+ return { hasLogs: true, errCount, warnCount, deprCount, rows };
1220
+ }
1221
+ function buildRoutingPanel(profile) {
1222
+ const routeParams = profile.route.params ?? {};
1223
+ const queryParams = profile.request.query ?? {};
1224
+ return {
1225
+ routeRows: [
1226
+ { key: "Route", value: profile.route.path, code: true },
1227
+ { key: "Method", badge: { text: profile.route.method, variant: "info" } },
1228
+ { key: "Controller", value: profile.route.controller ?? "unknown" },
1229
+ { key: "Full URL", value: profile.request.url, code: true }
1230
+ ],
1231
+ hasRouteParams: Object.keys(routeParams).length > 0,
1232
+ routeParamsRows: Object.entries(routeParams).map(([key, v]) => ({ key, value: String(v), code: true })),
1233
+ hasQueryParams: Object.keys(queryParams).length > 0,
1234
+ queryParamsRows: Object.entries(queryParams).map(([key, v]) => ({ key, value: String(v), code: true }))
1235
+ };
1236
+ }
1237
+ function buildConfigurationPanel(profile) {
1238
+ const safeEnv = Object.entries(process.env).filter(([k]) => !/(PASSWORD|SECRET|KEY|TOKEN|AUTH|PASS|PRIVATE)/i.test(k)).sort(([a], [b]) => a.localeCompare(b));
1239
+ return {
1240
+ appInfo: [
1241
+ { key: "App Version", value: profile.appVersion },
1242
+ { key: "Node.js", value: profile.nodeVersion },
1243
+ { key: "Environment", badge: { text: profile.environment, variant: profile.environment === "production" ? "warn" : "info" } },
1244
+ { key: "Platform", value: `${process.platform} (${process.arch})` }
1245
+ ],
1246
+ envRows: safeEnv.map(([key, v]) => ({ key, value: v ?? "", title: v ?? "", truncate: true })),
1247
+ envCount: safeEnv.length
1248
+ };
1249
+ }
1250
+ function buildExceptionPanel(profile) {
1251
+ const errLogs = profile.logs.filter((l) => l.level === "error" || l.level === "critical");
1252
+ const isError = profile.statusCode >= 500;
1253
+ const isNotFound = profile.statusCode === 404;
1254
+ return {
1255
+ noException: errLogs.length === 0 && !isError,
1256
+ showBanner: isError || isNotFound,
1257
+ statusCode: profile.statusCode,
1258
+ statusMessage: profile.statusMessage,
1259
+ method: profile.method,
1260
+ url: profile.url,
1261
+ errLogs: errLogs.map((l) => ({
1262
+ levelUpper: l.level.toUpperCase(),
1263
+ message: l.message,
1264
+ contextJson: l.context ? highlightJson(l.context) : null
1265
+ }))
1266
+ };
1267
+ }
1268
+ function pathToSegments(path2) {
1269
+ const segments = [];
1270
+ const re = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;
1271
+ let lastIndex = 0;
1272
+ let m;
1273
+ while ((m = re.exec(path2)) !== null) {
1274
+ if (m.index > lastIndex) segments.push({ type: "text", value: path2.slice(lastIndex, m.index) });
1275
+ segments.push({ type: "param", value: m[1] });
1276
+ lastIndex = re.lastIndex;
1277
+ }
1278
+ if (lastIndex < path2.length) segments.push({ type: "text", value: path2.slice(lastIndex) });
1279
+ return segments;
1280
+ }
1281
+ function methodClassOf(method) {
1282
+ return ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"].includes(method) ? `rm-${method}` : "rm-ALL";
1283
+ }
1284
+ function toRouteRows(routes) {
1285
+ return routes.map((r) => ({
1286
+ methodClass: methodClassOf(r.method),
1287
+ method: r.method,
1288
+ pathSegments: pathToSegments(r.path),
1289
+ middlewareCount: r.middlewareCount
1290
+ }));
1291
+ }
1292
+ function buildRoutesPanel(routes) {
1293
+ if (routes.length === 0) {
1294
+ return {
1295
+ hasRoutes: false,
1296
+ totalRoutes: 0,
1297
+ appRoutesCount: 0,
1298
+ methodCounts: [],
1299
+ hasAppRoutes: false,
1300
+ appRoutesRows: [],
1301
+ hasDebugRoutes: false,
1302
+ debugRoutesRows: []
1303
+ };
1304
+ }
1305
+ const methods = [...new Set(routes.map((r) => r.method))].sort();
1306
+ const appRoutes = routes.filter((r) => !r.path.startsWith("/_profiler") && r.path !== "/");
1307
+ const debugRoutes = routes.filter((r) => r.path.startsWith("/_profiler") || r.path === "/");
1308
+ return {
1309
+ hasRoutes: true,
1310
+ totalRoutes: routes.length,
1311
+ appRoutesCount: appRoutes.length,
1312
+ methodCounts: methods.map((m) => ({ method: m, count: routes.filter((r) => r.method === m).length })),
1313
+ hasAppRoutes: appRoutes.length > 0,
1314
+ appRoutesRows: toRouteRows(appRoutes),
1315
+ hasDebugRoutes: debugRoutes.length > 0,
1316
+ debugRoutesRows: toRouteRows(debugRoutes)
1317
+ };
1318
+ }
1319
+ function buildProfilerDetailViewModel(profile, panelName = "request", appRoutes = []) {
1320
+ const logErrors = profile.logs.filter((l) => l.level === "error" || l.level === "critical").length;
1321
+ const hasException = profile.statusCode >= 400 || logErrors > 0;
1322
+ let panelData;
1323
+ switch (panelName) {
1324
+ case "performance":
1325
+ panelData = buildPerformancePanel(profile);
1326
+ break;
1327
+ case "logs":
1328
+ panelData = buildLogsPanel(profile);
1329
+ break;
1330
+ case "routing":
1331
+ panelData = buildRoutingPanel(profile);
1332
+ break;
1333
+ case "configuration":
1334
+ panelData = buildConfigurationPanel(profile);
1335
+ break;
1336
+ case "database":
1337
+ panelData = buildDatabasePanel(profile);
1338
+ break;
1339
+ case "exception":
1340
+ panelData = buildExceptionPanel(profile);
1341
+ break;
1342
+ case "routes":
1343
+ panelData = buildRoutesPanel(appRoutes);
1344
+ break;
1345
+ default:
1346
+ panelData = buildRequestPanel(profile);
1347
+ }
1348
+ let banner;
1349
+ if (profile.statusCode >= 500) {
1350
+ banner = { bg: "#fcebec", border: "#e1142d", code: "#e1142d", msg: "#d98a8f", isError: true };
1351
+ } else if (profile.statusCode >= 400) {
1352
+ banner = { bg: "#fcebec", border: "#e1142d", code: "#e1142d", msg: "#d98a8f", isError: true };
1353
+ } else if (profile.statusCode >= 300) {
1354
+ banner = { bg: "#e3edf8", border: "#1565c0", code: "#1565c0", msg: "#6a9fd8", isError: false };
1355
+ } else {
1356
+ banner = { bg: "#e8f4ec", border: "#2e7d32", code: "#2e7d32", msg: "#6aaa6a", isError: false };
1357
+ }
1358
+ const panelLabel = {
1359
+ request: "panel.request",
1360
+ performance: "panel.performance",
1361
+ logs: "panel.logs",
1362
+ routing: "panel.routing",
1363
+ configuration: "panel.configuration",
1364
+ database: "panel.database",
1365
+ exception: "panel.exception",
1366
+ routes: "panel.routes"
1367
+ };
1368
+ const appRoutesForBadge = appRoutes.filter((r) => !r.path.startsWith("/_profiler") && r.path !== "/").length;
1369
+ const sidebarItems = [
1370
+ { icon: "route", label: "panel.request", panel: "request", active: panelName === "request", badge: 0, badgeIsError: false },
1371
+ { icon: "perf", label: "panel.performance", panel: "performance", active: panelName === "performance", badge: 0, badgeIsError: false },
1372
+ {
1373
+ icon: "exception",
1374
+ label: "panel.exception",
1375
+ panel: "exception",
1376
+ active: panelName === "exception",
1377
+ badge: hasException ? profile.statusCode >= 400 ? 1 : logErrors : 0,
1378
+ badgeIsError: true
1379
+ },
1380
+ {
1381
+ icon: "log",
1382
+ label: "panel.logs",
1383
+ panel: "logs",
1384
+ active: panelName === "logs",
1385
+ badge: profile.logs.length,
1386
+ badgeIsError: logErrors > 0
1387
+ },
1388
+ {
1389
+ icon: "map",
1390
+ label: "panel.routes",
1391
+ panel: "routes",
1392
+ active: panelName === "routes",
1393
+ badge: appRoutesForBadge,
1394
+ badgeIsError: false
1395
+ },
1396
+ { icon: "route", label: "panel.routing", panel: "routing", active: panelName === "routing", badge: 0, badgeIsError: false },
1397
+ {
1398
+ icon: "db",
1399
+ label: "panel.database",
1400
+ panel: "database",
1401
+ active: panelName === "database",
1402
+ badge: profile.queries.length,
1403
+ badgeIsError: false
1404
+ },
1405
+ { icon: "config", label: "panel.configuration", panel: "configuration", active: panelName === "configuration", badge: 0, badgeIsError: false }
1406
+ ];
1407
+ return {
1408
+ profile: {
1409
+ method: profile.method,
1410
+ url: profile.url,
1411
+ statusCode: profile.statusCode,
1412
+ statusMessage: profile.statusMessage,
1413
+ ip: profile.request.ip,
1414
+ date: new Date(profile.timestamp).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
1415
+ time: new Date(profile.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" }),
1416
+ token6: profile.token.slice(0, 6)
1417
+ },
1418
+ banner,
1419
+ sidebarItems,
1420
+ panelName,
1421
+ panelLabel: panelLabel[panelName],
1422
+ panelData
1423
+ };
1424
+ }
1425
+
1426
+ // src/controllers/profiler.controller.ts
1427
+ var SSE_HEARTBEAT_MS = 15e3;
1428
+ var PANELS = [
1429
+ "request",
1430
+ "performance",
1431
+ "logs",
1432
+ "routing",
1433
+ "configuration",
1434
+ "database",
1435
+ "exception",
1436
+ "routes"
1437
+ ];
1438
+ function isValidPanel(p) {
1439
+ return typeof p === "string" && PANELS.includes(p);
1440
+ }
1441
+ function getNpmVersion() {
1442
+ try {
1443
+ return execSync("npm --version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1444
+ } catch {
1445
+ return "\u2014";
1446
+ }
1447
+ }
1448
+ function createProfilerController(profiler) {
1449
+ const { storage, routePrefix, security, maxRequests, enabled } = profiler.options;
1450
+ async function recentLegacyProfiles(limit) {
1451
+ const profiles = await storage.find(limit);
1452
+ return profiles.map(toLegacyProfile);
1453
+ }
1454
+ return {
1455
+ async home(req, res) {
1456
+ const appVersion = process.env.npm_package_version ?? "1.0.0";
1457
+ const nodeVersion = process.version;
1458
+ const npmVersion = getNpmVersion();
1459
+ const environment = process.env.NODE_ENV ?? "development";
1460
+ await renderView(req, res, "home.njk", {
1461
+ appVersion,
1462
+ nodeVersion,
1463
+ environment,
1464
+ port: process.env.APP_PORT ?? "4200",
1465
+ host: process.env.APP_HOST ?? "localhost",
1466
+ showToolbar: enabled,
1467
+ isProd: environment === "production"
1468
+ });
1469
+ },
1470
+ async wdt(req, res) {
1471
+ const raw = await storage.read(req.params.token);
1472
+ if (!raw) {
1473
+ res.status(404).send("");
1474
+ return;
1475
+ }
1476
+ const profile = toLegacyProfile(raw);
1477
+ const recent = await recentLegacyProfiles(maxRequests);
1478
+ const vm = buildToolbarBarViewModel(profile, recent, security, routePrefix, maxRequests);
1479
+ await renderView(req, res, "toolbar-wdt.njk", { ...vm, profilerBase: routePrefix });
1480
+ },
1481
+ async list(req, res) {
1482
+ const qs = req.query;
1483
+ const str = (k) => typeof qs[k] === "string" ? qs[k] : "";
1484
+ const search = str("search");
1485
+ const method = str("method");
1486
+ const status = str("status");
1487
+ const ip = str("ip");
1488
+ const token = str("token");
1489
+ const from = str("from");
1490
+ const until = str("until");
1491
+ const limitRaw = typeof qs.limit === "string" ? parseInt(qs.limit, 10) : 10;
1492
+ const limit = [10, 25, 50, 100].includes(limitRaw) ? limitRaw : 10;
1493
+ const tab = qs.tab === "commands" ? "commands" : "requests";
1494
+ const all = await recentLegacyProfiles(1e3);
1495
+ await renderView(req, res, "profiler-list.njk", buildProfilerListViewModel(
1496
+ all,
1497
+ search,
1498
+ method,
1499
+ status,
1500
+ limit,
1501
+ ip,
1502
+ token,
1503
+ from,
1504
+ until,
1505
+ tab
1506
+ ));
1507
+ },
1508
+ /**
1509
+ * Server-Sent Events stream: pushes one "profile" event per request
1510
+ * the server finishes profiling, formatted exactly like a
1511
+ * profiler-list.njk row. Replaces the old `setTimeout(location.reload)`
1512
+ * polling on the list page — the client only re-renders when the
1513
+ * server actually has something new, nothing is polled.
1514
+ */
1515
+ stream(req, res) {
1516
+ res.setHeader("Content-Type", "text/event-stream");
1517
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1518
+ res.setHeader("Connection", "keep-alive");
1519
+ res.setHeader("X-Accel-Buffering", "no");
1520
+ res.flushHeaders?.();
1521
+ const send = (event, data) => {
1522
+ res.write(`event: ${event}
1523
+ data: ${JSON.stringify(data)}
1524
+
1525
+ `);
1526
+ };
1527
+ const onProfile = (profile) => {
1528
+ send("profile", toProfilerListRow(toLegacyProfile(profile)));
1529
+ };
1530
+ profiler.events.on("profile", onProfile);
1531
+ const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), SSE_HEARTBEAT_MS);
1532
+ req.on("close", () => {
1533
+ clearInterval(heartbeat);
1534
+ profiler.events.off("profile", onProfile);
1535
+ });
1536
+ },
1537
+ async latestRedirect(_req, res) {
1538
+ const [latest] = await storage.find(1);
1539
+ if (!latest) {
1540
+ res.redirect(routePrefix);
1541
+ return;
1542
+ }
1543
+ res.redirect(`${routePrefix}/${latest.token}`);
1544
+ },
1545
+ async detail(req, res) {
1546
+ const raw = await storage.read(req.params.token);
1547
+ if (!raw) {
1548
+ await renderView(req, res, "message.njk", {
1549
+ titleKey: "message.not_found_title",
1550
+ headingKey: "message.not_found_heading",
1551
+ messageKey: "message.not_found_body",
1552
+ messageParams: { token: req.params.token },
1553
+ link: { href: routePrefix, labelKey: "message.back_to_list" }
1554
+ });
1555
+ return;
1556
+ }
1557
+ const profile = toLegacyProfile(raw);
1558
+ const rawPanel = req.query.panel;
1559
+ const panel = isValidPanel(rawPanel) ? rawPanel : "request";
1560
+ await renderView(
1561
+ req,
1562
+ res,
1563
+ "profiler-detail.njk",
1564
+ buildProfilerDetailViewModel(profile, panel, getExpressRoutes())
1565
+ );
1566
+ }
1567
+ };
1568
+ }
1569
+
1570
+ // src/module.ts
1571
+ function registerProfilerViews(app, profiler) {
1572
+ setIntrospectedApp(app);
1573
+ configureProfilerViewEngine(app);
1574
+ app.use(`${profiler.options.routePrefix}/assets`, express.static(join3(__dirname, "assets"), { maxAge: 0, etag: true }));
1575
+ const controller = createProfilerController(profiler);
1576
+ app.use(OpticoreRoutingFactory.route({
1577
+ method: "get",
1578
+ path: "/",
1579
+ handler: (async (ctx) => {
1580
+ await controller.home(ctx.req, ctx.res);
1581
+ })
1582
+ }));
1583
+ }
1584
+
1585
+ // src/routes/profiler.router.handler.ts
1586
+ import { OpticoreRoutingFactory as OpticoreRoutingFactory2 } from "opticore-router";
1587
+ function createProfilerRouterHandler(profiler) {
1588
+ const controller = createProfilerController(profiler);
1589
+ const prefix = profiler.options.routePrefix;
1590
+ return OpticoreRoutingFactory2.routes(
1591
+ controller,
1592
+ [
1593
+ {
1594
+ path: `${prefix}/wdt/:token`,
1595
+ method: "get",
1596
+ middlewares: [],
1597
+ handler: async (ctx) => controller.wdt(ctx.req, ctx.res)
1598
+ },
1599
+ {
1600
+ path: `${prefix}/stream`,
1601
+ method: "get",
1602
+ middlewares: [],
1603
+ handler: async (ctx) => controller.stream(ctx.req, ctx.res)
1604
+ },
1605
+ {
1606
+ path: `${prefix}/latest`,
1607
+ method: "get",
1608
+ middlewares: [],
1609
+ handler: async (ctx) => controller.latestRedirect(ctx.req, ctx.res)
1610
+ },
1611
+ {
1612
+ path: `${prefix}/:token`,
1613
+ method: "get",
1614
+ middlewares: [],
1615
+ handler: async (ctx) => controller.detail(ctx.req, ctx.res)
1616
+ },
1617
+ {
1618
+ path: prefix,
1619
+ method: "get",
1620
+ middlewares: [],
1621
+ handler: async (ctx) => controller.list(ctx.req, ctx.res)
1622
+ }
1623
+ ]
1624
+ );
1625
+ }
1626
+
1627
+ // src/routes/profiler.router.ts
1628
+ function createProfilerRouter(profiler) {
1629
+ const def = createProfilerRouterHandler(profiler);
1630
+ return {
1631
+ routes: [
1632
+ { path: def.path, handler: def.handler }
1633
+ ]
1634
+ };
1635
+ }
1636
+
1637
+ // src/instrumentation/mysql.instrumentation.ts
1638
+ import { sep } from "path";
1639
+ var INSTRUMENTED = /* @__PURE__ */ new WeakSet();
1640
+ function extractSql(sql) {
1641
+ return typeof sql === "string" ? sql : sql.sql;
1642
+ }
1643
+ function captureCallSite() {
1644
+ const probe = { stack: "" };
1645
+ Error.captureStackTrace(probe, captureCallSite);
1646
+ const lines = probe.stack.split("\n").slice(1);
1647
+ const appFrame = lines.find(
1648
+ (l) => !l.includes("mysql.instrumentation") && !l.includes(`${sep}opticore-mysqldb${sep}`)
1649
+ );
1650
+ return (appFrame ?? lines[0] ?? "").trim();
1651
+ }
1652
+ function instrumentMySQL(DriverClass) {
1653
+ const proto = DriverClass.prototype;
1654
+ if (INSTRUMENTED.has(proto)) return;
1655
+ INSTRUMENTED.add(proto);
1656
+ const original = proto.makeQuery;
1657
+ proto.makeQuery = async function(sql, values, callback) {
1658
+ const collector = getActiveCollector("database");
1659
+ if (!collector) {
1660
+ return original.call(this, sql, values, callback);
1661
+ }
1662
+ const sqlText = extractSql(sql);
1663
+ const stack = captureCallSite();
1664
+ const start = process.hrtime.bigint();
1665
+ try {
1666
+ const result = await original.call(this, sql, values, callback);
1667
+ collector.record({
1668
+ sql: sqlText,
1669
+ bindings: values,
1670
+ duration: Number((process.hrtime.bigint() - start) / 1000000n),
1671
+ stack
1672
+ });
1673
+ return result;
1674
+ } catch (err) {
1675
+ collector.record({
1676
+ sql: sqlText,
1677
+ bindings: values,
1678
+ duration: Number((process.hrtime.bigint() - start) / 1000000n),
1679
+ stack,
1680
+ error: err instanceof Error ? err.message : String(err)
1681
+ });
1682
+ throw err;
1683
+ }
1684
+ };
1685
+ }
1686
+
1687
+ // src/instrumentation/logger.instrumentation.ts
1688
+ var INSTRUMENTED2 = /* @__PURE__ */ new WeakSet();
1689
+ function mirror(level, message, context) {
1690
+ const collector = getActiveCollector("logger");
1691
+ if (!collector) return;
1692
+ collector.record({ level, message, context });
1693
+ }
1694
+ function instrumentLogger(LoggerClass) {
1695
+ const proto = LoggerClass.prototype;
1696
+ if (INSTRUMENTED2.has(proto)) {
1697
+ return;
1698
+ }
1699
+ INSTRUMENTED2.add(proto);
1700
+ const originalSuccess = proto.success;
1701
+ proto.success = function(arg) {
1702
+ mirror("info", arg.message ?? arg.title ?? "", arg.title ? { title: arg.title } : void 0);
1703
+ return originalSuccess.call(this, arg);
1704
+ };
1705
+ const originalInfo = proto.info;
1706
+ proto.info = function(arg) {
1707
+ mirror("info", arg.message ?? arg.title ?? "", arg.title ? { title: arg.title } : void 0);
1708
+ return originalInfo.call(this, arg);
1709
+ };
1710
+ const originalWarn = proto.warn;
1711
+ proto.warn = function(arg) {
1712
+ mirror("warning", arg.message, arg.title ? { title: arg.title } : void 0);
1713
+ return originalWarn.call(this, arg);
1714
+ };
1715
+ const originalError = proto.error;
1716
+ proto.error = function(arg) {
1717
+ mirror("error", arg.message, {
1718
+ ...arg.title ? { title: arg.title } : {},
1719
+ ...arg.errorType ? { errorType: arg.errorType } : {},
1720
+ ...arg.stackTrace ? { stack: arg.stackTrace } : {}
1721
+ });
1722
+ return originalError.call(this, arg);
1723
+ };
1724
+ const originalDebug = proto.debug;
1725
+ proto.debug = function(message) {
1726
+ mirror("debug", message);
1727
+ return originalDebug.call(this, message);
1728
+ };
1729
+ }
1730
+
1731
+ // src/storage/memory.storage.ts
1732
+ var MemoryStorage = class {
1733
+ constructor(limit = 100) {
1734
+ this.limit = limit;
1735
+ if (limit < 1) throw new Error("MemoryStorage limit must be >= 1");
1736
+ }
1737
+ limit;
1738
+ profiles = /* @__PURE__ */ new Map();
1739
+ order = [];
1740
+ write(profile) {
1741
+ if (this.profiles.has(profile.token)) {
1742
+ const idx = this.order.indexOf(profile.token);
1743
+ if (idx !== -1) this.order.splice(idx, 1);
1744
+ } else if (this.order.length >= this.limit) {
1745
+ const oldest = this.order.shift();
1746
+ this.profiles.delete(oldest);
1747
+ }
1748
+ this.order.push(profile.token);
1749
+ this.profiles.set(profile.token, profile);
1750
+ }
1751
+ read(token) {
1752
+ return this.profiles.get(token);
1753
+ }
1754
+ find(limit) {
1755
+ const tokens = [...this.order].reverse();
1756
+ const sliced = limit ? tokens.slice(0, limit) : tokens;
1757
+ return sliced.map((t) => this.profiles.get(t)).filter(Boolean);
1758
+ }
1759
+ purge(olderThanMs) {
1760
+ if (!olderThanMs) return;
1761
+ const cutoff = Date.now() - olderThanMs;
1762
+ for (const token of [...this.order]) {
1763
+ const profile = this.profiles.get(token);
1764
+ if (profile && profile.time < cutoff) {
1765
+ this.profiles.delete(token);
1766
+ const idx = this.order.indexOf(token);
1767
+ if (idx !== -1) this.order.splice(idx, 1);
1768
+ }
1769
+ }
1770
+ }
1771
+ clear() {
1772
+ this.profiles.clear();
1773
+ this.order.length = 0;
1774
+ }
1775
+ count() {
1776
+ return this.order.length;
1777
+ }
1778
+ };
1779
+
1780
+ // src/storage/file.storage.ts
1781
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, unlinkSync } from "fs";
1782
+ import { join as join4 } from "path";
1783
+ var FileStorage = class {
1784
+ constructor(dir = ".opticore-profiler-cache", maxIndexSize = 100) {
1785
+ this.dir = dir;
1786
+ this.maxIndexSize = maxIndexSize;
1787
+ if (maxIndexSize < 1) throw new Error("FileStorage maxIndexSize must be >= 1");
1788
+ this.indexPath = join4(this.dir, "index.json");
1789
+ this.ensureDir();
1790
+ }
1791
+ dir;
1792
+ maxIndexSize;
1793
+ indexPath;
1794
+ ensureDir() {
1795
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
1796
+ }
1797
+ profilePath(token) {
1798
+ return join4(this.dir, `${token}.json`);
1799
+ }
1800
+ readIndex() {
1801
+ if (!existsSync(this.indexPath)) return [];
1802
+ try {
1803
+ return JSON.parse(readFileSync2(this.indexPath, "utf8"));
1804
+ } catch {
1805
+ return [];
1806
+ }
1807
+ }
1808
+ writeIndex(entries) {
1809
+ writeFileSync(this.indexPath, JSON.stringify(entries), "utf8");
1810
+ }
1811
+ write(profile) {
1812
+ this.ensureDir();
1813
+ writeFileSync(this.profilePath(profile.token), JSON.stringify(profile), "utf8");
1814
+ const index = this.readIndex().filter((e) => e.token !== profile.token);
1815
+ index.unshift({
1816
+ token: profile.token,
1817
+ ip: profile.ip,
1818
+ method: profile.method,
1819
+ url: profile.url,
1820
+ statusCode: profile.statusCode,
1821
+ time: profile.time,
1822
+ duration: profile.duration,
1823
+ contentType: profile.contentType
1824
+ });
1825
+ while (index.length > this.maxIndexSize) {
1826
+ const dropped = index.pop();
1827
+ this.deleteFile(dropped.token);
1828
+ }
1829
+ this.writeIndex(index);
1830
+ }
1831
+ read(token) {
1832
+ const path2 = this.profilePath(token);
1833
+ if (!existsSync(path2)) return void 0;
1834
+ try {
1835
+ return JSON.parse(readFileSync2(path2, "utf8"));
1836
+ } catch {
1837
+ return void 0;
1838
+ }
1839
+ }
1840
+ find(limit) {
1841
+ const entries = this.readIndex();
1842
+ const sliced = limit ? entries.slice(0, limit) : entries;
1843
+ return sliced.map((e) => ({ ...e, collectors: {} }));
1844
+ }
1845
+ purge(olderThanMs) {
1846
+ if (!olderThanMs) return;
1847
+ const cutoff = Date.now() - olderThanMs;
1848
+ const index = this.readIndex();
1849
+ const kept = index.filter((e) => {
1850
+ if (e.time >= cutoff) return true;
1851
+ this.deleteFile(e.token);
1852
+ return false;
1853
+ });
1854
+ if (kept.length !== index.length) this.writeIndex(kept);
1855
+ }
1856
+ clear() {
1857
+ for (const entry of this.readIndex()) {
1858
+ this.deleteFile(entry.token);
1859
+ }
1860
+ this.writeIndex([]);
1861
+ }
1862
+ deleteFile(token) {
1863
+ const path2 = this.profilePath(token);
1864
+ if (existsSync(path2)) {
1865
+ try {
1866
+ unlinkSync(path2);
1867
+ } catch {
1868
+ }
1869
+ }
1870
+ }
1871
+ };
1872
+ export {
1873
+ CollectorRegistry,
1874
+ DEFAULT_EXCLUDE_PATHS,
1875
+ DEFAULT_SENSITIVE_KEYS,
1876
+ DatabaseCollector,
1877
+ ExceptionCollector,
1878
+ FileStorage,
1879
+ LoggerCollector,
1880
+ MemoryCollector,
1881
+ MemoryStorage,
1882
+ RequestCollector,
1883
+ TimeCollector,
1884
+ createProfilerRouter,
1885
+ generateToken,
1886
+ getActiveCollector,
1887
+ instrumentLogger,
1888
+ instrumentMySQL,
1889
+ isValidToken,
1890
+ opticoreProfiler,
1891
+ profilerErrorHandler,
1892
+ registerProfilerViews
1893
+ };