@vipin733/nodescope 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2092 @@
1
+ import {
2
+ MemoryStorage
3
+ } from "./chunk-6KBKW63X.js";
4
+ import {
5
+ SQLiteStorage
6
+ } from "./chunk-BOBKU5LG.js";
7
+ import {
8
+ PostgreSQLStorage
9
+ } from "./chunk-6H665NNC.js";
10
+ import {
11
+ MySQLStorage
12
+ } from "./chunk-V5BR4MSS.js";
13
+ import {
14
+ createStorageAdapter
15
+ } from "./chunk-OF6NKXP5.js";
16
+
17
+ // src/watchers/base.ts
18
+ import { nanoid } from "nanoid";
19
+ var BaseWatcher = class {
20
+ /** Whether this watcher is enabled */
21
+ enabled = true;
22
+ /**
23
+ * Create an entry from captured data
24
+ */
25
+ createEntry(content, options = {}) {
26
+ return {
27
+ id: nanoid(),
28
+ batchId: options.batchId ?? nanoid(),
29
+ type: this.type,
30
+ content,
31
+ tags: options.tags ?? [],
32
+ createdAt: /* @__PURE__ */ new Date(),
33
+ duration: options.duration,
34
+ memoryUsage: options.memoryUsage
35
+ };
36
+ }
37
+ };
38
+ function createRequestContext() {
39
+ return {
40
+ batchId: nanoid(),
41
+ startTime: performance.now(),
42
+ startMemory: process.memoryUsage?.()?.heapUsed
43
+ };
44
+ }
45
+ function getDuration(ctx) {
46
+ return Math.round(performance.now() - ctx.startTime);
47
+ }
48
+ function getMemoryDelta(ctx) {
49
+ if (ctx.startMemory === void 0) return void 0;
50
+ const currentMemory = process.memoryUsage?.()?.heapUsed;
51
+ if (currentMemory === void 0) return void 0;
52
+ return currentMemory - ctx.startMemory;
53
+ }
54
+
55
+ // src/watchers/request.ts
56
+ var DEFAULT_SIZE_LIMIT = 64;
57
+ var DEFAULT_HIDE_HEADERS = ["authorization", "cookie", "set-cookie"];
58
+ var RequestWatcher = class extends BaseWatcher {
59
+ type = "request";
60
+ options;
61
+ constructor(options = {}) {
62
+ super();
63
+ this.enabled = options.enabled ?? true;
64
+ this.options = {
65
+ enabled: options.enabled ?? true,
66
+ sizeLimit: options.sizeLimit ?? DEFAULT_SIZE_LIMIT,
67
+ ignorePaths: options.ignorePaths ?? ["/_nodescope", "/favicon.ico"],
68
+ captureBody: options.captureBody ?? true,
69
+ captureResponse: options.captureResponse ?? true,
70
+ hideHeaders: options.hideHeaders ?? DEFAULT_HIDE_HEADERS
71
+ };
72
+ }
73
+ /**
74
+ * Record a request/response pair
75
+ */
76
+ record(data) {
77
+ if (this.options.ignorePaths.some((p) => data.path.startsWith(p))) {
78
+ return null;
79
+ }
80
+ const duration = Math.round(performance.now() - data.startTime);
81
+ const filteredHeaders = this.filterHeaders(data.headers);
82
+ const filteredResponseHeaders = this.filterHeaders(data.response.headers);
83
+ const requestBody = this.options.captureBody ? this.truncateBody(data.body) : void 0;
84
+ const responseBody = this.options.captureResponse ? this.truncateBody(data.response.body) : void 0;
85
+ const content = {
86
+ method: data.method.toUpperCase(),
87
+ url: data.url,
88
+ path: data.path,
89
+ query: data.query,
90
+ headers: filteredHeaders,
91
+ body: requestBody,
92
+ ip: data.ip,
93
+ userAgent: data.userAgent,
94
+ session: data.session,
95
+ response: {
96
+ status: data.response.status,
97
+ headers: filteredResponseHeaders,
98
+ body: responseBody,
99
+ size: this.getBodySize(data.response.body)
100
+ },
101
+ middleware: data.middleware,
102
+ controllerAction: data.controllerAction
103
+ };
104
+ const entry = this.createEntry(content, {
105
+ batchId: data.batchId,
106
+ duration,
107
+ tags: this.generateTags(content)
108
+ });
109
+ return entry;
110
+ }
111
+ filterHeaders(headers) {
112
+ const filtered = {};
113
+ for (const [key, value] of Object.entries(headers)) {
114
+ if (this.options.hideHeaders.includes(key.toLowerCase())) {
115
+ filtered[key] = "[HIDDEN]";
116
+ } else {
117
+ filtered[key] = value;
118
+ }
119
+ }
120
+ return filtered;
121
+ }
122
+ truncateBody(body) {
123
+ if (body === void 0 || body === null) return body;
124
+ const serialized = JSON.stringify(body);
125
+ const sizeKB = Buffer.byteLength(serialized, "utf8") / 1024;
126
+ if (sizeKB > this.options.sizeLimit) {
127
+ return `[TRUNCATED - ${Math.round(sizeKB)}KB exceeds ${this.options.sizeLimit}KB limit]`;
128
+ }
129
+ return body;
130
+ }
131
+ getBodySize(body) {
132
+ if (body === void 0 || body === null) return void 0;
133
+ try {
134
+ return Buffer.byteLength(JSON.stringify(body), "utf8");
135
+ } catch {
136
+ return void 0;
137
+ }
138
+ }
139
+ generateTags(content) {
140
+ const tags = [];
141
+ tags.push(`method:${content.method}`);
142
+ tags.push(`status:${content.response.status}`);
143
+ if (content.response.status >= 400) {
144
+ tags.push("error");
145
+ }
146
+ if (content.response.status >= 500) {
147
+ tags.push("server-error");
148
+ }
149
+ return tags;
150
+ }
151
+ };
152
+
153
+ // src/watchers/query.ts
154
+ var DEFAULT_SLOW_THRESHOLD = 100;
155
+ var QueryWatcher = class extends BaseWatcher {
156
+ type = "query";
157
+ slowThreshold;
158
+ constructor(options = {}) {
159
+ super();
160
+ this.enabled = options.enabled ?? true;
161
+ this.slowThreshold = options.slowThreshold ?? DEFAULT_SLOW_THRESHOLD;
162
+ }
163
+ /**
164
+ * Record a database query
165
+ */
166
+ record(data) {
167
+ const slow = data.duration > this.slowThreshold;
168
+ const content = {
169
+ sql: data.sql,
170
+ bindings: data.bindings ?? [],
171
+ connection: data.connection ?? "default",
172
+ database: data.database,
173
+ slow,
174
+ rowCount: data.rowCount
175
+ };
176
+ const tags = [];
177
+ if (slow) {
178
+ tags.push("slow");
179
+ }
180
+ const queryType = this.extractQueryType(data.sql);
181
+ if (queryType) {
182
+ tags.push(`query:${queryType}`);
183
+ }
184
+ return this.createEntry(content, {
185
+ batchId: data.batchId,
186
+ duration: data.duration,
187
+ tags
188
+ });
189
+ }
190
+ extractQueryType(sql) {
191
+ const normalized = sql.trim().toLowerCase();
192
+ if (normalized.startsWith("select")) return "select";
193
+ if (normalized.startsWith("insert")) return "insert";
194
+ if (normalized.startsWith("update")) return "update";
195
+ if (normalized.startsWith("delete")) return "delete";
196
+ if (normalized.startsWith("create")) return "create";
197
+ if (normalized.startsWith("alter")) return "alter";
198
+ if (normalized.startsWith("drop")) return "drop";
199
+ return null;
200
+ }
201
+ };
202
+ function wrapPrisma(prisma, watcher, batchId) {
203
+ if ("$on" in prisma && typeof prisma.$on === "function") {
204
+ prisma.$on("query", (e) => {
205
+ watcher.record({
206
+ batchId,
207
+ sql: e.query,
208
+ bindings: e.params ? JSON.parse(e.params) : [],
209
+ duration: e.duration
210
+ });
211
+ });
212
+ }
213
+ return prisma;
214
+ }
215
+ function createQueryInterceptor(watcher, batchId) {
216
+ return {
217
+ /**
218
+ * Wrap a query function to track its execution
219
+ */
220
+ wrap(queryFn, getSql, getBindings) {
221
+ return async (...args) => {
222
+ const startTime = performance.now();
223
+ try {
224
+ const result = await queryFn(...args);
225
+ const duration = Math.round(performance.now() - startTime);
226
+ watcher.record({
227
+ batchId,
228
+ sql: getSql(...args),
229
+ bindings: getBindings?.(...args) ?? [],
230
+ duration,
231
+ rowCount: Array.isArray(result) ? result.length : void 0
232
+ });
233
+ return result;
234
+ } catch (error) {
235
+ const duration = Math.round(performance.now() - startTime);
236
+ watcher.record({
237
+ batchId,
238
+ sql: getSql(...args),
239
+ bindings: getBindings?.(...args) ?? [],
240
+ duration
241
+ });
242
+ throw error;
243
+ }
244
+ };
245
+ }
246
+ };
247
+ }
248
+
249
+ // src/watchers/cache.ts
250
+ var CacheWatcher = class extends BaseWatcher {
251
+ type = "cache";
252
+ constructor(options = {}) {
253
+ super();
254
+ this.enabled = options.enabled ?? true;
255
+ }
256
+ /**
257
+ * Record a cache operation
258
+ */
259
+ record(data) {
260
+ const content = {
261
+ key: data.key,
262
+ value: this.truncateValue(data.value),
263
+ operation: data.operation,
264
+ driver: data.driver ?? "unknown",
265
+ ttl: data.ttl,
266
+ tags: data.tags
267
+ };
268
+ const entryTags = [`operation:${data.operation}`];
269
+ if (data.operation === "hit") {
270
+ entryTags.push("hit");
271
+ } else if (data.operation === "miss") {
272
+ entryTags.push("miss");
273
+ }
274
+ return this.createEntry(content, {
275
+ batchId: data.batchId,
276
+ duration: data.duration,
277
+ tags: entryTags
278
+ });
279
+ }
280
+ truncateValue(value) {
281
+ if (value === void 0 || value === null) return value;
282
+ try {
283
+ const serialized = JSON.stringify(value);
284
+ if (serialized.length > 1024) {
285
+ return `[TRUNCATED - ${serialized.length} bytes]`;
286
+ }
287
+ return value;
288
+ } catch {
289
+ return "[UNSERIALIZABLE]";
290
+ }
291
+ }
292
+ };
293
+ function createCacheWrapper(cache, watcher, driver, batchId) {
294
+ const handler = {
295
+ get(target, prop) {
296
+ const value = target[prop];
297
+ if (typeof value !== "function") {
298
+ return value;
299
+ }
300
+ const methodName = String(prop).toLowerCase();
301
+ return async (...args) => {
302
+ const startTime = performance.now();
303
+ try {
304
+ const result = await value.apply(target, args);
305
+ const duration = Math.round(performance.now() - startTime);
306
+ let operation = "get";
307
+ let key = "";
308
+ let cacheValue;
309
+ let ttl;
310
+ if (methodName.includes("get") || methodName === "fetch") {
311
+ key = String(args[0] ?? "");
312
+ operation = result !== null && result !== void 0 ? "hit" : "miss";
313
+ cacheValue = result;
314
+ } else if (methodName.includes("set") || methodName === "put") {
315
+ key = String(args[0] ?? "");
316
+ operation = "set";
317
+ cacheValue = args[1];
318
+ ttl = typeof args[2] === "number" ? args[2] : void 0;
319
+ } else if (methodName.includes("del") || methodName === "remove") {
320
+ key = String(args[0] ?? "");
321
+ operation = "delete";
322
+ } else if (methodName.includes("flush") || methodName === "clear") {
323
+ key = "*";
324
+ operation = "flush";
325
+ }
326
+ if (key) {
327
+ watcher.record({
328
+ batchId,
329
+ key,
330
+ value: cacheValue,
331
+ operation,
332
+ driver,
333
+ ttl,
334
+ duration
335
+ });
336
+ }
337
+ return result;
338
+ } catch (error) {
339
+ throw error;
340
+ }
341
+ };
342
+ }
343
+ };
344
+ return new Proxy(cache, handler);
345
+ }
346
+
347
+ // src/watchers/log.ts
348
+ var LOG_LEVEL_PRIORITY = {
349
+ debug: 0,
350
+ info: 1,
351
+ warn: 2,
352
+ error: 3
353
+ };
354
+ var LogWatcher = class extends BaseWatcher {
355
+ type = "log";
356
+ minLevel;
357
+ constructor(options = {}) {
358
+ super();
359
+ this.enabled = options.enabled ?? true;
360
+ this.minLevel = options.level ?? "debug";
361
+ }
362
+ /**
363
+ * Record a log entry
364
+ */
365
+ record(data) {
366
+ if (LOG_LEVEL_PRIORITY[data.level] < LOG_LEVEL_PRIORITY[this.minLevel]) {
367
+ return null;
368
+ }
369
+ const content = {
370
+ level: data.level,
371
+ message: data.message,
372
+ context: data.context,
373
+ channel: data.channel
374
+ };
375
+ const tags = [`level:${data.level}`];
376
+ if (data.level === "error") {
377
+ tags.push("error");
378
+ }
379
+ if (data.channel) {
380
+ tags.push(`channel:${data.channel}`);
381
+ }
382
+ return this.createEntry(content, {
383
+ batchId: data.batchId,
384
+ tags
385
+ });
386
+ }
387
+ /**
388
+ * Create a logger instance that automatically records to NodeScope
389
+ */
390
+ createLogger(batchId, channel) {
391
+ return {
392
+ debug: (message, context) => {
393
+ return this.record({ batchId, level: "debug", message, context, channel });
394
+ },
395
+ info: (message, context) => {
396
+ return this.record({ batchId, level: "info", message, context, channel });
397
+ },
398
+ warn: (message, context) => {
399
+ return this.record({ batchId, level: "warn", message, context, channel });
400
+ },
401
+ error: (message, context) => {
402
+ return this.record({ batchId, level: "error", message, context, channel });
403
+ }
404
+ };
405
+ }
406
+ };
407
+ function interceptConsole(watcher, batchIdFn) {
408
+ const originalConsole = {
409
+ log: console.log,
410
+ info: console.info,
411
+ warn: console.warn,
412
+ error: console.error,
413
+ debug: console.debug
414
+ };
415
+ const createInterceptor = (level, original) => {
416
+ return (...args) => {
417
+ original.apply(console, args);
418
+ const message = args.map((arg) => {
419
+ if (typeof arg === "string") return arg;
420
+ try {
421
+ return JSON.stringify(arg);
422
+ } catch {
423
+ return String(arg);
424
+ }
425
+ }).join(" ");
426
+ watcher.record({
427
+ batchId: batchIdFn?.(),
428
+ level,
429
+ message,
430
+ channel: "console"
431
+ });
432
+ };
433
+ };
434
+ console.log = createInterceptor("info", originalConsole.log);
435
+ console.info = createInterceptor("info", originalConsole.info);
436
+ console.warn = createInterceptor("warn", originalConsole.warn);
437
+ console.error = createInterceptor("error", originalConsole.error);
438
+ console.debug = createInterceptor("debug", originalConsole.debug);
439
+ return () => {
440
+ console.log = originalConsole.log;
441
+ console.info = originalConsole.info;
442
+ console.warn = originalConsole.warn;
443
+ console.error = originalConsole.error;
444
+ console.debug = originalConsole.debug;
445
+ };
446
+ }
447
+
448
+ // src/watchers/exception.ts
449
+ var ExceptionWatcher = class extends BaseWatcher {
450
+ type = "exception";
451
+ constructor(options = {}) {
452
+ super();
453
+ this.enabled = options.enabled ?? true;
454
+ }
455
+ /**
456
+ * Record an exception
457
+ */
458
+ record(data) {
459
+ const content = this.errorToContent(data.error, data.context);
460
+ const tags = ["error", `class:${content.class}`];
461
+ return this.createEntry(content, {
462
+ batchId: data.batchId,
463
+ tags
464
+ });
465
+ }
466
+ errorToContent(error, context) {
467
+ const { file, line } = this.extractLocation(error.stack);
468
+ const content = {
469
+ class: error.name || "Error",
470
+ message: error.message,
471
+ stack: error.stack || "",
472
+ file,
473
+ line,
474
+ context
475
+ };
476
+ if ("cause" in error && error.cause instanceof Error) {
477
+ content.previous = this.errorToContent(error.cause);
478
+ }
479
+ return content;
480
+ }
481
+ extractLocation(stack) {
482
+ if (!stack) return {};
483
+ const lines = stack.split("\n");
484
+ for (const line of lines) {
485
+ const match = line.match(/at\s+(?:.+?\s+)?\(?(.+?):(\d+):\d+\)?/);
486
+ if (match) {
487
+ return {
488
+ file: match[1],
489
+ line: parseInt(match[2], 10)
490
+ };
491
+ }
492
+ }
493
+ return {};
494
+ }
495
+ };
496
+ function setupGlobalErrorHandlers(watcher, onEntry) {
497
+ const handleUncaughtException = (error) => {
498
+ const entry = watcher.record({
499
+ error,
500
+ context: { uncaught: true, type: "uncaughtException" }
501
+ });
502
+ onEntry(entry);
503
+ };
504
+ const handleUnhandledRejection = (reason) => {
505
+ const error = reason instanceof Error ? reason : new Error(String(reason));
506
+ const entry = watcher.record({
507
+ error,
508
+ context: { uncaught: true, type: "unhandledRejection" }
509
+ });
510
+ onEntry(entry);
511
+ };
512
+ process.on("uncaughtException", handleUncaughtException);
513
+ process.on("unhandledRejection", handleUnhandledRejection);
514
+ return () => {
515
+ process.removeListener("uncaughtException", handleUncaughtException);
516
+ process.removeListener("unhandledRejection", handleUnhandledRejection);
517
+ };
518
+ }
519
+
520
+ // src/watchers/http-client.ts
521
+ var DEFAULT_SIZE_LIMIT2 = 64;
522
+ var HttpClientWatcher = class extends BaseWatcher {
523
+ type = "http_client";
524
+ sizeLimit;
525
+ constructor(options = {}) {
526
+ super();
527
+ this.enabled = options.enabled ?? true;
528
+ this.sizeLimit = options.sizeLimit ?? DEFAULT_SIZE_LIMIT2;
529
+ }
530
+ /**
531
+ * Record an outgoing HTTP request
532
+ */
533
+ record(data) {
534
+ const content = {
535
+ method: data.method.toUpperCase(),
536
+ url: data.url,
537
+ headers: this.sanitizeHeaders(data.headers),
538
+ body: this.truncateBody(data.body),
539
+ response: {
540
+ status: data.response.status,
541
+ headers: data.response.headers,
542
+ body: this.truncateBody(data.response.body),
543
+ size: this.getBodySize(data.response.body)
544
+ }
545
+ };
546
+ const tags = [
547
+ `method:${content.method}`,
548
+ `status:${content.response.status}`
549
+ ];
550
+ if (content.response.status >= 400) {
551
+ tags.push("error");
552
+ }
553
+ try {
554
+ const url = new URL(data.url);
555
+ tags.push(`host:${url.host}`);
556
+ } catch {
557
+ }
558
+ return this.createEntry(content, {
559
+ batchId: data.batchId,
560
+ duration: data.duration,
561
+ tags
562
+ });
563
+ }
564
+ sanitizeHeaders(headers) {
565
+ const sanitized = {};
566
+ const sensitiveHeaders = ["authorization", "cookie", "x-api-key", "api-key"];
567
+ for (const [key, value] of Object.entries(headers)) {
568
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
569
+ sanitized[key] = "[HIDDEN]";
570
+ } else {
571
+ sanitized[key] = value;
572
+ }
573
+ }
574
+ return sanitized;
575
+ }
576
+ truncateBody(body) {
577
+ if (body === void 0 || body === null) return body;
578
+ try {
579
+ const serialized = JSON.stringify(body);
580
+ const sizeKB = Buffer.byteLength(serialized, "utf8") / 1024;
581
+ if (sizeKB > this.sizeLimit) {
582
+ return `[TRUNCATED - ${Math.round(sizeKB)}KB]`;
583
+ }
584
+ return body;
585
+ } catch {
586
+ return "[UNSERIALIZABLE]";
587
+ }
588
+ }
589
+ getBodySize(body) {
590
+ if (body === void 0 || body === null) return void 0;
591
+ try {
592
+ return Buffer.byteLength(JSON.stringify(body), "utf8");
593
+ } catch {
594
+ return void 0;
595
+ }
596
+ }
597
+ };
598
+ function wrapFetch(watcher, batchIdFn) {
599
+ const originalFetch = globalThis.fetch;
600
+ return async (input, init) => {
601
+ const startTime = performance.now();
602
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
603
+ const method = init?.method || (typeof input === "object" && "method" in input ? input.method : "GET");
604
+ const headers = init?.headers ? Object.fromEntries(
605
+ init.headers instanceof Headers ? init.headers.entries() : Array.isArray(init.headers) ? init.headers : Object.entries(init.headers)
606
+ ) : {};
607
+ let requestBody;
608
+ if (init?.body) {
609
+ try {
610
+ requestBody = typeof init.body === "string" ? JSON.parse(init.body) : init.body;
611
+ } catch {
612
+ requestBody = init.body;
613
+ }
614
+ }
615
+ try {
616
+ const response = await originalFetch(input, init);
617
+ const duration = Math.round(performance.now() - startTime);
618
+ const clonedResponse = response.clone();
619
+ let responseBody;
620
+ try {
621
+ responseBody = await clonedResponse.json();
622
+ } catch {
623
+ try {
624
+ responseBody = await clonedResponse.text();
625
+ } catch {
626
+ responseBody = void 0;
627
+ }
628
+ }
629
+ watcher.record({
630
+ batchId: batchIdFn?.(),
631
+ method: method || "GET",
632
+ url,
633
+ headers,
634
+ body: requestBody,
635
+ response: {
636
+ status: response.status,
637
+ headers: Object.fromEntries(response.headers.entries()),
638
+ body: responseBody
639
+ },
640
+ duration
641
+ });
642
+ return response;
643
+ } catch (error) {
644
+ const duration = Math.round(performance.now() - startTime);
645
+ watcher.record({
646
+ batchId: batchIdFn?.(),
647
+ method: method || "GET",
648
+ url,
649
+ headers,
650
+ body: requestBody,
651
+ response: {
652
+ status: 0,
653
+ headers: {},
654
+ body: error instanceof Error ? error.message : String(error)
655
+ },
656
+ duration
657
+ });
658
+ throw error;
659
+ }
660
+ };
661
+ }
662
+ function interceptFetch(watcher, batchIdFn) {
663
+ const originalFetch = globalThis.fetch;
664
+ globalThis.fetch = wrapFetch(watcher, batchIdFn);
665
+ return () => {
666
+ globalThis.fetch = originalFetch;
667
+ };
668
+ }
669
+
670
+ // src/watchers/event.ts
671
+ var EventWatcher = class extends BaseWatcher {
672
+ type = "event";
673
+ ignorePatterns;
674
+ constructor(options = {}) {
675
+ super();
676
+ this.enabled = options.enabled ?? true;
677
+ this.ignorePatterns = options.ignore ?? [];
678
+ }
679
+ /**
680
+ * Record an event
681
+ */
682
+ record(data) {
683
+ if (this.ignorePatterns.some((pattern) => data.name.includes(pattern))) {
684
+ return null;
685
+ }
686
+ const content = {
687
+ name: data.name,
688
+ payload: data.payload,
689
+ listeners: data.listeners ?? [],
690
+ broadcast: data.broadcast
691
+ };
692
+ const tags = [`event:${data.name}`];
693
+ if (data.broadcast) {
694
+ tags.push("broadcast");
695
+ tags.push(`channel:${data.broadcast.channel}`);
696
+ }
697
+ return this.createEntry(content, {
698
+ batchId: data.batchId,
699
+ tags
700
+ });
701
+ }
702
+ };
703
+ var TrackedEventEmitter = class {
704
+ listeners = /* @__PURE__ */ new Map();
705
+ watcher;
706
+ batchIdFn;
707
+ constructor(watcher, batchIdFn) {
708
+ this.watcher = watcher;
709
+ this.batchIdFn = batchIdFn;
710
+ }
711
+ on(event, handler, handlerName) {
712
+ if (!this.listeners.has(event)) {
713
+ this.listeners.set(event, []);
714
+ }
715
+ this.listeners.get(event).push({
716
+ name: handlerName ?? (handler.name || "anonymous"),
717
+ handler
718
+ });
719
+ }
720
+ off(event, handler) {
721
+ const handlers = this.listeners.get(event);
722
+ if (handlers) {
723
+ const index = handlers.findIndex((h) => h.handler === handler);
724
+ if (index !== -1) {
725
+ handlers.splice(index, 1);
726
+ }
727
+ }
728
+ }
729
+ emit(event, payload) {
730
+ const handlers = this.listeners.get(event) ?? [];
731
+ const listenerNames = handlers.map((h) => h.name);
732
+ this.watcher.record({
733
+ batchId: this.batchIdFn?.(),
734
+ name: event,
735
+ payload,
736
+ listeners: listenerNames
737
+ });
738
+ for (const { handler } of handlers) {
739
+ try {
740
+ handler(payload);
741
+ } catch (error) {
742
+ console.error(`Error in event handler for ${event}:`, error);
743
+ }
744
+ }
745
+ }
746
+ listenerCount(event) {
747
+ return this.listeners.get(event)?.length ?? 0;
748
+ }
749
+ };
750
+
751
+ // src/watchers/job.ts
752
+ var JobWatcher = class extends BaseWatcher {
753
+ type = "job";
754
+ constructor(options = {}) {
755
+ super();
756
+ this.enabled = options.enabled ?? true;
757
+ }
758
+ /**
759
+ * Record a job
760
+ */
761
+ record(data) {
762
+ const content = {
763
+ name: data.name,
764
+ queue: data.queue ?? "default",
765
+ data: data.data,
766
+ status: data.status,
767
+ attempts: data.attempts ?? 1,
768
+ maxAttempts: data.maxAttempts,
769
+ error: data.error
770
+ };
771
+ const tags = [
772
+ `status:${data.status}`,
773
+ `queue:${content.queue}`,
774
+ `job:${data.name}`
775
+ ];
776
+ if (data.status === "failed") {
777
+ tags.push("failed");
778
+ }
779
+ return this.createEntry(content, {
780
+ batchId: data.batchId,
781
+ duration: data.duration,
782
+ tags
783
+ });
784
+ }
785
+ /**
786
+ * Create a job tracker that can be used to track job lifecycle
787
+ */
788
+ createJobTracker(name, options = {}) {
789
+ let attempts = 0;
790
+ const { batchId, queue, data, maxAttempts } = options;
791
+ return {
792
+ start: () => {
793
+ attempts++;
794
+ return this.record({
795
+ batchId,
796
+ name,
797
+ queue,
798
+ data,
799
+ status: "processing",
800
+ attempts,
801
+ maxAttempts
802
+ });
803
+ },
804
+ complete: (duration) => {
805
+ return this.record({
806
+ batchId,
807
+ name,
808
+ queue,
809
+ data,
810
+ status: "completed",
811
+ attempts,
812
+ maxAttempts,
813
+ duration
814
+ });
815
+ },
816
+ fail: (error, duration) => {
817
+ return this.record({
818
+ batchId,
819
+ name,
820
+ queue,
821
+ data,
822
+ status: "failed",
823
+ attempts,
824
+ maxAttempts,
825
+ error: error instanceof Error ? error.message : error,
826
+ duration
827
+ });
828
+ }
829
+ };
830
+ }
831
+ };
832
+ function wrapJobProcessor(watcher, name, processor, options = {}) {
833
+ return async (data) => {
834
+ const tracker = watcher.createJobTracker(name, {
835
+ queue: options.queue,
836
+ data,
837
+ maxAttempts: options.maxAttempts
838
+ });
839
+ const startTime = performance.now();
840
+ tracker.start();
841
+ try {
842
+ const result = await processor(data);
843
+ const duration = Math.round(performance.now() - startTime);
844
+ tracker.complete(duration);
845
+ return result;
846
+ } catch (error) {
847
+ const duration = Math.round(performance.now() - startTime);
848
+ tracker.fail(error instanceof Error ? error : String(error), duration);
849
+ throw error;
850
+ }
851
+ };
852
+ }
853
+
854
+ // src/server/api.ts
855
+ var ApiHandler = class {
856
+ constructor(storage) {
857
+ this.storage = storage;
858
+ }
859
+ /**
860
+ * Handle an API request
861
+ */
862
+ async handle(req) {
863
+ const path = new URL(req.url, "http://localhost").pathname;
864
+ try {
865
+ if (req.method === "GET" && path === "/api/entries") {
866
+ return this.listEntries(req);
867
+ }
868
+ if (req.method === "GET" && path.match(/^\/api\/entries\/[^/]+$/)) {
869
+ const id = path.split("/").pop();
870
+ return this.getEntry(id);
871
+ }
872
+ if (req.method === "GET" && path.match(/^\/api\/batch\/[^/]+$/)) {
873
+ const batchId = path.split("/").pop();
874
+ return this.getBatch(batchId);
875
+ }
876
+ if (req.method === "GET" && path === "/api/stats") {
877
+ return this.getStats();
878
+ }
879
+ if (req.method === "DELETE" && path === "/api/entries") {
880
+ return this.clearEntries();
881
+ }
882
+ if (req.method === "POST" && path === "/api/prune") {
883
+ return this.pruneEntries(req);
884
+ }
885
+ return {
886
+ status: 404,
887
+ body: { error: "Not found" }
888
+ };
889
+ } catch (error) {
890
+ console.error("NodeScope API error:", error);
891
+ return {
892
+ status: 500,
893
+ body: { error: error instanceof Error ? error.message : "Internal server error" }
894
+ };
895
+ }
896
+ }
897
+ async listEntries(req) {
898
+ const options = {};
899
+ if (req.query.type) {
900
+ options.type = String(req.query.type);
901
+ }
902
+ if (req.query.batchId) {
903
+ options.batchId = String(req.query.batchId);
904
+ }
905
+ if (req.query.search) {
906
+ options.search = String(req.query.search);
907
+ }
908
+ if (req.query.tags) {
909
+ options.tags = Array.isArray(req.query.tags) ? req.query.tags : [String(req.query.tags)];
910
+ }
911
+ if (req.query.before) {
912
+ options.before = new Date(String(req.query.before));
913
+ }
914
+ if (req.query.after) {
915
+ options.after = new Date(String(req.query.after));
916
+ }
917
+ if (req.query.limit) {
918
+ options.limit = parseInt(String(req.query.limit), 10);
919
+ }
920
+ if (req.query.offset) {
921
+ options.offset = parseInt(String(req.query.offset), 10);
922
+ }
923
+ const result = await this.storage.list(options);
924
+ return {
925
+ status: 200,
926
+ body: result
927
+ };
928
+ }
929
+ async getEntry(id) {
930
+ const entry = await this.storage.find(id);
931
+ if (!entry) {
932
+ return {
933
+ status: 404,
934
+ body: { error: "Entry not found" }
935
+ };
936
+ }
937
+ return {
938
+ status: 200,
939
+ body: entry
940
+ };
941
+ }
942
+ async getBatch(batchId) {
943
+ const entries = await this.storage.findByBatch(batchId);
944
+ return {
945
+ status: 200,
946
+ body: { batchId, entries }
947
+ };
948
+ }
949
+ async getStats() {
950
+ const stats = await this.storage.stats();
951
+ return {
952
+ status: 200,
953
+ body: stats
954
+ };
955
+ }
956
+ async clearEntries() {
957
+ await this.storage.clear();
958
+ return {
959
+ status: 200,
960
+ body: { success: true, message: "All entries cleared" }
961
+ };
962
+ }
963
+ async pruneEntries(req) {
964
+ const body = req.body;
965
+ const hours = body?.hours ?? 24;
966
+ const beforeDate = new Date(Date.now() - hours * 60 * 60 * 1e3);
967
+ const pruned = await this.storage.prune(beforeDate);
968
+ return {
969
+ status: 200,
970
+ body: { success: true, pruned, message: `Pruned ${pruned} entries older than ${hours} hours` }
971
+ };
972
+ }
973
+ };
974
+
975
+ // src/server/websocket.ts
976
+ var RealTimeServer = class {
977
+ clients = /* @__PURE__ */ new Set();
978
+ heartbeatInterval;
979
+ heartbeatMs;
980
+ constructor(options = {}) {
981
+ this.heartbeatMs = options.heartbeatInterval ?? 3e4;
982
+ }
983
+ /**
984
+ * Handle a new WebSocket connection
985
+ */
986
+ handleConnection(ws) {
987
+ this.clients.add(ws);
988
+ this.sendTo(ws, {
989
+ type: "connected",
990
+ clients: this.clients.size
991
+ });
992
+ }
993
+ /**
994
+ * Handle WebSocket disconnection
995
+ */
996
+ handleDisconnection(ws) {
997
+ this.clients.delete(ws);
998
+ }
999
+ /**
1000
+ * Broadcast a new entry to all connected clients
1001
+ */
1002
+ broadcastEntry(entry) {
1003
+ this.broadcast({
1004
+ type: "entry",
1005
+ data: entry
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Broadcast stats update to all clients
1010
+ */
1011
+ broadcastStats(stats) {
1012
+ this.broadcast({
1013
+ type: "stats",
1014
+ data: stats
1015
+ });
1016
+ }
1017
+ /**
1018
+ * Start heartbeat to keep connections alive
1019
+ */
1020
+ startHeartbeat() {
1021
+ if (this.heartbeatInterval) return;
1022
+ this.heartbeatInterval = setInterval(() => {
1023
+ this.broadcast({ type: "ping", timestamp: Date.now() });
1024
+ }, this.heartbeatMs);
1025
+ }
1026
+ /**
1027
+ * Stop heartbeat
1028
+ */
1029
+ stopHeartbeat() {
1030
+ if (this.heartbeatInterval) {
1031
+ clearInterval(this.heartbeatInterval);
1032
+ this.heartbeatInterval = void 0;
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Get number of connected clients
1037
+ */
1038
+ get clientCount() {
1039
+ return this.clients.size;
1040
+ }
1041
+ broadcast(message) {
1042
+ const data = JSON.stringify(message);
1043
+ for (const client of this.clients) {
1044
+ if (client.readyState === 1) {
1045
+ try {
1046
+ client.send(data);
1047
+ } catch {
1048
+ this.clients.delete(client);
1049
+ }
1050
+ }
1051
+ }
1052
+ }
1053
+ sendTo(ws, message) {
1054
+ if (ws.readyState === 1) {
1055
+ ws.send(JSON.stringify(message));
1056
+ }
1057
+ }
1058
+ };
1059
+
1060
+ // src/nodescope.ts
1061
+ var DEFAULT_CONFIG = {
1062
+ enabled: true,
1063
+ storage: "memory",
1064
+ databaseUrl: void 0,
1065
+ dashboardPath: "/_nodescope",
1066
+ watchers: {
1067
+ request: true,
1068
+ query: true,
1069
+ cache: true,
1070
+ log: true,
1071
+ exception: true,
1072
+ httpClient: true,
1073
+ event: true,
1074
+ job: true
1075
+ },
1076
+ realtime: true,
1077
+ pruneAfterHours: 24
1078
+ };
1079
+ var NodeScope = class {
1080
+ config;
1081
+ storage;
1082
+ apiHandler;
1083
+ realTimeServer;
1084
+ initialized = false;
1085
+ cleanupFns = [];
1086
+ // Watchers
1087
+ requestWatcher;
1088
+ queryWatcher;
1089
+ cacheWatcher;
1090
+ logWatcher;
1091
+ exceptionWatcher;
1092
+ httpClientWatcher;
1093
+ eventWatcher;
1094
+ jobWatcher;
1095
+ // Current request context (for middleware use)
1096
+ currentBatchId;
1097
+ constructor(config = {}) {
1098
+ this.config = { ...DEFAULT_CONFIG, ...config };
1099
+ this.realTimeServer = new RealTimeServer();
1100
+ const wc = this.config.watchers;
1101
+ this.requestWatcher = new RequestWatcher(
1102
+ typeof wc.request === "object" ? wc.request : {}
1103
+ );
1104
+ this.requestWatcher.enabled = wc.request !== false;
1105
+ this.queryWatcher = new QueryWatcher(
1106
+ typeof wc.query === "object" ? wc.query : {}
1107
+ );
1108
+ this.queryWatcher.enabled = wc.query !== false;
1109
+ this.cacheWatcher = new CacheWatcher(
1110
+ typeof wc.cache === "object" ? wc.cache : {}
1111
+ );
1112
+ this.cacheWatcher.enabled = wc.cache !== false;
1113
+ this.logWatcher = new LogWatcher(
1114
+ typeof wc.log === "object" ? wc.log : {}
1115
+ );
1116
+ this.logWatcher.enabled = wc.log !== false;
1117
+ this.exceptionWatcher = new ExceptionWatcher(
1118
+ typeof wc.exception === "object" ? wc.exception : {}
1119
+ );
1120
+ this.exceptionWatcher.enabled = wc.exception !== false;
1121
+ this.httpClientWatcher = new HttpClientWatcher(
1122
+ typeof wc.httpClient === "object" ? wc.httpClient : {}
1123
+ );
1124
+ this.httpClientWatcher.enabled = wc.httpClient !== false;
1125
+ this.eventWatcher = new EventWatcher(
1126
+ typeof wc.event === "object" ? wc.event : {}
1127
+ );
1128
+ this.eventWatcher.enabled = wc.event !== false;
1129
+ this.jobWatcher = new JobWatcher(
1130
+ typeof wc.job === "object" ? wc.job : {}
1131
+ );
1132
+ this.jobWatcher.enabled = wc.job !== false;
1133
+ }
1134
+ /**
1135
+ * Initialize storage and start background processes
1136
+ */
1137
+ async initialize() {
1138
+ if (this.initialized) return;
1139
+ this.storage = await createStorageAdapter(this.config.storage, {
1140
+ databaseUrl: this.config.databaseUrl
1141
+ });
1142
+ await this.storage.initialize();
1143
+ this.apiHandler = new ApiHandler(this.storage);
1144
+ if (this.exceptionWatcher.enabled) {
1145
+ const cleanup = setupGlobalErrorHandlers(
1146
+ this.exceptionWatcher,
1147
+ (entry) => this.recordEntry(entry)
1148
+ );
1149
+ this.cleanupFns.push(cleanup);
1150
+ }
1151
+ if (this.config.realtime) {
1152
+ this.realTimeServer.startHeartbeat();
1153
+ }
1154
+ if (this.config.pruneAfterHours && this.config.pruneAfterHours > 0) {
1155
+ const pruneInterval = setInterval(async () => {
1156
+ const beforeDate = new Date(
1157
+ Date.now() - this.config.pruneAfterHours * 60 * 60 * 1e3
1158
+ );
1159
+ await this.storage.prune(beforeDate);
1160
+ }, 60 * 60 * 1e3);
1161
+ this.cleanupFns.push(() => clearInterval(pruneInterval));
1162
+ }
1163
+ this.initialized = true;
1164
+ }
1165
+ /**
1166
+ * Record an entry to storage and broadcast
1167
+ */
1168
+ async recordEntry(entry) {
1169
+ if (!this.config.enabled) return;
1170
+ if (this.config.filter && !this.config.filter(entry)) {
1171
+ return;
1172
+ }
1173
+ if (this.config.tag) {
1174
+ const customTags = this.config.tag(entry);
1175
+ entry.tags = [...entry.tags, ...customTags];
1176
+ }
1177
+ await this.storage.save(entry);
1178
+ if (this.config.realtime) {
1179
+ this.realTimeServer.broadcastEntry(entry);
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Get the current batch ID
1184
+ */
1185
+ get batchId() {
1186
+ return this.currentBatchId;
1187
+ }
1188
+ /**
1189
+ * Create a new request context
1190
+ */
1191
+ createContext() {
1192
+ const ctx = createRequestContext();
1193
+ this.currentBatchId = ctx.batchId;
1194
+ return ctx;
1195
+ }
1196
+ /**
1197
+ * Get the dashboard path
1198
+ */
1199
+ get dashboardPath() {
1200
+ return this.config.dashboardPath;
1201
+ }
1202
+ /**
1203
+ * Get the API handler
1204
+ */
1205
+ get api() {
1206
+ return this.apiHandler;
1207
+ }
1208
+ /**
1209
+ * Get the real-time server
1210
+ */
1211
+ get realtime() {
1212
+ return this.realTimeServer;
1213
+ }
1214
+ /**
1215
+ * Get storage adapter
1216
+ */
1217
+ getStorage() {
1218
+ return this.storage;
1219
+ }
1220
+ /**
1221
+ * Check if NodeScope is enabled
1222
+ */
1223
+ get isEnabled() {
1224
+ return this.config.enabled;
1225
+ }
1226
+ /**
1227
+ * Check authorization
1228
+ */
1229
+ async checkAuthorization(req) {
1230
+ if (!this.config.authorization) return true;
1231
+ return this.config.authorization(req);
1232
+ }
1233
+ /**
1234
+ * Cleanup and close connections
1235
+ */
1236
+ async close() {
1237
+ this.realTimeServer.stopHeartbeat();
1238
+ for (const cleanup of this.cleanupFns) {
1239
+ cleanup();
1240
+ }
1241
+ this.cleanupFns = [];
1242
+ if (this.storage) {
1243
+ await this.storage.close();
1244
+ }
1245
+ }
1246
+ };
1247
+ var defaultInstance = null;
1248
+ function getNodeScope() {
1249
+ if (!defaultInstance) {
1250
+ defaultInstance = new NodeScope();
1251
+ }
1252
+ return defaultInstance;
1253
+ }
1254
+ async function initNodeScope(config = {}) {
1255
+ defaultInstance = new NodeScope(config);
1256
+ await defaultInstance.initialize();
1257
+ return defaultInstance;
1258
+ }
1259
+
1260
+ // src/dashboard/index.ts
1261
+ function getDashboardHtml(basePath) {
1262
+ return `<!DOCTYPE html>
1263
+ <html lang="en" class="dark">
1264
+ <head>
1265
+ <meta charset="UTF-8">
1266
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1267
+ <title>NodeScope</title>
1268
+ <script src="https://cdn.tailwindcss.com"></script>
1269
+ <script>
1270
+ tailwind.config = {
1271
+ darkMode: 'class',
1272
+ theme: {
1273
+ extend: {
1274
+ colors: {
1275
+ primary: {
1276
+ 50: '#f0f9ff',
1277
+ 100: '#e0f2fe',
1278
+ 200: '#bae6fd',
1279
+ 300: '#7dd3fc',
1280
+ 400: '#38bdf8',
1281
+ 500: '#0ea5e9',
1282
+ 600: '#0284c7',
1283
+ 700: '#0369a1',
1284
+ 800: '#075985',
1285
+ 900: '#0c4a6e',
1286
+ 950: '#082f49',
1287
+ },
1288
+ },
1289
+ },
1290
+ },
1291
+ }
1292
+ </script>
1293
+ <style>
1294
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
1295
+
1296
+ body {
1297
+ font-family: 'Inter', sans-serif;
1298
+ }
1299
+
1300
+ code, pre, .mono {
1301
+ font-family: 'JetBrains Mono', monospace;
1302
+ }
1303
+
1304
+ .glass {
1305
+ background: rgba(15, 23, 42, 0.7);
1306
+ backdrop-filter: blur(12px);
1307
+ border: 1px solid rgba(255, 255, 255, 0.1);
1308
+ }
1309
+
1310
+ .entry-row:hover {
1311
+ background: rgba(255, 255, 255, 0.05);
1312
+ }
1313
+
1314
+ .status-success { color: #4ade80; }
1315
+ .status-warning { color: #fbbf24; }
1316
+ .status-error { color: #f87171; }
1317
+ .status-info { color: #60a5fa; }
1318
+
1319
+ .animate-pulse-slow {
1320
+ animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1321
+ }
1322
+
1323
+ .scrollbar-thin::-webkit-scrollbar {
1324
+ width: 6px;
1325
+ }
1326
+ .scrollbar-thin::-webkit-scrollbar-track {
1327
+ background: rgba(255, 255, 255, 0.05);
1328
+ }
1329
+ .scrollbar-thin::-webkit-scrollbar-thumb {
1330
+ background: rgba(255, 255, 255, 0.2);
1331
+ border-radius: 3px;
1332
+ }
1333
+
1334
+ /* JSON syntax highlighting */
1335
+ .json-key { color: #93c5fd; }
1336
+ .json-string { color: #86efac; }
1337
+ .json-number { color: #fcd34d; }
1338
+ .json-boolean { color: #f9a8d4; }
1339
+ .json-null { color: #a78bfa; }
1340
+ </style>
1341
+ </head>
1342
+ <body class="bg-slate-950 text-slate-100 min-h-screen">
1343
+ <div id="app"></div>
1344
+
1345
+ <script>
1346
+ const API_BASE = '${basePath}/api';
1347
+ const WS_URL = location.protocol === 'https:'
1348
+ ? 'wss://' + location.host + '${basePath}/ws'
1349
+ : 'ws://' + location.host + '${basePath}/ws';
1350
+
1351
+ // State
1352
+ let state = {
1353
+ entries: [],
1354
+ stats: null,
1355
+ selectedEntry: null,
1356
+ selectedType: null,
1357
+ search: '',
1358
+ loading: true,
1359
+ connected: false,
1360
+ page: 0,
1361
+ hasMore: false,
1362
+ };
1363
+
1364
+ // Types
1365
+ const ENTRY_TYPES = [
1366
+ { type: 'request', label: 'Requests', icon: '\u{1F310}' },
1367
+ { type: 'query', label: 'Queries', icon: '\u{1F50D}' },
1368
+ { type: 'cache', label: 'Cache', icon: '\u{1F4BE}' },
1369
+ { type: 'log', label: 'Logs', icon: '\u{1F4DD}' },
1370
+ { type: 'exception', label: 'Exceptions', icon: '\u26A0\uFE0F' },
1371
+ { type: 'http_client', label: 'HTTP Client', icon: '\u{1F4E1}' },
1372
+ { type: 'event', label: 'Events', icon: '\u{1F4E3}' },
1373
+ { type: 'job', label: 'Jobs', icon: '\u2699\uFE0F' },
1374
+ ];
1375
+
1376
+ // Fetch entries
1377
+ async function fetchEntries() {
1378
+ state.loading = true;
1379
+ render();
1380
+
1381
+ const params = new URLSearchParams();
1382
+ if (state.selectedType) params.set('type', state.selectedType);
1383
+ if (state.search) params.set('search', state.search);
1384
+ params.set('limit', '50');
1385
+ params.set('offset', String(state.page * 50));
1386
+
1387
+ try {
1388
+ const res = await fetch(API_BASE + '/entries?' + params);
1389
+ const data = await res.json();
1390
+ state.entries = data.data || [];
1391
+ state.hasMore = data.hasMore;
1392
+ } catch (e) {
1393
+ console.error('Failed to fetch entries:', e);
1394
+ state.entries = [];
1395
+ }
1396
+
1397
+ state.loading = false;
1398
+ render();
1399
+ }
1400
+
1401
+ // Fetch stats
1402
+ async function fetchStats() {
1403
+ try {
1404
+ const res = await fetch(API_BASE + '/stats');
1405
+ state.stats = await res.json();
1406
+ } catch (e) {
1407
+ console.error('Failed to fetch stats:', e);
1408
+ }
1409
+ render();
1410
+ }
1411
+
1412
+ // Clear entries
1413
+ async function clearEntries() {
1414
+ if (!confirm('Are you sure you want to clear all entries?')) return;
1415
+
1416
+ try {
1417
+ await fetch(API_BASE + '/entries', { method: 'DELETE' });
1418
+ state.entries = [];
1419
+ state.selectedEntry = null;
1420
+ fetchStats();
1421
+ } catch (e) {
1422
+ console.error('Failed to clear entries:', e);
1423
+ }
1424
+ render();
1425
+ }
1426
+
1427
+ // Format date
1428
+ function formatDate(dateStr) {
1429
+ const date = new Date(dateStr);
1430
+ return date.toLocaleTimeString();
1431
+ }
1432
+
1433
+ // Format duration
1434
+ function formatDuration(ms) {
1435
+ if (ms < 1) return '<1ms';
1436
+ if (ms < 1000) return Math.round(ms) + 'ms';
1437
+ return (ms / 1000).toFixed(2) + 's';
1438
+ }
1439
+
1440
+ // Get status color
1441
+ function getStatusColor(status) {
1442
+ if (status >= 500) return 'status-error';
1443
+ if (status >= 400) return 'status-warning';
1444
+ if (status >= 200 && status < 300) return 'status-success';
1445
+ return 'status-info';
1446
+ }
1447
+
1448
+ // Syntax highlight JSON
1449
+ function highlightJson(obj, indent = 0) {
1450
+ if (obj === null) return '<span class="json-null">null</span>';
1451
+ if (typeof obj === 'boolean') return '<span class="json-boolean">' + obj + '</span>';
1452
+ if (typeof obj === 'number') return '<span class="json-number">' + obj + '</span>';
1453
+ if (typeof obj === 'string') {
1454
+ const escaped = obj.replace(/</g, '&lt;').replace(/>/g, '&gt;');
1455
+ if (escaped.length > 200) {
1456
+ return '<span class="json-string">"' + escaped.substring(0, 200) + '..."</span>';
1457
+ }
1458
+ return '<span class="json-string">"' + escaped + '"</span>';
1459
+ }
1460
+ if (Array.isArray(obj)) {
1461
+ if (obj.length === 0) return '[]';
1462
+ const items = obj.map(i => ' '.repeat(indent + 1) + highlightJson(i, indent + 1)).join(',\\n');
1463
+ return '[\\n' + items + '\\n' + ' '.repeat(indent) + ']';
1464
+ }
1465
+ if (typeof obj === 'object') {
1466
+ const keys = Object.keys(obj);
1467
+ if (keys.length === 0) return '{}';
1468
+ const items = keys.map(k => {
1469
+ const key = '<span class="json-key">"' + k + '"</span>';
1470
+ const value = highlightJson(obj[k], indent + 1);
1471
+ return ' '.repeat(indent + 1) + key + ': ' + value;
1472
+ }).join(',\\n');
1473
+ return '{\\n' + items + '\\n' + ' '.repeat(indent) + '}';
1474
+ }
1475
+ return String(obj);
1476
+ }
1477
+
1478
+ // Render entry summary
1479
+ function renderEntrySummary(entry) {
1480
+ switch (entry.type) {
1481
+ case 'request':
1482
+ const req = entry.content;
1483
+ return \`
1484
+ <span class="font-medium">\${req.method}</span>
1485
+ <span class="text-slate-400 mx-1">\${req.path}</span>
1486
+ <span class="\${getStatusColor(req.response?.status || 0)}">\${req.response?.status || '...'}</span>
1487
+ \`;
1488
+ case 'query':
1489
+ const sql = entry.content.sql?.substring(0, 60) || '';
1490
+ return \`
1491
+ <span class="mono text-sm">\${sql}\${sql.length >= 60 ? '...' : ''}</span>
1492
+ \${entry.content.slow ? '<span class="ml-2 px-1 py-0.5 rounded bg-yellow-900 text-yellow-200 text-xs">slow</span>' : ''}
1493
+ \`;
1494
+ case 'cache':
1495
+ return \`
1496
+ <span class="font-medium">\${entry.content.operation}</span>
1497
+ <span class="text-slate-400 mx-1">\${entry.content.key}</span>
1498
+ \`;
1499
+ case 'log':
1500
+ return \`
1501
+ <span class="\${entry.content.level === 'error' ? 'status-error' : entry.content.level === 'warn' ? 'status-warning' : 'text-slate-300'}">\${entry.content.message?.substring(0, 80) || ''}</span>
1502
+ \`;
1503
+ case 'exception':
1504
+ return \`
1505
+ <span class="status-error font-medium">\${entry.content.class}</span>
1506
+ <span class="text-slate-400 mx-1">\${entry.content.message?.substring(0, 50) || ''}</span>
1507
+ \`;
1508
+ case 'http_client':
1509
+ const hc = entry.content;
1510
+ return \`
1511
+ <span class="font-medium">\${hc.method}</span>
1512
+ <span class="text-slate-400 mx-1 mono text-sm">\${new URL(hc.url).host}</span>
1513
+ <span class="\${getStatusColor(hc.response?.status || 0)}">\${hc.response?.status || '...'}</span>
1514
+ \`;
1515
+ case 'event':
1516
+ return \`
1517
+ <span class="font-medium">\${entry.content.name}</span>
1518
+ <span class="text-slate-400 mx-1">\${entry.content.listeners?.length || 0} listeners</span>
1519
+ \`;
1520
+ case 'job':
1521
+ return \`
1522
+ <span class="font-medium">\${entry.content.name}</span>
1523
+ <span class="\${entry.content.status === 'completed' ? 'status-success' : entry.content.status === 'failed' ? 'status-error' : 'status-info'} ml-1">\${entry.content.status}</span>
1524
+ \`;
1525
+ default:
1526
+ return \`<span class="text-slate-400">\${entry.type}</span>\`;
1527
+ }
1528
+ }
1529
+
1530
+ // Render entry detail
1531
+ function renderEntryDetail(entry) {
1532
+ if (!entry) {
1533
+ return \`
1534
+ <div class="flex items-center justify-center h-full text-slate-500">
1535
+ <p>Select an entry to view details</p>
1536
+ </div>
1537
+ \`;
1538
+ }
1539
+
1540
+ return \`
1541
+ <div class="p-4 space-y-4 overflow-y-auto h-full scrollbar-thin">
1542
+ <div class="flex items-center justify-between">
1543
+ <h3 class="text-lg font-semibold capitalize">\${entry.type}</h3>
1544
+ <span class="text-sm text-slate-400">\${formatDate(entry.createdAt)}</span>
1545
+ </div>
1546
+
1547
+ \${entry.duration ? \`
1548
+ <div class="flex items-center gap-4 text-sm">
1549
+ <span class="text-slate-400">Duration:</span>
1550
+ <span class="font-mono">\${formatDuration(entry.duration)}</span>
1551
+ </div>
1552
+ \` : ''}
1553
+
1554
+ \${entry.tags.length ? \`
1555
+ <div class="flex flex-wrap gap-1">
1556
+ \${entry.tags.map(t => \`<span class="px-2 py-0.5 rounded-full bg-slate-800 text-slate-300 text-xs">\${t}</span>\`).join('')}
1557
+ </div>
1558
+ \` : ''}
1559
+
1560
+ <div class="space-y-2">
1561
+ <h4 class="text-sm font-medium text-slate-400">Content</h4>
1562
+ <pre class="p-4 rounded-lg bg-slate-900 overflow-x-auto text-sm mono">\${highlightJson(entry.content)}</pre>
1563
+ </div>
1564
+ </div>
1565
+ \`;
1566
+ }
1567
+
1568
+ // Main render function
1569
+ function render() {
1570
+ const app = document.getElementById('app');
1571
+
1572
+ app.innerHTML = \`
1573
+ <div class="flex h-screen">
1574
+ <!-- Sidebar -->
1575
+ <aside class="w-64 glass border-r border-slate-800 flex flex-col">
1576
+ <div class="p-4 border-b border-slate-800">
1577
+ <h1 class="text-xl font-bold bg-gradient-to-r from-primary-400 to-primary-600 bg-clip-text text-transparent">
1578
+ \u26A1 NodeScope
1579
+ </h1>
1580
+ <p class="text-xs text-slate-500 mt-1">Debug Assistant</p>
1581
+ </div>
1582
+
1583
+ <nav class="flex-1 p-2 space-y-1 overflow-y-auto scrollbar-thin">
1584
+ <button
1585
+ onclick="state.selectedType = null; state.page = 0; fetchEntries();"
1586
+ class="w-full text-left px-3 py-2 rounded-lg flex items-center gap-2 \${!state.selectedType ? 'bg-primary-600/20 text-primary-300' : 'hover:bg-slate-800 text-slate-300'}">
1587
+ <span>\u{1F4CA}</span>
1588
+ <span>All</span>
1589
+ \${state.stats ? \`<span class="ml-auto text-xs text-slate-500">\${state.stats.totalEntries}</span>\` : ''}
1590
+ </button>
1591
+
1592
+ \${ENTRY_TYPES.map(t => \`
1593
+ <button
1594
+ onclick="state.selectedType = '\${t.type}'; state.page = 0; fetchEntries();"
1595
+ class="w-full text-left px-3 py-2 rounded-lg flex items-center gap-2 \${state.selectedType === t.type ? 'bg-primary-600/20 text-primary-300' : 'hover:bg-slate-800 text-slate-300'}">
1596
+ <span>\${t.icon}</span>
1597
+ <span>\${t.label}</span>
1598
+ \${state.stats?.entriesByType ? \`<span class="ml-auto text-xs text-slate-500">\${state.stats.entriesByType[t.type] || 0}</span>\` : ''}
1599
+ </button>
1600
+ \`).join('')}
1601
+ </nav>
1602
+
1603
+ <div class="p-2 border-t border-slate-800">
1604
+ <button
1605
+ onclick="clearEntries()"
1606
+ class="w-full px-3 py-2 rounded-lg text-red-400 hover:bg-red-900/20 flex items-center gap-2 text-sm">
1607
+ <span>\u{1F5D1}\uFE0F</span>
1608
+ <span>Clear All</span>
1609
+ </button>
1610
+ </div>
1611
+ </aside>
1612
+
1613
+ <!-- Main content -->
1614
+ <main class="flex-1 flex flex-col">
1615
+ <!-- Header -->
1616
+ <header class="glass border-b border-slate-800 p-4 flex items-center gap-4">
1617
+ <div class="relative flex-1 max-w-md">
1618
+ <input
1619
+ type="text"
1620
+ placeholder="Search entries..."
1621
+ value="\${state.search}"
1622
+ oninput="state.search = this.value; state.page = 0; fetchEntries();"
1623
+ class="w-full px-4 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:border-primary-500 focus:outline-none text-sm">
1624
+ </div>
1625
+
1626
+ <div class="flex items-center gap-2">
1627
+ <span class="w-2 h-2 rounded-full \${state.connected ? 'bg-green-500 animate-pulse-slow' : 'bg-red-500'}"></span>
1628
+ <span class="text-xs text-slate-500">\${state.connected ? 'Live' : 'Offline'}</span>
1629
+ </div>
1630
+
1631
+ <button
1632
+ onclick="fetchEntries(); fetchStats();"
1633
+ class="px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-sm">
1634
+ \u{1F504} Refresh
1635
+ </button>
1636
+ </header>
1637
+
1638
+ <!-- Content -->
1639
+ <div class="flex-1 flex overflow-hidden">
1640
+ <!-- Entry list -->
1641
+ <div class="w-1/2 border-r border-slate-800 flex flex-col">
1642
+ <div class="flex-1 overflow-y-auto scrollbar-thin">
1643
+ \${state.loading ? \`
1644
+ <div class="flex items-center justify-center h-32">
1645
+ <div class="animate-spin w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full"></div>
1646
+ </div>
1647
+ \` : state.entries.length === 0 ? \`
1648
+ <div class="flex items-center justify-center h-32 text-slate-500">
1649
+ <p>No entries found</p>
1650
+ </div>
1651
+ \` : \`
1652
+ <div class="divide-y divide-slate-800">
1653
+ \${state.entries.map((entry, i) => \`
1654
+ <div
1655
+ onclick="state.selectedEntry = state.entries[\${i}]; render();"
1656
+ class="entry-row p-3 cursor-pointer \${state.selectedEntry?.id === entry.id ? 'bg-primary-600/10 border-l-2 border-primary-500' : ''}">
1657
+ <div class="flex items-center justify-between text-sm">
1658
+ <div class="flex items-center gap-2 flex-1 min-w-0">
1659
+ <span class="text-xs text-slate-500">\${formatDate(entry.createdAt)}</span>
1660
+ <span class="truncate">\${renderEntrySummary(entry)}</span>
1661
+ </div>
1662
+ \${entry.duration ? \`<span class="text-xs text-slate-500 ml-2">\${formatDuration(entry.duration)}</span>\` : ''}
1663
+ </div>
1664
+ </div>
1665
+ \`).join('')}
1666
+ </div>
1667
+ \`}
1668
+ </div>
1669
+
1670
+ \${state.hasMore || state.page > 0 ? \`
1671
+ <div class="p-2 border-t border-slate-800 flex justify-between">
1672
+ <button
1673
+ onclick="state.page = Math.max(0, state.page - 1); fetchEntries();"
1674
+ \${state.page === 0 ? 'disabled' : ''}
1675
+ class="px-3 py-1 rounded bg-slate-800 hover:bg-slate-700 text-sm disabled:opacity-50">
1676
+ \u2190 Prev
1677
+ </button>
1678
+ <span class="text-sm text-slate-500">Page \${state.page + 1}</span>
1679
+ <button
1680
+ onclick="state.page++; fetchEntries();"
1681
+ \${!state.hasMore ? 'disabled' : ''}
1682
+ class="px-3 py-1 rounded bg-slate-800 hover:bg-slate-700 text-sm disabled:opacity-50">
1683
+ Next \u2192
1684
+ </button>
1685
+ </div>
1686
+ \` : ''}
1687
+ </div>
1688
+
1689
+ <!-- Entry detail -->
1690
+ <div class="w-1/2 bg-slate-900/50">
1691
+ \${renderEntryDetail(state.selectedEntry)}
1692
+ </div>
1693
+ </div>
1694
+ </main>
1695
+ </div>
1696
+ \`;
1697
+ }
1698
+
1699
+ // Initialize
1700
+ fetchStats();
1701
+ fetchEntries();
1702
+
1703
+ // WebSocket connection (optional)
1704
+ try {
1705
+ const ws = new WebSocket(WS_URL);
1706
+ ws.onopen = () => {
1707
+ state.connected = true;
1708
+ render();
1709
+ };
1710
+ ws.onclose = () => {
1711
+ state.connected = false;
1712
+ render();
1713
+ };
1714
+ ws.onmessage = (event) => {
1715
+ const data = JSON.parse(event.data);
1716
+ if (data.type === 'entry') {
1717
+ state.entries.unshift(data.data);
1718
+ if (state.entries.length > 50) state.entries.pop();
1719
+ render();
1720
+ }
1721
+ if (data.type === 'stats') {
1722
+ state.stats = data.data;
1723
+ render();
1724
+ }
1725
+ };
1726
+ } catch (e) {
1727
+ console.log('WebSocket not available');
1728
+ }
1729
+ </script>
1730
+ </body>
1731
+ </html>`;
1732
+ }
1733
+
1734
+ // src/adapters/express.ts
1735
+ function createExpressMiddleware(nodescope2) {
1736
+ return async (req, res, next) => {
1737
+ if (!nodescope2.isEnabled) {
1738
+ return next();
1739
+ }
1740
+ const dashboardPath = nodescope2.dashboardPath;
1741
+ if (req.path.startsWith(dashboardPath)) {
1742
+ return next();
1743
+ }
1744
+ const ctx = nodescope2.createContext();
1745
+ req.nodescope = ctx;
1746
+ const originalSend = res.send;
1747
+ const originalJson = res.json;
1748
+ let responseBody;
1749
+ res.send = function(body) {
1750
+ responseBody = body;
1751
+ return originalSend.call(this, body);
1752
+ };
1753
+ res.json = function(body) {
1754
+ responseBody = body;
1755
+ return originalJson.call(this, body);
1756
+ };
1757
+ res.on("finish", async () => {
1758
+ if (!nodescope2.requestWatcher.enabled) return;
1759
+ try {
1760
+ const entry = nodescope2.requestWatcher.record({
1761
+ batchId: ctx.batchId,
1762
+ startTime: ctx.startTime,
1763
+ method: req.method,
1764
+ url: req.originalUrl || req.url,
1765
+ path: req.path,
1766
+ query: req.query,
1767
+ headers: req.headers,
1768
+ body: req.body,
1769
+ ip: req.ip || req.socket?.remoteAddress,
1770
+ userAgent: req.get("user-agent"),
1771
+ session: req.session,
1772
+ response: {
1773
+ status: res.statusCode,
1774
+ headers: res.getHeaders(),
1775
+ body: responseBody
1776
+ }
1777
+ });
1778
+ if (entry) {
1779
+ await nodescope2.recordEntry(entry);
1780
+ }
1781
+ } catch (error) {
1782
+ console.error("NodeScope error recording request:", error);
1783
+ }
1784
+ });
1785
+ next();
1786
+ };
1787
+ }
1788
+ async function mountExpressRoutes(app, nodescope2) {
1789
+ const dashboardPath = nodescope2.dashboardPath;
1790
+ app.use(createExpressMiddleware(nodescope2));
1791
+ app.use(dashboardPath, async (req, res, next) => {
1792
+ const authorized = await nodescope2.checkAuthorization(req);
1793
+ if (!authorized) {
1794
+ res.status(403).json({ error: "Unauthorized" });
1795
+ return;
1796
+ }
1797
+ if (req.path === "/" || req.path === "") {
1798
+ res.setHeader("Content-Type", "text/html");
1799
+ res.send(getDashboardHtml(dashboardPath));
1800
+ return;
1801
+ }
1802
+ next();
1803
+ });
1804
+ app.use(`${dashboardPath}/api`, async (req, res) => {
1805
+ const authorized = await nodescope2.checkAuthorization(req);
1806
+ if (!authorized) {
1807
+ res.status(403).json({ error: "Unauthorized" });
1808
+ return;
1809
+ }
1810
+ const apiPath = `/api${req.path}`;
1811
+ const response = await nodescope2.api.handle({
1812
+ method: req.method,
1813
+ url: apiPath,
1814
+ query: req.query,
1815
+ body: req.body
1816
+ });
1817
+ if (response.headers) {
1818
+ for (const [key, value] of Object.entries(response.headers)) {
1819
+ res.setHeader(key, value);
1820
+ }
1821
+ }
1822
+ res.status(response.status).json(response.body);
1823
+ });
1824
+ }
1825
+ function attachWebSocket(server, nodescope2, options = {}) {
1826
+ const wsPath = options.path ?? `${nodescope2.dashboardPath}/ws`;
1827
+ import("ws").then(({ WebSocketServer }) => {
1828
+ const wss = new WebSocketServer({ noServer: true });
1829
+ server.on("upgrade", (request, socket, head) => {
1830
+ const url = new URL(request.url || "", `http://${request.headers.host}`);
1831
+ if (url.pathname === wsPath) {
1832
+ wss.handleUpgrade(request, socket, head, (ws) => {
1833
+ nodescope2.realtime.handleConnection(ws);
1834
+ ws.on("close", () => {
1835
+ nodescope2.realtime.handleDisconnection(ws);
1836
+ });
1837
+ ws.on("message", (data) => {
1838
+ try {
1839
+ const message = JSON.parse(data.toString());
1840
+ if (message.type === "pong") {
1841
+ }
1842
+ } catch {
1843
+ }
1844
+ });
1845
+ });
1846
+ } else {
1847
+ socket.destroy();
1848
+ }
1849
+ });
1850
+ nodescope2.realtime.startHeartbeat();
1851
+ console.log(`\u26A1 NodeScope WebSocket available at ws://localhost:PORT${wsPath}`);
1852
+ }).catch(() => {
1853
+ console.warn("NodeScope: ws package not installed, real-time updates disabled");
1854
+ console.warn("Install with: npm install ws");
1855
+ });
1856
+ }
1857
+
1858
+ // src/adapters/hono.ts
1859
+ function createHonoMiddleware(nodescope2) {
1860
+ return async (c, next) => {
1861
+ if (!nodescope2.isEnabled) {
1862
+ return next();
1863
+ }
1864
+ const dashboardPath = nodescope2.dashboardPath;
1865
+ if (c.req.path.startsWith(dashboardPath)) {
1866
+ return next();
1867
+ }
1868
+ const ctx = nodescope2.createContext();
1869
+ c.set("nodescope", ctx);
1870
+ const startTime = ctx.startTime;
1871
+ await next();
1872
+ if (!nodescope2.requestWatcher.enabled) return;
1873
+ try {
1874
+ let responseBody;
1875
+ const response = c.res;
1876
+ if (response.headers.get("content-type")?.includes("application/json")) {
1877
+ try {
1878
+ responseBody = await response.clone().json();
1879
+ } catch {
1880
+ responseBody = void 0;
1881
+ }
1882
+ }
1883
+ const entry = nodescope2.requestWatcher.record({
1884
+ batchId: ctx.batchId,
1885
+ startTime,
1886
+ method: c.req.method,
1887
+ url: c.req.url,
1888
+ path: c.req.path,
1889
+ query: Object.fromEntries(new URL(c.req.url).searchParams),
1890
+ headers: Object.fromEntries(c.req.raw.headers),
1891
+ body: await getRequestBody(c),
1892
+ ip: c.req.header("x-forwarded-for") || c.req.header("x-real-ip"),
1893
+ userAgent: c.req.header("user-agent"),
1894
+ response: {
1895
+ status: c.res.status,
1896
+ headers: Object.fromEntries(c.res.headers),
1897
+ body: responseBody
1898
+ }
1899
+ });
1900
+ if (entry) {
1901
+ await nodescope2.recordEntry(entry);
1902
+ }
1903
+ } catch (error) {
1904
+ console.error("NodeScope error recording request:", error);
1905
+ }
1906
+ };
1907
+ }
1908
+ async function getRequestBody(c) {
1909
+ try {
1910
+ const contentType = c.req.header("content-type") || "";
1911
+ if (contentType.includes("application/json")) {
1912
+ return await c.req.json();
1913
+ }
1914
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1915
+ return await c.req.parseBody();
1916
+ }
1917
+ return void 0;
1918
+ } catch {
1919
+ return void 0;
1920
+ }
1921
+ }
1922
+ function createHonoDashboardRoutes(nodescope2) {
1923
+ const createRoutes = async () => {
1924
+ const { Hono } = await import("hono");
1925
+ const app = new Hono();
1926
+ const dashboardPath = nodescope2.dashboardPath;
1927
+ app.get("/", async (c) => {
1928
+ const authorized = await nodescope2.checkAuthorization(c.req.raw);
1929
+ if (!authorized) {
1930
+ return c.json({ error: "Unauthorized" }, 403);
1931
+ }
1932
+ return c.html(getDashboardHtml(dashboardPath));
1933
+ });
1934
+ app.all("/api/*", async (c) => {
1935
+ const authorized = await nodescope2.checkAuthorization(c.req.raw);
1936
+ if (!authorized) {
1937
+ return c.json({ error: "Unauthorized" }, 403);
1938
+ }
1939
+ const response = await nodescope2.api.handle({
1940
+ method: c.req.method,
1941
+ url: c.req.url,
1942
+ query: Object.fromEntries(new URL(c.req.url).searchParams),
1943
+ body: c.req.method !== "GET" ? await c.req.json().catch(() => void 0) : void 0
1944
+ });
1945
+ return c.json(response.body, response.status);
1946
+ });
1947
+ return app;
1948
+ };
1949
+ return createRoutes();
1950
+ }
1951
+ function nodescope(config = {}) {
1952
+ const ns = new NodeScope(config);
1953
+ let initialized = false;
1954
+ return async (c, next) => {
1955
+ if (!initialized) {
1956
+ await ns.initialize();
1957
+ initialized = true;
1958
+ }
1959
+ const dashboardPath = ns.dashboardPath;
1960
+ if (c.req.path.startsWith(dashboardPath)) {
1961
+ const authorized = await ns.checkAuthorization(c.req.raw);
1962
+ if (!authorized) {
1963
+ return c.json({ error: "Unauthorized" }, 403);
1964
+ }
1965
+ const subPath = c.req.path.slice(dashboardPath.length) || "/";
1966
+ if (subPath === "/" || subPath === "") {
1967
+ return c.html(getDashboardHtml(dashboardPath));
1968
+ }
1969
+ if (subPath.startsWith("/api")) {
1970
+ const response = await ns.api.handle({
1971
+ method: c.req.method,
1972
+ url: c.req.url,
1973
+ query: Object.fromEntries(new URL(c.req.url).searchParams),
1974
+ body: c.req.method !== "GET" ? await c.req.json().catch(() => void 0) : void 0
1975
+ });
1976
+ return c.json(response.body, response.status);
1977
+ }
1978
+ }
1979
+ return createHonoMiddleware(ns)(c, next);
1980
+ };
1981
+ }
1982
+
1983
+ // src/adapters/fastify.ts
1984
+ async function fastifyNodeScope(fastify, options) {
1985
+ const { nodescope: nodescope2 } = options;
1986
+ const dashboardPath = nodescope2.dashboardPath;
1987
+ fastify.addHook("onRequest", async (request) => {
1988
+ if (!nodescope2.isEnabled) return;
1989
+ if (request.url.startsWith(dashboardPath)) return;
1990
+ const ctx = nodescope2.createContext();
1991
+ request.nodescope = ctx;
1992
+ });
1993
+ fastify.addHook("onResponse", async (request, reply) => {
1994
+ if (!nodescope2.isEnabled) return;
1995
+ if (!nodescope2.requestWatcher.enabled) return;
1996
+ if (!request.nodescope) return;
1997
+ try {
1998
+ const entry = nodescope2.requestWatcher.record({
1999
+ batchId: request.nodescope.batchId,
2000
+ startTime: request.nodescope.startTime,
2001
+ method: request.method,
2002
+ url: request.url,
2003
+ path: request.routeOptions?.url || request.url.split("?")[0],
2004
+ query: request.query,
2005
+ headers: request.headers,
2006
+ body: request.body,
2007
+ ip: request.ip,
2008
+ userAgent: request.headers["user-agent"],
2009
+ session: void 0,
2010
+ response: {
2011
+ status: reply.statusCode,
2012
+ headers: reply.getHeaders(),
2013
+ body: void 0
2014
+ // Fastify doesn't expose response body easily
2015
+ }
2016
+ });
2017
+ if (entry) {
2018
+ await nodescope2.recordEntry(entry);
2019
+ }
2020
+ } catch (error) {
2021
+ fastify.log.error("NodeScope error recording request:", error);
2022
+ }
2023
+ });
2024
+ fastify.get(dashboardPath, async (request, reply) => {
2025
+ const authorized = await nodescope2.checkAuthorization(request);
2026
+ if (!authorized) {
2027
+ return reply.status(403).send({ error: "Unauthorized" });
2028
+ }
2029
+ return reply.type("text/html").send(getDashboardHtml(dashboardPath));
2030
+ });
2031
+ fastify.get(`${dashboardPath}/*`, async (request, reply) => {
2032
+ const authorized = await nodescope2.checkAuthorization(request);
2033
+ if (!authorized) {
2034
+ return reply.status(403).send({ error: "Unauthorized" });
2035
+ }
2036
+ return reply.type("text/html").send(getDashboardHtml(dashboardPath));
2037
+ });
2038
+ fastify.all(`${dashboardPath}/api/*`, async (request, reply) => {
2039
+ const authorized = await nodescope2.checkAuthorization(request);
2040
+ if (!authorized) {
2041
+ return reply.status(403).send({ error: "Unauthorized" });
2042
+ }
2043
+ const response = await nodescope2.api.handle({
2044
+ method: request.method,
2045
+ url: request.url,
2046
+ query: request.query,
2047
+ body: request.body
2048
+ });
2049
+ return reply.status(response.status).send(response.body);
2050
+ });
2051
+ }
2052
+ export {
2053
+ ApiHandler,
2054
+ BaseWatcher,
2055
+ CacheWatcher,
2056
+ EventWatcher,
2057
+ ExceptionWatcher,
2058
+ HttpClientWatcher,
2059
+ JobWatcher,
2060
+ LogWatcher,
2061
+ MemoryStorage,
2062
+ MySQLStorage,
2063
+ NodeScope,
2064
+ PostgreSQLStorage,
2065
+ QueryWatcher,
2066
+ RealTimeServer,
2067
+ RequestWatcher,
2068
+ SQLiteStorage,
2069
+ TrackedEventEmitter,
2070
+ attachWebSocket,
2071
+ createCacheWrapper,
2072
+ createExpressMiddleware,
2073
+ createHonoDashboardRoutes,
2074
+ createHonoMiddleware,
2075
+ createQueryInterceptor,
2076
+ createRequestContext,
2077
+ createStorageAdapter,
2078
+ fastifyNodeScope,
2079
+ getDashboardHtml,
2080
+ getDuration,
2081
+ getMemoryDelta,
2082
+ getNodeScope,
2083
+ initNodeScope,
2084
+ interceptConsole,
2085
+ interceptFetch,
2086
+ mountExpressRoutes,
2087
+ nodescope,
2088
+ setupGlobalErrorHandlers,
2089
+ wrapFetch,
2090
+ wrapJobProcessor,
2091
+ wrapPrisma
2092
+ };