@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/LICENSE +21 -0
- package/README.md +412 -0
- package/dist/chunk-6H665NNC.js +198 -0
- package/dist/chunk-6KBKW63X.js +145 -0
- package/dist/chunk-BOBKU5LG.js +163 -0
- package/dist/chunk-OF6NKXP5.js +44 -0
- package/dist/chunk-V5BR4MSS.js +195 -0
- package/dist/index.cjs +2926 -0
- package/dist/index.d.cts +928 -0
- package/dist/index.d.ts +928 -0
- package/dist/index.js +2092 -0
- package/dist/memory-5GF7O2HJ.js +7 -0
- package/dist/mysql-KNBA3N7P.js +7 -0
- package/dist/postgresql-XD7N5SFI.js +7 -0
- package/dist/sqlite-DMOIPBIO.js +7 -0
- package/package.json +108 -0
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, '<').replace(/>/g, '>');
|
|
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
|
+
};
|