@runtimescope/server-sdk 0.6.0 → 0.6.1
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.cjs +1635 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +186 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +34 -10
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1635 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
RuntimeScope: () => RuntimeScope,
|
|
34
|
+
Sampler: () => Sampler,
|
|
35
|
+
_log: () => _log,
|
|
36
|
+
generateId: () => generateId,
|
|
37
|
+
generateSessionId: () => generateSessionId,
|
|
38
|
+
getRequestContext: () => getRequestContext,
|
|
39
|
+
getSessionId: () => getSessionId,
|
|
40
|
+
normalizeQuery: () => normalizeQuery,
|
|
41
|
+
parseOperation: () => parseOperation,
|
|
42
|
+
parseTablesAccessed: () => parseTablesAccessed,
|
|
43
|
+
redactParams: () => redactParams,
|
|
44
|
+
runWithContext: () => runWithContext,
|
|
45
|
+
runtimeScopeMiddleware: () => runtimeScopeMiddleware
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/transport.ts
|
|
50
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
51
|
+
var ServerTransport = class {
|
|
52
|
+
ws = null;
|
|
53
|
+
url;
|
|
54
|
+
sessionId;
|
|
55
|
+
appName;
|
|
56
|
+
sdkVersion;
|
|
57
|
+
queue = [];
|
|
58
|
+
maxQueueSize;
|
|
59
|
+
_droppedCount = 0;
|
|
60
|
+
reconnectTimer = null;
|
|
61
|
+
connected = false;
|
|
62
|
+
flushTimer = null;
|
|
63
|
+
authToken;
|
|
64
|
+
authFailed = false;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.url = options.url;
|
|
67
|
+
this.sessionId = options.sessionId;
|
|
68
|
+
this.appName = options.appName;
|
|
69
|
+
this.sdkVersion = options.sdkVersion;
|
|
70
|
+
this.authToken = options.authToken;
|
|
71
|
+
this.maxQueueSize = options.maxQueueSize ?? 1e4;
|
|
72
|
+
}
|
|
73
|
+
get droppedCount() {
|
|
74
|
+
return this._droppedCount;
|
|
75
|
+
}
|
|
76
|
+
connect() {
|
|
77
|
+
if (this.authFailed) return;
|
|
78
|
+
try {
|
|
79
|
+
this.ws = new import_ws.default(this.url);
|
|
80
|
+
this.ws.on("open", () => {
|
|
81
|
+
this.connected = true;
|
|
82
|
+
this.sendHandshake();
|
|
83
|
+
this.flushQueue();
|
|
84
|
+
this.flushTimer = setInterval(() => this.flushQueue(), 100);
|
|
85
|
+
});
|
|
86
|
+
this.ws.on("message", (data) => {
|
|
87
|
+
try {
|
|
88
|
+
const msg = JSON.parse(data.toString());
|
|
89
|
+
if (msg.type === "error" && msg.payload?.code === "AUTH_FAILED") {
|
|
90
|
+
console.error("[RuntimeScope] Authentication failed \u2014 stopping reconnection");
|
|
91
|
+
this.authFailed = true;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
this.ws.on("close", () => {
|
|
97
|
+
this.connected = false;
|
|
98
|
+
this.cleanup();
|
|
99
|
+
if (!this.authFailed) this.scheduleReconnect();
|
|
100
|
+
});
|
|
101
|
+
this.ws.on("error", () => {
|
|
102
|
+
this.connected = false;
|
|
103
|
+
this.cleanup();
|
|
104
|
+
if (!this.authFailed) this.scheduleReconnect();
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
this.scheduleReconnect();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
cleanup() {
|
|
111
|
+
if (this.flushTimer) {
|
|
112
|
+
clearInterval(this.flushTimer);
|
|
113
|
+
this.flushTimer = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
scheduleReconnect() {
|
|
117
|
+
if (this.reconnectTimer) return;
|
|
118
|
+
this.reconnectTimer = setTimeout(() => {
|
|
119
|
+
this.reconnectTimer = null;
|
|
120
|
+
this.connect();
|
|
121
|
+
}, 3e3);
|
|
122
|
+
}
|
|
123
|
+
sendHandshake() {
|
|
124
|
+
this.send({
|
|
125
|
+
type: "handshake",
|
|
126
|
+
payload: {
|
|
127
|
+
appName: this.appName,
|
|
128
|
+
sdkVersion: this.sdkVersion,
|
|
129
|
+
sessionId: this.sessionId,
|
|
130
|
+
...this.authToken ? { authToken: this.authToken } : {}
|
|
131
|
+
},
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
sessionId: this.sessionId
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
sendEvent(event) {
|
|
137
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
138
|
+
this.queue.shift();
|
|
139
|
+
this._droppedCount++;
|
|
140
|
+
}
|
|
141
|
+
this.queue.push(event);
|
|
142
|
+
if (this.connected && this.queue.length >= 10) {
|
|
143
|
+
this.flushQueue();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
flushQueue() {
|
|
147
|
+
if (!this.connected || this.queue.length === 0) return;
|
|
148
|
+
const batch = this.queue.splice(0);
|
|
149
|
+
this.send({
|
|
150
|
+
type: "event",
|
|
151
|
+
payload: { events: batch },
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
sessionId: this.sessionId
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
send(msg) {
|
|
157
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
158
|
+
try {
|
|
159
|
+
this.ws.send(JSON.stringify(msg));
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
disconnect() {
|
|
165
|
+
if (this.reconnectTimer) {
|
|
166
|
+
clearTimeout(this.reconnectTimer);
|
|
167
|
+
this.reconnectTimer = null;
|
|
168
|
+
}
|
|
169
|
+
this.cleanup();
|
|
170
|
+
this.flushQueue();
|
|
171
|
+
if (this.ws) {
|
|
172
|
+
this.ws.close();
|
|
173
|
+
this.ws = null;
|
|
174
|
+
}
|
|
175
|
+
this.connected = false;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/utils/id.ts
|
|
180
|
+
var import_node_crypto = require("crypto");
|
|
181
|
+
function generateId() {
|
|
182
|
+
return (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
183
|
+
}
|
|
184
|
+
function generateSessionId() {
|
|
185
|
+
return `srv-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/utils/stack.ts
|
|
189
|
+
var INTERNAL_FRAMES = [
|
|
190
|
+
"RuntimeScope",
|
|
191
|
+
"server-sdk",
|
|
192
|
+
"captureQuery",
|
|
193
|
+
"instrumentPrisma",
|
|
194
|
+
"instrumentDrizzle",
|
|
195
|
+
"instrumentKnex",
|
|
196
|
+
"instrumentPg",
|
|
197
|
+
"instrumentMysql2",
|
|
198
|
+
"instrumentBetterSqlite3"
|
|
199
|
+
];
|
|
200
|
+
function captureStack(skipFrames = 3) {
|
|
201
|
+
const err = new Error();
|
|
202
|
+
const stack = err.stack ?? "";
|
|
203
|
+
const lines = stack.split("\n").slice(skipFrames);
|
|
204
|
+
const filtered = lines.filter(
|
|
205
|
+
(line) => !INTERNAL_FRAMES.some((frame) => line.includes(frame))
|
|
206
|
+
);
|
|
207
|
+
return filtered.slice(0, 10).join("\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/sampler.ts
|
|
211
|
+
var Sampler = class {
|
|
212
|
+
constructor(config) {
|
|
213
|
+
this.config = config;
|
|
214
|
+
}
|
|
215
|
+
windowCount = 0;
|
|
216
|
+
windowStart = Date.now();
|
|
217
|
+
_droppedCount = 0;
|
|
218
|
+
get droppedCount() {
|
|
219
|
+
return this._droppedCount;
|
|
220
|
+
}
|
|
221
|
+
shouldSample(_event) {
|
|
222
|
+
const rate = this.config.sampleRate;
|
|
223
|
+
if (rate !== void 0 && rate < 1) {
|
|
224
|
+
if (Math.random() > rate) {
|
|
225
|
+
this._droppedCount++;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const maxPerSec = this.config.maxEventsPerSecond;
|
|
230
|
+
if (maxPerSec !== void 0) {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
if (now - this.windowStart >= 1e3) {
|
|
233
|
+
this.windowCount = 0;
|
|
234
|
+
this.windowStart = now;
|
|
235
|
+
}
|
|
236
|
+
if (this.windowCount >= maxPerSec) {
|
|
237
|
+
this._droppedCount++;
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
this.windowCount++;
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
reset() {
|
|
245
|
+
this.windowCount = 0;
|
|
246
|
+
this.windowStart = Date.now();
|
|
247
|
+
this._droppedCount = 0;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/utils/sql-parser.ts
|
|
252
|
+
var OPERATION_RE = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|WITH)\b/i;
|
|
253
|
+
var TABLE_RE = /\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\s+["'`]?(\w+)["'`]?/gi;
|
|
254
|
+
var PARAM_RE = /\$\d+|\?|:\w+/g;
|
|
255
|
+
var STRING_LITERAL_RE = /'[^']*'/g;
|
|
256
|
+
var NUMBER_LITERAL_RE = /\b\d+\.?\d*\b/g;
|
|
257
|
+
function parseOperation(query) {
|
|
258
|
+
const match = query.match(OPERATION_RE);
|
|
259
|
+
if (!match) return "OTHER";
|
|
260
|
+
const op = match[1].toUpperCase();
|
|
261
|
+
if (op === "SELECT" || op === "WITH") return "SELECT";
|
|
262
|
+
if (op === "INSERT") return "INSERT";
|
|
263
|
+
if (op === "UPDATE") return "UPDATE";
|
|
264
|
+
if (op === "DELETE") return "DELETE";
|
|
265
|
+
return "OTHER";
|
|
266
|
+
}
|
|
267
|
+
function parseTablesAccessed(query) {
|
|
268
|
+
const tables = /* @__PURE__ */ new Set();
|
|
269
|
+
let match;
|
|
270
|
+
const re = new RegExp(TABLE_RE.source, TABLE_RE.flags);
|
|
271
|
+
while ((match = re.exec(query)) !== null) {
|
|
272
|
+
const table = match[1].toLowerCase();
|
|
273
|
+
if (!["select", "where", "and", "or", "not", "null", "set", "values"].includes(table)) {
|
|
274
|
+
tables.add(table);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return [...tables];
|
|
278
|
+
}
|
|
279
|
+
function normalizeQuery(query) {
|
|
280
|
+
return query.replace(STRING_LITERAL_RE, "?").replace(NUMBER_LITERAL_RE, "?").replace(PARAM_RE, "?").replace(/\s+/g, " ").trim();
|
|
281
|
+
}
|
|
282
|
+
function redactParams(params) {
|
|
283
|
+
return JSON.stringify(
|
|
284
|
+
params.map((p) => {
|
|
285
|
+
if (p === null) return "NULL";
|
|
286
|
+
if (typeof p === "string") return "<string>";
|
|
287
|
+
if (typeof p === "number") return "<number>";
|
|
288
|
+
if (typeof p === "boolean") return "<boolean>";
|
|
289
|
+
if (p instanceof Date) return "<date>";
|
|
290
|
+
if (Buffer.isBuffer(p)) return "<buffer>";
|
|
291
|
+
return "<object>";
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/utils/log.ts
|
|
297
|
+
var _log = {
|
|
298
|
+
log: console.log.bind(console),
|
|
299
|
+
warn: console.warn.bind(console),
|
|
300
|
+
error: console.error.bind(console),
|
|
301
|
+
debug: console.debug.bind(console)
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/integrations/prisma.ts
|
|
305
|
+
function instrumentPrisma(client, options) {
|
|
306
|
+
const prisma = client;
|
|
307
|
+
const source = "prisma";
|
|
308
|
+
if (typeof prisma.$extends === "function") {
|
|
309
|
+
try {
|
|
310
|
+
const extended = prisma.$extends({
|
|
311
|
+
query: {
|
|
312
|
+
$allOperations({ operation, model, args, query }) {
|
|
313
|
+
const start = performance.now();
|
|
314
|
+
return query(args).then((result) => {
|
|
315
|
+
const duration = performance.now() - start;
|
|
316
|
+
const queryStr = `prisma.${model}.${operation}(${JSON.stringify(args).slice(0, 200)})`;
|
|
317
|
+
options.onEvent({
|
|
318
|
+
eventId: generateId(),
|
|
319
|
+
sessionId: options.sessionId,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
eventType: "database",
|
|
322
|
+
query: queryStr,
|
|
323
|
+
normalizedQuery: `prisma.${model}.${operation}(?)`,
|
|
324
|
+
duration,
|
|
325
|
+
tablesAccessed: model ? [model.toLowerCase()] : [],
|
|
326
|
+
operation: mapPrismaOperation(operation),
|
|
327
|
+
source,
|
|
328
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0,
|
|
329
|
+
rowsReturned: Array.isArray(result) ? result.length : void 0
|
|
330
|
+
});
|
|
331
|
+
return result;
|
|
332
|
+
}).catch((err) => {
|
|
333
|
+
const duration = performance.now() - start;
|
|
334
|
+
const queryStr = `prisma.${model}.${operation}(${JSON.stringify(args).slice(0, 200)})`;
|
|
335
|
+
options.onEvent({
|
|
336
|
+
eventId: generateId(),
|
|
337
|
+
sessionId: options.sessionId,
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
eventType: "database",
|
|
340
|
+
query: queryStr,
|
|
341
|
+
normalizedQuery: `prisma.${model}.${operation}(?)`,
|
|
342
|
+
duration,
|
|
343
|
+
tablesAccessed: model ? [model.toLowerCase()] : [],
|
|
344
|
+
operation: mapPrismaOperation(operation),
|
|
345
|
+
source,
|
|
346
|
+
error: err.message,
|
|
347
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
348
|
+
});
|
|
349
|
+
throw err;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
Object.assign(prisma, extended);
|
|
355
|
+
return () => {
|
|
356
|
+
};
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (typeof prisma.$on === "function") {
|
|
361
|
+
prisma.$on("query", (e) => {
|
|
362
|
+
options.onEvent({
|
|
363
|
+
eventId: generateId(),
|
|
364
|
+
sessionId: options.sessionId,
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
eventType: "database",
|
|
367
|
+
query: e.query,
|
|
368
|
+
normalizedQuery: normalizeQuery(e.query),
|
|
369
|
+
duration: e.duration,
|
|
370
|
+
tablesAccessed: parseTablesAccessed(e.query),
|
|
371
|
+
operation: parseOperation(e.query),
|
|
372
|
+
source,
|
|
373
|
+
params: options.redact !== false ? redactParams(JSON.parse(e.params || "[]")) : e.params,
|
|
374
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
return () => {
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
_log.warn("[RuntimeScope] Prisma client does not support $on or $extends");
|
|
381
|
+
return () => {
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function mapPrismaOperation(op) {
|
|
385
|
+
const lower = op.toLowerCase();
|
|
386
|
+
if (lower.includes("find") || lower === "aggregate" || lower === "count" || lower === "groupby") return "SELECT";
|
|
387
|
+
if (lower.includes("create") || lower === "createMany") return "INSERT";
|
|
388
|
+
if (lower.includes("update") || lower === "upsert") return "UPDATE";
|
|
389
|
+
if (lower.includes("delete")) return "DELETE";
|
|
390
|
+
return "OTHER";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/integrations/pg.ts
|
|
394
|
+
function instrumentPg(pool, options) {
|
|
395
|
+
const client = pool;
|
|
396
|
+
const originalQuery = client.query;
|
|
397
|
+
if (typeof originalQuery !== "function") {
|
|
398
|
+
_log.warn("[RuntimeScope] pg client does not have a query method");
|
|
399
|
+
return () => {
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
client.query = function(...args) {
|
|
403
|
+
const start = performance.now();
|
|
404
|
+
let queryText = "";
|
|
405
|
+
let params;
|
|
406
|
+
if (typeof args[0] === "string") {
|
|
407
|
+
queryText = args[0];
|
|
408
|
+
if (Array.isArray(args[1])) params = args[1];
|
|
409
|
+
} else if (typeof args[0] === "object" && args[0] !== null) {
|
|
410
|
+
const config = args[0];
|
|
411
|
+
queryText = config.text ?? "";
|
|
412
|
+
params = config.values;
|
|
413
|
+
}
|
|
414
|
+
const result = originalQuery.apply(this, args);
|
|
415
|
+
if (result && typeof result.then === "function") {
|
|
416
|
+
return result.then((res) => {
|
|
417
|
+
const duration = performance.now() - start;
|
|
418
|
+
const pgResult = res;
|
|
419
|
+
options.onEvent({
|
|
420
|
+
eventId: generateId(),
|
|
421
|
+
sessionId: options.sessionId,
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
eventType: "database",
|
|
424
|
+
query: queryText,
|
|
425
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
426
|
+
duration,
|
|
427
|
+
rowsReturned: pgResult.rows?.length,
|
|
428
|
+
rowsAffected: pgResult.rowCount ?? void 0,
|
|
429
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
430
|
+
operation: parseOperation(queryText),
|
|
431
|
+
source: "pg",
|
|
432
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
433
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
434
|
+
});
|
|
435
|
+
return res;
|
|
436
|
+
}).catch((err) => {
|
|
437
|
+
const duration = performance.now() - start;
|
|
438
|
+
options.onEvent({
|
|
439
|
+
eventId: generateId(),
|
|
440
|
+
sessionId: options.sessionId,
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
eventType: "database",
|
|
443
|
+
query: queryText,
|
|
444
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
445
|
+
duration,
|
|
446
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
447
|
+
operation: parseOperation(queryText),
|
|
448
|
+
source: "pg",
|
|
449
|
+
error: err.message,
|
|
450
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
451
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
452
|
+
});
|
|
453
|
+
throw err;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
};
|
|
458
|
+
return () => {
|
|
459
|
+
client.query = originalQuery;
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/integrations/knex.ts
|
|
464
|
+
function instrumentKnex(knex, options) {
|
|
465
|
+
const instance = knex;
|
|
466
|
+
const pendingQueries = /* @__PURE__ */ new Map();
|
|
467
|
+
const onQuery = (data) => {
|
|
468
|
+
pendingQueries.set(data.__knexQueryUid, {
|
|
469
|
+
query: data.sql,
|
|
470
|
+
params: data.bindings ?? [],
|
|
471
|
+
start: performance.now()
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
const onQueryResponse = (_response, data, _builder) => {
|
|
475
|
+
const pending = pendingQueries.get(data.__knexQueryUid);
|
|
476
|
+
pendingQueries.delete(data.__knexQueryUid);
|
|
477
|
+
const duration = pending ? performance.now() - pending.start : 0;
|
|
478
|
+
const queryText = pending?.query ?? data.sql;
|
|
479
|
+
const params = pending?.params ?? data.bindings;
|
|
480
|
+
const rows = Array.isArray(_response) ? _response.length : void 0;
|
|
481
|
+
options.onEvent({
|
|
482
|
+
eventId: generateId(),
|
|
483
|
+
sessionId: options.sessionId,
|
|
484
|
+
timestamp: Date.now(),
|
|
485
|
+
eventType: "database",
|
|
486
|
+
query: queryText,
|
|
487
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
488
|
+
duration,
|
|
489
|
+
rowsReturned: rows,
|
|
490
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
491
|
+
operation: parseOperation(queryText),
|
|
492
|
+
source: "knex",
|
|
493
|
+
params: params.length > 0 && options.redact !== false ? redactParams(params) : void 0,
|
|
494
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
const onQueryError = (error, data) => {
|
|
498
|
+
const pending = pendingQueries.get(data.__knexQueryUid);
|
|
499
|
+
pendingQueries.delete(data.__knexQueryUid);
|
|
500
|
+
const duration = pending ? performance.now() - pending.start : 0;
|
|
501
|
+
const queryText = pending?.query ?? data.sql;
|
|
502
|
+
const params = pending?.params ?? data.bindings;
|
|
503
|
+
options.onEvent({
|
|
504
|
+
eventId: generateId(),
|
|
505
|
+
sessionId: options.sessionId,
|
|
506
|
+
timestamp: Date.now(),
|
|
507
|
+
eventType: "database",
|
|
508
|
+
query: queryText,
|
|
509
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
510
|
+
duration,
|
|
511
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
512
|
+
operation: parseOperation(queryText),
|
|
513
|
+
source: "knex",
|
|
514
|
+
error: error.message,
|
|
515
|
+
params: params.length > 0 && options.redact !== false ? redactParams(params) : void 0,
|
|
516
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
instance.on("query", onQuery);
|
|
520
|
+
instance.on("query-response", onQueryResponse);
|
|
521
|
+
instance.on("query-error", onQueryError);
|
|
522
|
+
return () => {
|
|
523
|
+
instance.removeListener("query", onQuery);
|
|
524
|
+
instance.removeListener("query-response", onQueryResponse);
|
|
525
|
+
instance.removeListener("query-error", onQueryError);
|
|
526
|
+
pendingQueries.clear();
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/integrations/drizzle.ts
|
|
531
|
+
function instrumentDrizzle(db, options) {
|
|
532
|
+
const drizzleDb = db;
|
|
533
|
+
const internals = drizzleDb._;
|
|
534
|
+
if (!internals) {
|
|
535
|
+
_log.warn("[RuntimeScope] Drizzle db does not have internals (_). Cannot instrument.");
|
|
536
|
+
return () => {
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const session = internals.session;
|
|
540
|
+
if (!session) {
|
|
541
|
+
_log.warn("[RuntimeScope] Drizzle db does not have a session. Cannot instrument.");
|
|
542
|
+
return () => {
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const originalExecute = session.execute;
|
|
546
|
+
if (typeof originalExecute === "function") {
|
|
547
|
+
session.execute = function(queryOrSql) {
|
|
548
|
+
const start = performance.now();
|
|
549
|
+
let queryText = "";
|
|
550
|
+
let params;
|
|
551
|
+
if (queryOrSql && typeof queryOrSql === "object") {
|
|
552
|
+
const q = queryOrSql;
|
|
553
|
+
queryText = q.sql ?? q.query ?? String(queryOrSql);
|
|
554
|
+
params = q.params;
|
|
555
|
+
} else if (typeof queryOrSql === "string") {
|
|
556
|
+
queryText = queryOrSql;
|
|
557
|
+
}
|
|
558
|
+
const stack = options.captureStackTraces ? captureStack() : void 0;
|
|
559
|
+
const result = originalExecute.apply(this, arguments);
|
|
560
|
+
if (result && typeof result.then === "function") {
|
|
561
|
+
return result.then((res) => {
|
|
562
|
+
const duration = performance.now() - start;
|
|
563
|
+
const rows = Array.isArray(res) ? res.length : void 0;
|
|
564
|
+
options.onEvent({
|
|
565
|
+
eventId: generateId(),
|
|
566
|
+
sessionId: options.sessionId,
|
|
567
|
+
timestamp: Date.now(),
|
|
568
|
+
eventType: "database",
|
|
569
|
+
query: queryText,
|
|
570
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
571
|
+
duration,
|
|
572
|
+
rowsReturned: rows,
|
|
573
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
574
|
+
operation: parseOperation(queryText),
|
|
575
|
+
source: "drizzle",
|
|
576
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
577
|
+
stackTrace: stack
|
|
578
|
+
});
|
|
579
|
+
return res;
|
|
580
|
+
}).catch((err) => {
|
|
581
|
+
const duration = performance.now() - start;
|
|
582
|
+
options.onEvent({
|
|
583
|
+
eventId: generateId(),
|
|
584
|
+
sessionId: options.sessionId,
|
|
585
|
+
timestamp: Date.now(),
|
|
586
|
+
eventType: "database",
|
|
587
|
+
query: queryText,
|
|
588
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
589
|
+
duration,
|
|
590
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
591
|
+
operation: parseOperation(queryText),
|
|
592
|
+
source: "drizzle",
|
|
593
|
+
error: err.message,
|
|
594
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
595
|
+
stackTrace: stack
|
|
596
|
+
});
|
|
597
|
+
throw err;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return result;
|
|
601
|
+
};
|
|
602
|
+
return () => {
|
|
603
|
+
session.execute = originalExecute;
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const client = session.client;
|
|
607
|
+
const originalQuery = client?.query;
|
|
608
|
+
if (client && typeof originalQuery === "function") {
|
|
609
|
+
client.query = function(...args) {
|
|
610
|
+
const start = performance.now();
|
|
611
|
+
const queryText = typeof args[0] === "string" ? args[0] : "";
|
|
612
|
+
const params = Array.isArray(args[1]) ? args[1] : void 0;
|
|
613
|
+
const stack = options.captureStackTraces ? captureStack() : void 0;
|
|
614
|
+
const result = originalQuery.apply(this, args);
|
|
615
|
+
if (result && typeof result.then === "function") {
|
|
616
|
+
return result.then((res) => {
|
|
617
|
+
const duration = performance.now() - start;
|
|
618
|
+
options.onEvent({
|
|
619
|
+
eventId: generateId(),
|
|
620
|
+
sessionId: options.sessionId,
|
|
621
|
+
timestamp: Date.now(),
|
|
622
|
+
eventType: "database",
|
|
623
|
+
query: queryText,
|
|
624
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
625
|
+
duration,
|
|
626
|
+
rowsReturned: Array.isArray(res) ? res.length : void 0,
|
|
627
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
628
|
+
operation: parseOperation(queryText),
|
|
629
|
+
source: "drizzle",
|
|
630
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
631
|
+
stackTrace: stack
|
|
632
|
+
});
|
|
633
|
+
return res;
|
|
634
|
+
}).catch((err) => {
|
|
635
|
+
const duration = performance.now() - start;
|
|
636
|
+
options.onEvent({
|
|
637
|
+
eventId: generateId(),
|
|
638
|
+
sessionId: options.sessionId,
|
|
639
|
+
timestamp: Date.now(),
|
|
640
|
+
eventType: "database",
|
|
641
|
+
query: queryText,
|
|
642
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
643
|
+
duration,
|
|
644
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
645
|
+
operation: parseOperation(queryText),
|
|
646
|
+
source: "drizzle",
|
|
647
|
+
error: err.message,
|
|
648
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
649
|
+
stackTrace: stack
|
|
650
|
+
});
|
|
651
|
+
throw err;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return result;
|
|
655
|
+
};
|
|
656
|
+
return () => {
|
|
657
|
+
client.query = originalQuery;
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
_log.warn("[RuntimeScope] Drizzle db has no instrumentable execute or query method.");
|
|
661
|
+
return () => {
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/integrations/mysql2.ts
|
|
666
|
+
function instrumentMysql2(pool, options) {
|
|
667
|
+
const client = pool;
|
|
668
|
+
const originalQuery = client.query;
|
|
669
|
+
const originalExecute = client.execute;
|
|
670
|
+
if (typeof originalQuery !== "function") {
|
|
671
|
+
_log.warn("[RuntimeScope] mysql2 client does not have a query method");
|
|
672
|
+
return () => {
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function wrapMethod(original, methodName) {
|
|
676
|
+
return function(...args) {
|
|
677
|
+
const start = performance.now();
|
|
678
|
+
let queryText = "";
|
|
679
|
+
let params;
|
|
680
|
+
if (typeof args[0] === "string") {
|
|
681
|
+
queryText = args[0];
|
|
682
|
+
if (Array.isArray(args[1])) params = args[1];
|
|
683
|
+
} else if (typeof args[0] === "object" && args[0] !== null) {
|
|
684
|
+
const config = args[0];
|
|
685
|
+
queryText = config.sql ?? "";
|
|
686
|
+
params = config.values;
|
|
687
|
+
if (!params && Array.isArray(args[1])) params = args[1];
|
|
688
|
+
}
|
|
689
|
+
const lastArg = args[args.length - 1];
|
|
690
|
+
const hasCallback = typeof lastArg === "function";
|
|
691
|
+
if (hasCallback) {
|
|
692
|
+
const cb = lastArg;
|
|
693
|
+
args[args.length - 1] = function(err, results, fields) {
|
|
694
|
+
const duration = performance.now() - start;
|
|
695
|
+
const rows = Array.isArray(results) ? results.length : void 0;
|
|
696
|
+
const affectedRows = results && typeof results === "object" ? results.affectedRows : void 0;
|
|
697
|
+
options.onEvent({
|
|
698
|
+
eventId: generateId(),
|
|
699
|
+
sessionId: options.sessionId,
|
|
700
|
+
timestamp: Date.now(),
|
|
701
|
+
eventType: "database",
|
|
702
|
+
query: queryText,
|
|
703
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
704
|
+
duration,
|
|
705
|
+
rowsReturned: rows,
|
|
706
|
+
rowsAffected: affectedRows,
|
|
707
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
708
|
+
operation: parseOperation(queryText),
|
|
709
|
+
source: "mysql2",
|
|
710
|
+
error: err?.message,
|
|
711
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
712
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
713
|
+
});
|
|
714
|
+
cb(err, results, fields);
|
|
715
|
+
};
|
|
716
|
+
return original.apply(this, args);
|
|
717
|
+
}
|
|
718
|
+
const result = original.apply(this, args);
|
|
719
|
+
if (result && typeof result.then === "function") {
|
|
720
|
+
return result.then((res) => {
|
|
721
|
+
const duration = performance.now() - start;
|
|
722
|
+
const resultArr = Array.isArray(res) ? res : [res];
|
|
723
|
+
const rows = Array.isArray(resultArr[0]) ? resultArr[0].length : void 0;
|
|
724
|
+
const affectedRows = resultArr[0] && typeof resultArr[0] === "object" ? resultArr[0].affectedRows : void 0;
|
|
725
|
+
options.onEvent({
|
|
726
|
+
eventId: generateId(),
|
|
727
|
+
sessionId: options.sessionId,
|
|
728
|
+
timestamp: Date.now(),
|
|
729
|
+
eventType: "database",
|
|
730
|
+
query: queryText,
|
|
731
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
732
|
+
duration,
|
|
733
|
+
rowsReturned: rows,
|
|
734
|
+
rowsAffected: affectedRows,
|
|
735
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
736
|
+
operation: parseOperation(queryText),
|
|
737
|
+
source: "mysql2",
|
|
738
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
739
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
740
|
+
});
|
|
741
|
+
return res;
|
|
742
|
+
}).catch((err) => {
|
|
743
|
+
const duration = performance.now() - start;
|
|
744
|
+
options.onEvent({
|
|
745
|
+
eventId: generateId(),
|
|
746
|
+
sessionId: options.sessionId,
|
|
747
|
+
timestamp: Date.now(),
|
|
748
|
+
eventType: "database",
|
|
749
|
+
query: queryText,
|
|
750
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
751
|
+
duration,
|
|
752
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
753
|
+
operation: parseOperation(queryText),
|
|
754
|
+
source: "mysql2",
|
|
755
|
+
error: err.message,
|
|
756
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
757
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
758
|
+
});
|
|
759
|
+
throw err;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
client.query = wrapMethod(originalQuery, "query");
|
|
766
|
+
if (typeof originalExecute === "function") {
|
|
767
|
+
client.execute = wrapMethod(originalExecute, "execute");
|
|
768
|
+
}
|
|
769
|
+
return () => {
|
|
770
|
+
client.query = originalQuery;
|
|
771
|
+
if (originalExecute) client.execute = originalExecute;
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/integrations/better-sqlite3.ts
|
|
776
|
+
function instrumentBetterSqlite3(db, options) {
|
|
777
|
+
const database = db;
|
|
778
|
+
const originalPrepare = database.prepare;
|
|
779
|
+
const originalExec = database.exec;
|
|
780
|
+
if (typeof originalPrepare !== "function") {
|
|
781
|
+
_log.warn("[RuntimeScope] better-sqlite3 db does not have a prepare method");
|
|
782
|
+
return () => {
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function emitQuery(queryText, start, result, error, params) {
|
|
786
|
+
const duration = performance.now() - start;
|
|
787
|
+
const rows = Array.isArray(result) ? result.length : void 0;
|
|
788
|
+
const changes = result && typeof result === "object" ? result.changes : void 0;
|
|
789
|
+
options.onEvent({
|
|
790
|
+
eventId: generateId(),
|
|
791
|
+
sessionId: options.sessionId,
|
|
792
|
+
timestamp: Date.now(),
|
|
793
|
+
eventType: "database",
|
|
794
|
+
query: queryText,
|
|
795
|
+
normalizedQuery: normalizeQuery(queryText),
|
|
796
|
+
duration,
|
|
797
|
+
rowsReturned: rows,
|
|
798
|
+
rowsAffected: changes,
|
|
799
|
+
tablesAccessed: parseTablesAccessed(queryText),
|
|
800
|
+
operation: parseOperation(queryText),
|
|
801
|
+
source: "better-sqlite3",
|
|
802
|
+
error,
|
|
803
|
+
params: params && options.redact !== false ? redactParams(params) : void 0,
|
|
804
|
+
stackTrace: options.captureStackTraces ? captureStack() : void 0
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
database.prepare = function(sql) {
|
|
808
|
+
const stmt = originalPrepare.call(this, sql);
|
|
809
|
+
const stmtObj = stmt;
|
|
810
|
+
const methods = ["run", "get", "all"];
|
|
811
|
+
for (const method of methods) {
|
|
812
|
+
const original = stmtObj[method];
|
|
813
|
+
if (typeof original !== "function") continue;
|
|
814
|
+
stmtObj[method] = function(...args) {
|
|
815
|
+
const start = performance.now();
|
|
816
|
+
try {
|
|
817
|
+
const result = original.apply(this, args);
|
|
818
|
+
emitQuery(sql, start, result, void 0, args);
|
|
819
|
+
return result;
|
|
820
|
+
} catch (err) {
|
|
821
|
+
emitQuery(sql, start, void 0, err.message, args);
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const originalIterate = stmtObj.iterate;
|
|
827
|
+
if (typeof originalIterate === "function") {
|
|
828
|
+
stmtObj.iterate = function(...args) {
|
|
829
|
+
const start = performance.now();
|
|
830
|
+
const iter = originalIterate.apply(this, args);
|
|
831
|
+
let rowCount = 0;
|
|
832
|
+
const originalNext = iter.next.bind(iter);
|
|
833
|
+
let emitted = false;
|
|
834
|
+
iter.next = function() {
|
|
835
|
+
const result = originalNext();
|
|
836
|
+
if (!result.done) rowCount++;
|
|
837
|
+
if (result.done && !emitted) {
|
|
838
|
+
emitted = true;
|
|
839
|
+
emitQuery(sql, start, { length: rowCount }, void 0, args);
|
|
840
|
+
}
|
|
841
|
+
return result;
|
|
842
|
+
};
|
|
843
|
+
return iter;
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
return stmt;
|
|
847
|
+
};
|
|
848
|
+
if (typeof originalExec === "function") {
|
|
849
|
+
database.exec = function(sql) {
|
|
850
|
+
const start = performance.now();
|
|
851
|
+
try {
|
|
852
|
+
const result = originalExec.call(this, sql);
|
|
853
|
+
emitQuery(sql, start);
|
|
854
|
+
return result;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
emitQuery(sql, start, void 0, err.message);
|
|
857
|
+
throw err;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
return () => {
|
|
862
|
+
database.prepare = originalPrepare;
|
|
863
|
+
if (originalExec) database.exec = originalExec;
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/utils/serialize.ts
|
|
868
|
+
function safeSerialize(value, maxDepth = 5) {
|
|
869
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
870
|
+
function walk(val, depth) {
|
|
871
|
+
if (depth > maxDepth) return "[max depth]";
|
|
872
|
+
if (val === null || val === void 0) return val;
|
|
873
|
+
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
874
|
+
if (typeof val === "symbol") return val.toString();
|
|
875
|
+
if (typeof val === "bigint") return val.toString();
|
|
876
|
+
if (typeof val !== "object") return val;
|
|
877
|
+
if (val instanceof Error) {
|
|
878
|
+
return { name: val.name, message: val.message, stack: val.stack };
|
|
879
|
+
}
|
|
880
|
+
if (val instanceof Date) {
|
|
881
|
+
return val.toISOString();
|
|
882
|
+
}
|
|
883
|
+
if (val instanceof RegExp) {
|
|
884
|
+
return val.toString();
|
|
885
|
+
}
|
|
886
|
+
if (seen.has(val)) return "[Circular]";
|
|
887
|
+
seen.add(val);
|
|
888
|
+
if (Array.isArray(val)) {
|
|
889
|
+
return val.map((v) => walk(v, depth + 1));
|
|
890
|
+
}
|
|
891
|
+
const result = {};
|
|
892
|
+
for (const key of Object.keys(val)) {
|
|
893
|
+
result[key] = walk(val[key], depth + 1);
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
return walk(value, 0);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/context.ts
|
|
901
|
+
var import_node_async_hooks = require("async_hooks");
|
|
902
|
+
var storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
903
|
+
function runWithContext(ctx, fn) {
|
|
904
|
+
return storage.run(ctx, fn);
|
|
905
|
+
}
|
|
906
|
+
function getRequestContext() {
|
|
907
|
+
return storage.getStore();
|
|
908
|
+
}
|
|
909
|
+
function getSessionId(fallback) {
|
|
910
|
+
return storage.getStore()?.sessionId ?? fallback;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/interceptors/console.ts
|
|
914
|
+
var LEVELS = ["log", "warn", "error", "info", "debug", "trace"];
|
|
915
|
+
function interceptConsole(emit, sessionId, options) {
|
|
916
|
+
const levels = options?.levels ?? LEVELS;
|
|
917
|
+
const captureStack2 = options?.captureStackTraces ?? true;
|
|
918
|
+
const originals = {};
|
|
919
|
+
for (const level of levels) {
|
|
920
|
+
originals[level] = console[level].bind(console);
|
|
921
|
+
console[level] = (...args) => {
|
|
922
|
+
const message = args.map((a) => typeof a === "string" ? a : stringifyArg(a)).join(" ");
|
|
923
|
+
const event = {
|
|
924
|
+
eventId: generateId(),
|
|
925
|
+
sessionId: getSessionId(sessionId),
|
|
926
|
+
timestamp: Date.now(),
|
|
927
|
+
eventType: "console",
|
|
928
|
+
level,
|
|
929
|
+
message,
|
|
930
|
+
args: args.map((a) => safeSerialize(a, 3)),
|
|
931
|
+
stackTrace: captureStack2 && (level === "error" || level === "trace") ? new Error().stack?.split("\n").slice(2).join("\n") : void 0,
|
|
932
|
+
sourceFile: void 0
|
|
933
|
+
};
|
|
934
|
+
if (options?.beforeSend) {
|
|
935
|
+
const filtered = options.beforeSend(event);
|
|
936
|
+
if (filtered) emit(filtered);
|
|
937
|
+
} else {
|
|
938
|
+
emit(event);
|
|
939
|
+
}
|
|
940
|
+
originals[level](...args);
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
return () => {
|
|
944
|
+
for (const level of levels) {
|
|
945
|
+
console[level] = originals[level];
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function stringifyArg(arg) {
|
|
950
|
+
try {
|
|
951
|
+
return JSON.stringify(arg);
|
|
952
|
+
} catch {
|
|
953
|
+
return String(arg);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/interceptors/errors.ts
|
|
958
|
+
function interceptErrors(emit, sessionId, options) {
|
|
959
|
+
const onUncaughtException = (error) => {
|
|
960
|
+
const event = {
|
|
961
|
+
eventId: generateId(),
|
|
962
|
+
sessionId: getSessionId(sessionId),
|
|
963
|
+
timestamp: Date.now(),
|
|
964
|
+
eventType: "console",
|
|
965
|
+
level: "error",
|
|
966
|
+
message: `[Uncaught] ${error.message}`,
|
|
967
|
+
args: [safeSerialize(error, 3)],
|
|
968
|
+
stackTrace: error.stack,
|
|
969
|
+
sourceFile: extractSourceFile(error.stack)
|
|
970
|
+
};
|
|
971
|
+
if (options?.beforeSend) {
|
|
972
|
+
const filtered = options.beforeSend(event);
|
|
973
|
+
if (filtered) emit(filtered);
|
|
974
|
+
} else {
|
|
975
|
+
emit(event);
|
|
976
|
+
}
|
|
977
|
+
process.exitCode = 1;
|
|
978
|
+
};
|
|
979
|
+
const onUnhandledRejection = (reason) => {
|
|
980
|
+
let message;
|
|
981
|
+
let stackTrace;
|
|
982
|
+
if (reason instanceof Error) {
|
|
983
|
+
message = reason.message;
|
|
984
|
+
stackTrace = reason.stack;
|
|
985
|
+
} else if (typeof reason === "string") {
|
|
986
|
+
message = reason;
|
|
987
|
+
} else {
|
|
988
|
+
try {
|
|
989
|
+
message = JSON.stringify(reason);
|
|
990
|
+
} catch {
|
|
991
|
+
message = String(reason);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const event = {
|
|
995
|
+
eventId: generateId(),
|
|
996
|
+
sessionId: getSessionId(sessionId),
|
|
997
|
+
timestamp: Date.now(),
|
|
998
|
+
eventType: "console",
|
|
999
|
+
level: "error",
|
|
1000
|
+
message: `[Unhandled Rejection] ${message}`,
|
|
1001
|
+
args: [safeSerialize(reason, 3)],
|
|
1002
|
+
stackTrace,
|
|
1003
|
+
sourceFile: void 0
|
|
1004
|
+
};
|
|
1005
|
+
if (options?.beforeSend) {
|
|
1006
|
+
const filtered = options.beforeSend(event);
|
|
1007
|
+
if (filtered) emit(filtered);
|
|
1008
|
+
} else {
|
|
1009
|
+
emit(event);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
process.on("uncaughtException", onUncaughtException);
|
|
1013
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
1014
|
+
return () => {
|
|
1015
|
+
process.removeListener("uncaughtException", onUncaughtException);
|
|
1016
|
+
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function extractSourceFile(stack) {
|
|
1020
|
+
if (!stack) return void 0;
|
|
1021
|
+
const lines = stack.split("\n");
|
|
1022
|
+
for (const line of lines.slice(1)) {
|
|
1023
|
+
const match = line.match(/at\s+.*?\((.+?):(\d+):(\d+)\)/);
|
|
1024
|
+
if (match && !match[1].includes("node_modules") && !match[1].includes("node:")) {
|
|
1025
|
+
return `${match[1]}:${match[2]}:${match[3]}`;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/interceptors/http.ts
|
|
1032
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
1033
|
+
var import_node_https = __toESM(require("https"), 1);
|
|
1034
|
+
var DEFAULT_REDACT_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key"];
|
|
1035
|
+
function interceptHttp(emit, sessionId, options) {
|
|
1036
|
+
const maxBodySize = options?.maxBodySize ?? 65536;
|
|
1037
|
+
const redactSet = new Set(
|
|
1038
|
+
(options?.redactHeaders ?? DEFAULT_REDACT_HEADERS).map((h) => h.toLowerCase())
|
|
1039
|
+
);
|
|
1040
|
+
const ignorePatterns = options?.ignoreUrls ?? [];
|
|
1041
|
+
const originalHttpRequest = import_node_http.default.request;
|
|
1042
|
+
const originalHttpsRequest = import_node_https.default.request;
|
|
1043
|
+
function shouldIgnore(url) {
|
|
1044
|
+
for (const pattern of ignorePatterns) {
|
|
1045
|
+
if (typeof pattern === "string") {
|
|
1046
|
+
if (url.includes(pattern)) return true;
|
|
1047
|
+
} else if (pattern.test(url)) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
function redactHeaders(headers) {
|
|
1054
|
+
const result = {};
|
|
1055
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
1056
|
+
if (val === void 0) continue;
|
|
1057
|
+
const lk = key.toLowerCase();
|
|
1058
|
+
result[lk] = redactSet.has(lk) ? "[REDACTED]" : String(val);
|
|
1059
|
+
}
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
function buildUrl(input, protocol) {
|
|
1063
|
+
if (typeof input === "string") return input;
|
|
1064
|
+
if (input instanceof URL) return input.toString();
|
|
1065
|
+
const host = input.hostname ?? input.host ?? "localhost";
|
|
1066
|
+
const port = input.port ? `:${input.port}` : "";
|
|
1067
|
+
const path = input.path ?? "/";
|
|
1068
|
+
return `${protocol}://${host}${port}${path}`;
|
|
1069
|
+
}
|
|
1070
|
+
function wrapRequest(original, protocol) {
|
|
1071
|
+
return function wrappedRequest(...args) {
|
|
1072
|
+
const url = buildUrl(args[0], protocol);
|
|
1073
|
+
if (shouldIgnore(url)) {
|
|
1074
|
+
return original.apply(this, args);
|
|
1075
|
+
}
|
|
1076
|
+
const startTime = performance.now();
|
|
1077
|
+
let method = "GET";
|
|
1078
|
+
if (typeof args[0] !== "string" && !(args[0] instanceof URL)) {
|
|
1079
|
+
method = args[0].method?.toUpperCase() ?? "GET";
|
|
1080
|
+
} else if (args[1] && typeof args[1] === "object" && !("on" in args[1])) {
|
|
1081
|
+
method = args[1].method?.toUpperCase() ?? "GET";
|
|
1082
|
+
}
|
|
1083
|
+
let reqHeaders = {};
|
|
1084
|
+
if (typeof args[0] !== "string" && !(args[0] instanceof URL)) {
|
|
1085
|
+
reqHeaders = args[0].headers ?? {};
|
|
1086
|
+
} else if (args[1] && typeof args[1] === "object" && !("on" in args[1])) {
|
|
1087
|
+
reqHeaders = args[1].headers ?? {};
|
|
1088
|
+
}
|
|
1089
|
+
const req = original.apply(this, args);
|
|
1090
|
+
let requestBodySize = 0;
|
|
1091
|
+
let requestBody;
|
|
1092
|
+
const requestChunks = [];
|
|
1093
|
+
const origWrite = req.write;
|
|
1094
|
+
req.write = function(chunk, ...rest) {
|
|
1095
|
+
if (chunk) {
|
|
1096
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
1097
|
+
requestBodySize += buf.length;
|
|
1098
|
+
if (options?.captureBody) requestChunks.push(buf);
|
|
1099
|
+
}
|
|
1100
|
+
return origWrite.apply(req, [chunk, ...rest]);
|
|
1101
|
+
};
|
|
1102
|
+
const origEnd = req.end;
|
|
1103
|
+
req.end = function(chunk, ...rest) {
|
|
1104
|
+
if (chunk && typeof chunk !== "function") {
|
|
1105
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
1106
|
+
requestBodySize += buf.length;
|
|
1107
|
+
if (options?.captureBody) requestChunks.push(buf);
|
|
1108
|
+
}
|
|
1109
|
+
return origEnd.apply(req, [chunk, ...rest]);
|
|
1110
|
+
};
|
|
1111
|
+
req.on("response", (res) => {
|
|
1112
|
+
const responseChunks = [];
|
|
1113
|
+
let responseBodySize = 0;
|
|
1114
|
+
if (options?.captureBody) {
|
|
1115
|
+
const origPush = res.push;
|
|
1116
|
+
res.push = function(chunk, ...rest) {
|
|
1117
|
+
if (chunk) {
|
|
1118
|
+
responseBodySize += chunk.length;
|
|
1119
|
+
if (responseBodySize <= maxBodySize) responseChunks.push(chunk);
|
|
1120
|
+
}
|
|
1121
|
+
return origPush.apply(res, [chunk, ...rest]);
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
res.on("end", () => {
|
|
1125
|
+
const duration = performance.now() - startTime;
|
|
1126
|
+
if (options?.captureBody) {
|
|
1127
|
+
requestBody = joinChunks(requestChunks, maxBodySize);
|
|
1128
|
+
}
|
|
1129
|
+
const contentLength = parseInt(
|
|
1130
|
+
res.headers["content-length"] ?? "0",
|
|
1131
|
+
10
|
|
1132
|
+
);
|
|
1133
|
+
const event = {
|
|
1134
|
+
eventId: generateId(),
|
|
1135
|
+
sessionId: getSessionId(sessionId),
|
|
1136
|
+
timestamp: Date.now(),
|
|
1137
|
+
eventType: "network",
|
|
1138
|
+
url,
|
|
1139
|
+
method,
|
|
1140
|
+
status: res.statusCode ?? 0,
|
|
1141
|
+
requestHeaders: redactHeaders(reqHeaders),
|
|
1142
|
+
responseHeaders: redactHeaders(
|
|
1143
|
+
res.headers
|
|
1144
|
+
),
|
|
1145
|
+
requestBodySize,
|
|
1146
|
+
responseBodySize: contentLength || responseBodySize,
|
|
1147
|
+
duration: Math.round(duration * 100) / 100,
|
|
1148
|
+
ttfb: Math.round(duration * 100) / 100,
|
|
1149
|
+
requestBody,
|
|
1150
|
+
responseBody: options?.captureBody ? joinChunks(responseChunks, maxBodySize) : void 0,
|
|
1151
|
+
source: protocol === "https" ? "node-https" : "node-http"
|
|
1152
|
+
};
|
|
1153
|
+
if (options?.beforeSend) {
|
|
1154
|
+
const filtered = options.beforeSend(event);
|
|
1155
|
+
if (filtered) emit(filtered);
|
|
1156
|
+
} else {
|
|
1157
|
+
emit(event);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
req.on("error", (err) => {
|
|
1162
|
+
const duration = performance.now() - startTime;
|
|
1163
|
+
const event = {
|
|
1164
|
+
eventId: generateId(),
|
|
1165
|
+
sessionId: getSessionId(sessionId),
|
|
1166
|
+
timestamp: Date.now(),
|
|
1167
|
+
eventType: "network",
|
|
1168
|
+
url,
|
|
1169
|
+
method,
|
|
1170
|
+
status: 0,
|
|
1171
|
+
requestHeaders: redactHeaders(reqHeaders),
|
|
1172
|
+
responseHeaders: {},
|
|
1173
|
+
requestBodySize,
|
|
1174
|
+
responseBodySize: 0,
|
|
1175
|
+
duration: Math.round(duration * 100) / 100,
|
|
1176
|
+
ttfb: 0,
|
|
1177
|
+
errorPhase: "error",
|
|
1178
|
+
errorMessage: err.message,
|
|
1179
|
+
source: protocol === "https" ? "node-https" : "node-http"
|
|
1180
|
+
};
|
|
1181
|
+
if (options?.beforeSend) {
|
|
1182
|
+
const filtered = options.beforeSend(event);
|
|
1183
|
+
if (filtered) emit(filtered);
|
|
1184
|
+
} else {
|
|
1185
|
+
emit(event);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
return req;
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
import_node_http.default.request = wrapRequest(originalHttpRequest, "http");
|
|
1192
|
+
import_node_https.default.request = wrapRequest(originalHttpsRequest, "https");
|
|
1193
|
+
return () => {
|
|
1194
|
+
import_node_http.default.request = originalHttpRequest;
|
|
1195
|
+
import_node_https.default.request = originalHttpsRequest;
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function joinChunks(chunks, maxSize) {
|
|
1199
|
+
if (chunks.length === 0) return void 0;
|
|
1200
|
+
const combined = Buffer.concat(chunks);
|
|
1201
|
+
if (combined.length === 0) return void 0;
|
|
1202
|
+
return combined.toString("utf8", 0, Math.min(combined.length, maxSize));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/interceptors/perf-metrics.ts
|
|
1206
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
1207
|
+
var import_node_perf_hooks2 = require("perf_hooks");
|
|
1208
|
+
var ALL_METRICS = [
|
|
1209
|
+
"memory.rss",
|
|
1210
|
+
"memory.heapUsed",
|
|
1211
|
+
"memory.heapTotal",
|
|
1212
|
+
"memory.external",
|
|
1213
|
+
"eventloop.lag.mean",
|
|
1214
|
+
"eventloop.lag.p99",
|
|
1215
|
+
"eventloop.lag.max",
|
|
1216
|
+
"gc.pause.major",
|
|
1217
|
+
"gc.pause.minor",
|
|
1218
|
+
"cpu.user",
|
|
1219
|
+
"cpu.system",
|
|
1220
|
+
"handles.active",
|
|
1221
|
+
"requests.active"
|
|
1222
|
+
];
|
|
1223
|
+
function shouldCollect(metric, enabled) {
|
|
1224
|
+
return enabled.includes(metric);
|
|
1225
|
+
}
|
|
1226
|
+
function emitMetric(emit, sessionId, metricName, value, unit) {
|
|
1227
|
+
emit({
|
|
1228
|
+
eventId: generateId(),
|
|
1229
|
+
sessionId: getSessionId(sessionId),
|
|
1230
|
+
timestamp: Date.now(),
|
|
1231
|
+
eventType: "performance",
|
|
1232
|
+
metricName,
|
|
1233
|
+
value: Math.round(value * 100) / 100,
|
|
1234
|
+
unit
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
function startPerfMetrics(emit, sessionId, options) {
|
|
1238
|
+
const intervalMs = options?.intervalMs ?? 5e3;
|
|
1239
|
+
const enabled = options?.metrics ?? ALL_METRICS;
|
|
1240
|
+
let lastCpuUsage = process.cpuUsage();
|
|
1241
|
+
let lastCpuTime = Date.now();
|
|
1242
|
+
let histogram = null;
|
|
1243
|
+
const needsEventLoop = enabled.some((m) => m.startsWith("eventloop."));
|
|
1244
|
+
if (needsEventLoop) {
|
|
1245
|
+
histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
|
|
1246
|
+
histogram.enable();
|
|
1247
|
+
}
|
|
1248
|
+
let gcObserver = null;
|
|
1249
|
+
let lastMajorGcMs = 0;
|
|
1250
|
+
let lastMinorGcMs = 0;
|
|
1251
|
+
const needsGc = enabled.some((m) => m.startsWith("gc."));
|
|
1252
|
+
if (needsGc) {
|
|
1253
|
+
gcObserver = new import_node_perf_hooks2.PerformanceObserver((list) => {
|
|
1254
|
+
for (const entry of list.getEntries()) {
|
|
1255
|
+
const gcEntry = entry;
|
|
1256
|
+
const durationMs = entry.duration;
|
|
1257
|
+
const kind = gcEntry.detail?.kind ?? 0;
|
|
1258
|
+
if (kind === 2) {
|
|
1259
|
+
lastMajorGcMs += durationMs;
|
|
1260
|
+
} else if (kind === 1 || kind === 4 || kind === 8) {
|
|
1261
|
+
lastMinorGcMs += durationMs;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
gcObserver.observe({ entryTypes: ["gc"] });
|
|
1266
|
+
}
|
|
1267
|
+
const timer = setInterval(() => {
|
|
1268
|
+
if (enabled.some((m) => m.startsWith("memory."))) {
|
|
1269
|
+
const mem = process.memoryUsage();
|
|
1270
|
+
if (shouldCollect("memory.rss", enabled)) emitMetric(emit, sessionId, "memory.rss", mem.rss, "bytes");
|
|
1271
|
+
if (shouldCollect("memory.heapUsed", enabled)) emitMetric(emit, sessionId, "memory.heapUsed", mem.heapUsed, "bytes");
|
|
1272
|
+
if (shouldCollect("memory.heapTotal", enabled)) emitMetric(emit, sessionId, "memory.heapTotal", mem.heapTotal, "bytes");
|
|
1273
|
+
if (shouldCollect("memory.external", enabled)) emitMetric(emit, sessionId, "memory.external", mem.external, "bytes");
|
|
1274
|
+
}
|
|
1275
|
+
if (histogram) {
|
|
1276
|
+
if (shouldCollect("eventloop.lag.mean", enabled)) emitMetric(emit, sessionId, "eventloop.lag.mean", histogram.mean / 1e6, "ms");
|
|
1277
|
+
if (shouldCollect("eventloop.lag.p99", enabled)) emitMetric(emit, sessionId, "eventloop.lag.p99", histogram.percentile(99) / 1e6, "ms");
|
|
1278
|
+
if (shouldCollect("eventloop.lag.max", enabled)) emitMetric(emit, sessionId, "eventloop.lag.max", histogram.max / 1e6, "ms");
|
|
1279
|
+
histogram.reset();
|
|
1280
|
+
}
|
|
1281
|
+
if (needsGc) {
|
|
1282
|
+
if (shouldCollect("gc.pause.major", enabled)) emitMetric(emit, sessionId, "gc.pause.major", lastMajorGcMs, "ms");
|
|
1283
|
+
if (shouldCollect("gc.pause.minor", enabled)) emitMetric(emit, sessionId, "gc.pause.minor", lastMinorGcMs, "ms");
|
|
1284
|
+
lastMajorGcMs = 0;
|
|
1285
|
+
lastMinorGcMs = 0;
|
|
1286
|
+
}
|
|
1287
|
+
if (enabled.some((m) => m.startsWith("cpu."))) {
|
|
1288
|
+
const now = Date.now();
|
|
1289
|
+
const elapsed = (now - lastCpuTime) * 1e3;
|
|
1290
|
+
const cpu = process.cpuUsage(lastCpuUsage);
|
|
1291
|
+
lastCpuUsage = process.cpuUsage();
|
|
1292
|
+
lastCpuTime = now;
|
|
1293
|
+
if (elapsed > 0) {
|
|
1294
|
+
if (shouldCollect("cpu.user", enabled)) emitMetric(emit, sessionId, "cpu.user", cpu.user / elapsed * 100, "percent");
|
|
1295
|
+
if (shouldCollect("cpu.system", enabled)) emitMetric(emit, sessionId, "cpu.system", cpu.system / elapsed * 100, "percent");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (shouldCollect("handles.active", enabled)) {
|
|
1299
|
+
const handles = process._getActiveHandles?.()?.length ?? 0;
|
|
1300
|
+
emitMetric(emit, sessionId, "handles.active", handles, "count");
|
|
1301
|
+
}
|
|
1302
|
+
if (shouldCollect("requests.active", enabled)) {
|
|
1303
|
+
const requests = process._getActiveRequests?.()?.length ?? 0;
|
|
1304
|
+
emitMetric(emit, sessionId, "requests.active", requests, "count");
|
|
1305
|
+
}
|
|
1306
|
+
}, intervalMs);
|
|
1307
|
+
timer.unref();
|
|
1308
|
+
return () => {
|
|
1309
|
+
clearInterval(timer);
|
|
1310
|
+
if (histogram) {
|
|
1311
|
+
histogram.disable();
|
|
1312
|
+
}
|
|
1313
|
+
if (gcObserver) {
|
|
1314
|
+
gcObserver.disconnect();
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/interceptors/middleware.ts
|
|
1320
|
+
var DEFAULT_REDACT_HEADERS2 = ["authorization", "cookie", "set-cookie", "x-api-key"];
|
|
1321
|
+
function runtimeScopeMiddleware(emit, sessionId, options) {
|
|
1322
|
+
const maxBodySize = options?.maxBodySize ?? 65536;
|
|
1323
|
+
const redactSet = new Set(
|
|
1324
|
+
(options?.redactHeaders ?? DEFAULT_REDACT_HEADERS2).map((h) => h.toLowerCase())
|
|
1325
|
+
);
|
|
1326
|
+
const ignorePatterns = options?.ignoreRoutes ?? [];
|
|
1327
|
+
function shouldIgnore(path) {
|
|
1328
|
+
for (const pattern of ignorePatterns) {
|
|
1329
|
+
if (typeof pattern === "string") {
|
|
1330
|
+
if (path === pattern || path.startsWith(pattern)) return true;
|
|
1331
|
+
} else if (pattern.test(path)) {
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
function redactHeaders(headers) {
|
|
1338
|
+
const result = {};
|
|
1339
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
1340
|
+
if (val === void 0) continue;
|
|
1341
|
+
const lk = key.toLowerCase();
|
|
1342
|
+
result[lk] = redactSet.has(lk) ? "[REDACTED]" : String(val);
|
|
1343
|
+
}
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
return (req, res, next) => {
|
|
1347
|
+
const path = req.originalUrl ?? req.url ?? "/";
|
|
1348
|
+
if (shouldIgnore(path)) {
|
|
1349
|
+
return next();
|
|
1350
|
+
}
|
|
1351
|
+
const startTime = performance.now();
|
|
1352
|
+
const originalEnd = res.end.bind(res);
|
|
1353
|
+
let responseBody;
|
|
1354
|
+
res.end = function(chunk, ...rest) {
|
|
1355
|
+
const duration = performance.now() - startTime;
|
|
1356
|
+
if (options?.captureBody && chunk) {
|
|
1357
|
+
if (Buffer.isBuffer(chunk)) {
|
|
1358
|
+
responseBody = chunk.toString("utf8", 0, Math.min(chunk.length, maxBodySize));
|
|
1359
|
+
} else if (typeof chunk === "string") {
|
|
1360
|
+
responseBody = chunk.slice(0, maxBodySize);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const proto = req.protocol ?? (req.socket?.encrypted ? "https" : "http");
|
|
1364
|
+
const host = req.get?.("host") ?? req.headers?.host ?? "localhost";
|
|
1365
|
+
const url = `${proto}://${host}${path}`;
|
|
1366
|
+
const event = {
|
|
1367
|
+
eventId: generateId(),
|
|
1368
|
+
sessionId,
|
|
1369
|
+
timestamp: Date.now(),
|
|
1370
|
+
eventType: "network",
|
|
1371
|
+
url,
|
|
1372
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
1373
|
+
status: res.statusCode ?? 200,
|
|
1374
|
+
requestHeaders: redactHeaders(req.headers ?? {}),
|
|
1375
|
+
responseHeaders: redactHeaders(
|
|
1376
|
+
typeof res.getHeaders === "function" ? res.getHeaders() : {}
|
|
1377
|
+
),
|
|
1378
|
+
requestBodySize: parseInt(req.headers?.["content-length"] ?? "0", 10),
|
|
1379
|
+
responseBodySize: parseInt(
|
|
1380
|
+
(typeof res.getHeader === "function" ? res.getHeader("content-length") : void 0)?.toString() ?? "0",
|
|
1381
|
+
10
|
|
1382
|
+
),
|
|
1383
|
+
duration: Math.round(duration * 100) / 100,
|
|
1384
|
+
ttfb: Math.round(duration * 100) / 100,
|
|
1385
|
+
responseBody,
|
|
1386
|
+
source: "node-http"
|
|
1387
|
+
};
|
|
1388
|
+
if (options?.beforeSend) {
|
|
1389
|
+
const filtered = options.beforeSend(event);
|
|
1390
|
+
if (filtered) emit(filtered);
|
|
1391
|
+
} else {
|
|
1392
|
+
emit(event);
|
|
1393
|
+
}
|
|
1394
|
+
return originalEnd(chunk, ...rest);
|
|
1395
|
+
};
|
|
1396
|
+
const requestId = generateId();
|
|
1397
|
+
runWithContext({ sessionId, requestId }, () => next());
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/index.ts
|
|
1402
|
+
var SDK_VERSION = "0.6.1";
|
|
1403
|
+
var RuntimeScopeServer = class {
|
|
1404
|
+
transport = null;
|
|
1405
|
+
sessionId = "";
|
|
1406
|
+
config = {};
|
|
1407
|
+
sampler = null;
|
|
1408
|
+
restoreFunctions = [];
|
|
1409
|
+
connect(config = {}) {
|
|
1410
|
+
this.config = config;
|
|
1411
|
+
this.sessionId = config.sessionId ?? generateSessionId();
|
|
1412
|
+
const serverUrl = config.serverUrl ?? "ws://127.0.0.1:9090";
|
|
1413
|
+
this.transport = new ServerTransport({
|
|
1414
|
+
url: serverUrl,
|
|
1415
|
+
sessionId: this.sessionId,
|
|
1416
|
+
appName: config.appName ?? "server-app",
|
|
1417
|
+
sdkVersion: SDK_VERSION,
|
|
1418
|
+
authToken: config.authToken,
|
|
1419
|
+
maxQueueSize: config.maxQueueSize
|
|
1420
|
+
});
|
|
1421
|
+
this.transport.connect();
|
|
1422
|
+
if (config.sampleRate !== void 0 || config.maxEventsPerSecond !== void 0) {
|
|
1423
|
+
this.sampler = new Sampler({
|
|
1424
|
+
sampleRate: config.sampleRate,
|
|
1425
|
+
maxEventsPerSecond: config.maxEventsPerSecond
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
const emit = (event) => this.emitEvent(event);
|
|
1429
|
+
if (config.captureConsole !== false) {
|
|
1430
|
+
try {
|
|
1431
|
+
this.restoreFunctions.push(
|
|
1432
|
+
interceptConsole(emit, this.sessionId, {
|
|
1433
|
+
captureStackTraces: config.captureStackTraces,
|
|
1434
|
+
beforeSend: config.beforeSend
|
|
1435
|
+
})
|
|
1436
|
+
);
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (config.captureErrors !== false && config.captureConsole !== false) {
|
|
1441
|
+
try {
|
|
1442
|
+
this.restoreFunctions.push(
|
|
1443
|
+
interceptErrors(emit, this.sessionId, {
|
|
1444
|
+
beforeSend: config.beforeSend
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
} catch {
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (config.captureHttp) {
|
|
1451
|
+
try {
|
|
1452
|
+
this.restoreFunctions.push(
|
|
1453
|
+
interceptHttp(emit, this.sessionId, {
|
|
1454
|
+
captureBody: config.captureBody,
|
|
1455
|
+
maxBodySize: config.maxBodySize,
|
|
1456
|
+
redactHeaders: config.redactHeaders,
|
|
1457
|
+
// Auto-ignore the collector URL to prevent recursion
|
|
1458
|
+
ignoreUrls: [serverUrl.replace("ws://", "").replace("wss://", "")],
|
|
1459
|
+
beforeSend: config.beforeSend
|
|
1460
|
+
})
|
|
1461
|
+
);
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (config.capturePerformance) {
|
|
1466
|
+
try {
|
|
1467
|
+
this.restoreFunctions.push(
|
|
1468
|
+
startPerfMetrics(emit, this.sessionId, {
|
|
1469
|
+
intervalMs: config.performanceInterval,
|
|
1470
|
+
metrics: config.performanceMetrics
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
disconnect() {
|
|
1478
|
+
for (const restore of this.restoreFunctions) {
|
|
1479
|
+
try {
|
|
1480
|
+
restore();
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
this.restoreFunctions = [];
|
|
1485
|
+
this.sampler = null;
|
|
1486
|
+
this.transport?.disconnect();
|
|
1487
|
+
this.transport = null;
|
|
1488
|
+
}
|
|
1489
|
+
get currentSessionId() {
|
|
1490
|
+
return this.sessionId;
|
|
1491
|
+
}
|
|
1492
|
+
emitEvent(event) {
|
|
1493
|
+
if (this.sampler && !this.sampler.shouldSample(event)) return;
|
|
1494
|
+
if (this.config.beforeSend) {
|
|
1495
|
+
const filtered = this.config.beforeSend(event);
|
|
1496
|
+
if (!filtered) return;
|
|
1497
|
+
this.transport?.sendEvent(filtered);
|
|
1498
|
+
} else {
|
|
1499
|
+
this.transport?.sendEvent(event);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
// --- Express/Connect Middleware ---
|
|
1503
|
+
middleware(options) {
|
|
1504
|
+
return runtimeScopeMiddleware(
|
|
1505
|
+
(event) => this.emitEvent(event),
|
|
1506
|
+
this.sessionId,
|
|
1507
|
+
{
|
|
1508
|
+
...options,
|
|
1509
|
+
redactHeaders: options?.redactHeaders ?? this.config.redactHeaders,
|
|
1510
|
+
beforeSend: options?.beforeSend ?? this.config.beforeSend
|
|
1511
|
+
}
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
// --- ORM Instrumentation ---
|
|
1515
|
+
instrumentPrisma(client) {
|
|
1516
|
+
const restore = instrumentPrisma(client, {
|
|
1517
|
+
sessionId: this.sessionId,
|
|
1518
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1519
|
+
redact: this.config.redactParams,
|
|
1520
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1521
|
+
});
|
|
1522
|
+
this.restoreFunctions.push(restore);
|
|
1523
|
+
return client;
|
|
1524
|
+
}
|
|
1525
|
+
instrumentPg(pool) {
|
|
1526
|
+
const restore = instrumentPg(pool, {
|
|
1527
|
+
sessionId: this.sessionId,
|
|
1528
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1529
|
+
redact: this.config.redactParams,
|
|
1530
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1531
|
+
});
|
|
1532
|
+
this.restoreFunctions.push(restore);
|
|
1533
|
+
return pool;
|
|
1534
|
+
}
|
|
1535
|
+
instrumentKnex(knex) {
|
|
1536
|
+
const restore = instrumentKnex(knex, {
|
|
1537
|
+
sessionId: this.sessionId,
|
|
1538
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1539
|
+
redact: this.config.redactParams,
|
|
1540
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1541
|
+
});
|
|
1542
|
+
this.restoreFunctions.push(restore);
|
|
1543
|
+
return knex;
|
|
1544
|
+
}
|
|
1545
|
+
instrumentDrizzle(db) {
|
|
1546
|
+
const restore = instrumentDrizzle(db, {
|
|
1547
|
+
sessionId: this.sessionId,
|
|
1548
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1549
|
+
redact: this.config.redactParams,
|
|
1550
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1551
|
+
});
|
|
1552
|
+
this.restoreFunctions.push(restore);
|
|
1553
|
+
return db;
|
|
1554
|
+
}
|
|
1555
|
+
instrumentMysql2(pool) {
|
|
1556
|
+
const restore = instrumentMysql2(pool, {
|
|
1557
|
+
sessionId: this.sessionId,
|
|
1558
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1559
|
+
redact: this.config.redactParams,
|
|
1560
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1561
|
+
});
|
|
1562
|
+
this.restoreFunctions.push(restore);
|
|
1563
|
+
return pool;
|
|
1564
|
+
}
|
|
1565
|
+
instrumentBetterSqlite3(db) {
|
|
1566
|
+
const restore = instrumentBetterSqlite3(db, {
|
|
1567
|
+
sessionId: this.sessionId,
|
|
1568
|
+
captureStackTraces: this.config.captureStackTraces,
|
|
1569
|
+
redact: this.config.redactParams,
|
|
1570
|
+
onEvent: (event) => this.emitEvent(event)
|
|
1571
|
+
});
|
|
1572
|
+
this.restoreFunctions.push(restore);
|
|
1573
|
+
return db;
|
|
1574
|
+
}
|
|
1575
|
+
// --- Generic query capture ---
|
|
1576
|
+
async captureQuery(fn, options) {
|
|
1577
|
+
const start = performance.now();
|
|
1578
|
+
try {
|
|
1579
|
+
const result = await fn();
|
|
1580
|
+
const duration = performance.now() - start;
|
|
1581
|
+
this.emitEvent({
|
|
1582
|
+
eventId: generateId(),
|
|
1583
|
+
sessionId: this.sessionId,
|
|
1584
|
+
timestamp: Date.now(),
|
|
1585
|
+
eventType: "database",
|
|
1586
|
+
query: options?.label ?? "custom query",
|
|
1587
|
+
normalizedQuery: options?.label ?? "custom query",
|
|
1588
|
+
duration,
|
|
1589
|
+
tablesAccessed: [],
|
|
1590
|
+
operation: "OTHER",
|
|
1591
|
+
source: "generic",
|
|
1592
|
+
label: options?.label,
|
|
1593
|
+
stackTrace: this.config.captureStackTraces ? captureStack() : void 0,
|
|
1594
|
+
rowsReturned: Array.isArray(result) ? result.length : void 0
|
|
1595
|
+
});
|
|
1596
|
+
return result;
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
const duration = performance.now() - start;
|
|
1599
|
+
this.emitEvent({
|
|
1600
|
+
eventId: generateId(),
|
|
1601
|
+
sessionId: this.sessionId,
|
|
1602
|
+
timestamp: Date.now(),
|
|
1603
|
+
eventType: "database",
|
|
1604
|
+
query: options?.label ?? "custom query",
|
|
1605
|
+
normalizedQuery: options?.label ?? "custom query",
|
|
1606
|
+
duration,
|
|
1607
|
+
tablesAccessed: [],
|
|
1608
|
+
operation: "OTHER",
|
|
1609
|
+
source: "generic",
|
|
1610
|
+
label: options?.label,
|
|
1611
|
+
error: err.message,
|
|
1612
|
+
stackTrace: this.config.captureStackTraces ? captureStack() : void 0
|
|
1613
|
+
});
|
|
1614
|
+
throw err;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
var RuntimeScope = new RuntimeScopeServer();
|
|
1619
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1620
|
+
0 && (module.exports = {
|
|
1621
|
+
RuntimeScope,
|
|
1622
|
+
Sampler,
|
|
1623
|
+
_log,
|
|
1624
|
+
generateId,
|
|
1625
|
+
generateSessionId,
|
|
1626
|
+
getRequestContext,
|
|
1627
|
+
getSessionId,
|
|
1628
|
+
normalizeQuery,
|
|
1629
|
+
parseOperation,
|
|
1630
|
+
parseTablesAccessed,
|
|
1631
|
+
redactParams,
|
|
1632
|
+
runWithContext,
|
|
1633
|
+
runtimeScopeMiddleware
|
|
1634
|
+
});
|
|
1635
|
+
//# sourceMappingURL=index.cjs.map
|