@angelitosystems/nest-devtools 1.0.4 → 1.0.6
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 +704 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +707 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -23,13 +23,13 @@ __export(index_exports, {
|
|
|
23
23
|
NestDevTools: () => NestDevTools,
|
|
24
24
|
captureError: () => captureError,
|
|
25
25
|
devtools: () => devtools,
|
|
26
|
-
requestContext: () =>
|
|
26
|
+
requestContext: () => import_devtools_core12.requestContext
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
29
|
|
|
30
30
|
// src/sdk.ts
|
|
31
31
|
var import_os = require("os");
|
|
32
|
-
var
|
|
32
|
+
var import_devtools_core11 = require("@angelitosystems/devtools-core");
|
|
33
33
|
|
|
34
34
|
// src/instrumentation/http.ts
|
|
35
35
|
var import_devtools_protocol = require("@angelitosystems/devtools-protocol");
|
|
@@ -45,14 +45,37 @@ function emit(event, payload) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// src/instrumentation/timeline.ts
|
|
48
|
+
function startSpan(spans, layer, label, options) {
|
|
49
|
+
const span = {
|
|
50
|
+
layer,
|
|
51
|
+
label,
|
|
52
|
+
duration: 0,
|
|
53
|
+
startedAt: Date.now(),
|
|
54
|
+
status: options?.status ?? "ok",
|
|
55
|
+
...options?.detail !== void 0 ? { detail: options.detail } : {},
|
|
56
|
+
...options?.source !== void 0 ? { source: options.source } : {}
|
|
57
|
+
};
|
|
58
|
+
spans.push(span);
|
|
59
|
+
return span;
|
|
60
|
+
}
|
|
61
|
+
function endSpan(span, status) {
|
|
62
|
+
span.duration = Math.max(0, Date.now() - span.startedAt);
|
|
63
|
+
if (status) span.status = status;
|
|
64
|
+
}
|
|
48
65
|
var recorders = /* @__PURE__ */ new Map();
|
|
49
66
|
function trackSpans(requestId, spans) {
|
|
50
67
|
recorders.set(requestId, spans);
|
|
51
68
|
return () => recorders.delete(requestId);
|
|
52
69
|
}
|
|
70
|
+
function recordSpan(requestId, layer, label, options) {
|
|
71
|
+
const spans = recorders.get(requestId);
|
|
72
|
+
if (!spans) return;
|
|
73
|
+
const span = startSpan(spans, layer, label, options);
|
|
74
|
+
endSpan(span, options?.status);
|
|
75
|
+
}
|
|
53
76
|
|
|
54
77
|
// src/instrumentation/http.ts
|
|
55
|
-
var
|
|
78
|
+
var REQUEST_HEADERS = [
|
|
56
79
|
"content-type",
|
|
57
80
|
"content-length",
|
|
58
81
|
"accept",
|
|
@@ -60,7 +83,16 @@ var INTERESTING_HEADERS = [
|
|
|
60
83
|
"referer",
|
|
61
84
|
"user-agent",
|
|
62
85
|
"x-forwarded-for",
|
|
63
|
-
"x-request-id"
|
|
86
|
+
"x-request-id",
|
|
87
|
+
"x-forwarded-proto"
|
|
88
|
+
];
|
|
89
|
+
var RESPONSE_HEADERS = [
|
|
90
|
+
"content-type",
|
|
91
|
+
"content-length",
|
|
92
|
+
"location",
|
|
93
|
+
"set-cookie",
|
|
94
|
+
"x-request-id",
|
|
95
|
+
"x-response-time"
|
|
64
96
|
];
|
|
65
97
|
var WRAPPED = /* @__PURE__ */ Symbol("nest-devtools.wrapped");
|
|
66
98
|
var HttpInstrumentation = class {
|
|
@@ -143,7 +175,8 @@ var HttpInstrumentation = class {
|
|
|
143
175
|
projectId: this.ctx.projectInfo.projectId,
|
|
144
176
|
method,
|
|
145
177
|
url,
|
|
146
|
-
|
|
178
|
+
route: routeFromReq(req),
|
|
179
|
+
headers: this.collectRequestHeaders(req),
|
|
147
180
|
query: parseQuery(rawUrl),
|
|
148
181
|
ip: clientIp(req),
|
|
149
182
|
userAgent: asString(req.headers["user-agent"]) || void 0,
|
|
@@ -157,6 +190,7 @@ var HttpInstrumentation = class {
|
|
|
157
190
|
status: "ok"
|
|
158
191
|
};
|
|
159
192
|
spans.push(middlewareSpan);
|
|
193
|
+
const capturedBody = this.ctx.config.capture.requests && this.ctx.config.maxPayloadBytes > 0 ? captureRequestPreview(req, this.redactor) : void 0;
|
|
160
194
|
const onFinished = () => {
|
|
161
195
|
const finishedAt = Date.now();
|
|
162
196
|
middlewareSpan.duration = Math.max(0, finishedAt - middlewareSpan.startedAt);
|
|
@@ -166,20 +200,26 @@ var HttpInstrumentation = class {
|
|
|
166
200
|
label: "response",
|
|
167
201
|
duration: 0,
|
|
168
202
|
startedAt: finishedAt,
|
|
169
|
-
status: "ok"
|
|
203
|
+
status: res.statusCode >= 400 ? "error" : "ok"
|
|
170
204
|
};
|
|
171
205
|
spans.push(responseSpan);
|
|
206
|
+
const responseHeaders = this.collectResponseHeaders(res);
|
|
207
|
+
const responsePreview = this.ctx.config.capture.requests && this.ctx.config.maxPayloadBytes > 0 ? captureResponsePreview(res, this.redactor) : void 0;
|
|
172
208
|
const errored = res.statusCode >= 500;
|
|
173
209
|
const completed = {
|
|
174
210
|
requestId,
|
|
175
211
|
projectId: this.ctx.projectInfo.projectId,
|
|
176
212
|
method,
|
|
177
213
|
url,
|
|
214
|
+
route: routeFromReq(req),
|
|
178
215
|
statusCode: res.statusCode,
|
|
179
216
|
duration: finishedAt - startedAt,
|
|
180
217
|
startedAt,
|
|
181
218
|
timeline: spans,
|
|
182
219
|
query: parseQuery(rawUrl),
|
|
220
|
+
headers: responseHeaders,
|
|
221
|
+
responsePreview,
|
|
222
|
+
requestBody: capturedBody,
|
|
183
223
|
errored
|
|
184
224
|
};
|
|
185
225
|
emit("request.completed", completed);
|
|
@@ -190,7 +230,7 @@ var HttpInstrumentation = class {
|
|
|
190
230
|
name: "HttpError",
|
|
191
231
|
message: `${method} ${url} failed with status ${res.statusCode}`,
|
|
192
232
|
fingerprint: httpErrorFingerprint(method, url, res.statusCode),
|
|
193
|
-
request: { method, url, statusCode: res.statusCode },
|
|
233
|
+
request: { method, url: String(url), statusCode: res.statusCode },
|
|
194
234
|
timestamp: finishedAt
|
|
195
235
|
});
|
|
196
236
|
}
|
|
@@ -202,15 +242,30 @@ var HttpInstrumentation = class {
|
|
|
202
242
|
next();
|
|
203
243
|
});
|
|
204
244
|
}
|
|
205
|
-
|
|
245
|
+
collectRequestHeaders(req) {
|
|
206
246
|
const out = {};
|
|
207
|
-
for (const key of
|
|
247
|
+
for (const key of REQUEST_HEADERS) {
|
|
208
248
|
const value = req.headers[key];
|
|
209
249
|
if (value === void 0) continue;
|
|
210
250
|
out[key] = this.redactor.isSensitive(key) ? "[REDACTED]" : asString(value).slice(0, 200);
|
|
211
251
|
}
|
|
212
252
|
return out;
|
|
213
253
|
}
|
|
254
|
+
collectResponseHeaders(res) {
|
|
255
|
+
const out = {};
|
|
256
|
+
const headers = res._headers;
|
|
257
|
+
if (!headers || typeof headers !== "object") return out;
|
|
258
|
+
for (const rawKey of Object.keys(headers)) {
|
|
259
|
+
const key = rawKey.toLowerCase();
|
|
260
|
+
const ok = RESPONSE_HEADERS.some((h) => h.toLowerCase() === key);
|
|
261
|
+
if (!ok) continue;
|
|
262
|
+
const raw = headers[rawKey] ?? "";
|
|
263
|
+
const value = String(raw);
|
|
264
|
+
if (value === "") continue;
|
|
265
|
+
out[key] = this.redactor.isSensitive(key) ? "[REDACTED]" : value.slice(0, 200);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
214
269
|
};
|
|
215
270
|
function asString(value) {
|
|
216
271
|
if (Array.isArray(value)) return value.join(", ");
|
|
@@ -243,6 +298,54 @@ function clientIp(req) {
|
|
|
243
298
|
if (typeof forwarded === "string") return forwarded.split(",")[0]?.trim();
|
|
244
299
|
return req.socket?.remoteAddress ?? void 0;
|
|
245
300
|
}
|
|
301
|
+
function routeFromReq(req) {
|
|
302
|
+
try {
|
|
303
|
+
const http = req;
|
|
304
|
+
const out = {
|
|
305
|
+
requestId: "",
|
|
306
|
+
projectId: "",
|
|
307
|
+
method: "",
|
|
308
|
+
url: "",
|
|
309
|
+
route: void 0,
|
|
310
|
+
headers: {},
|
|
311
|
+
query: {},
|
|
312
|
+
ip: void 0,
|
|
313
|
+
userAgent: void 0,
|
|
314
|
+
startedAt: 0
|
|
315
|
+
};
|
|
316
|
+
const routeRaw = req.route;
|
|
317
|
+
if (routeRaw && typeof routeRaw === "object" && routeRaw !== null) {
|
|
318
|
+
const route = routeRaw;
|
|
319
|
+
const name = route.name;
|
|
320
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
321
|
+
}
|
|
322
|
+
const path = req.baseUrl ?? req.path;
|
|
323
|
+
if (typeof path === "string" && path.length > 0) return path;
|
|
324
|
+
return void 0;
|
|
325
|
+
} catch {
|
|
326
|
+
return void 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function captureRequestPreview(req, redactor) {
|
|
330
|
+
try {
|
|
331
|
+
const body = req.body;
|
|
332
|
+
if (body === void 0 || body === null) return void 0;
|
|
333
|
+
return redactor.redact(body);
|
|
334
|
+
} catch {
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function captureResponsePreview(res, redactor) {
|
|
339
|
+
try {
|
|
340
|
+
const resLike = res;
|
|
341
|
+
const body = resLike.body;
|
|
342
|
+
if (body === void 0 || body === null) return void 0;
|
|
343
|
+
if (typeof body === "string") return redactor.redactString(body);
|
|
344
|
+
return redactor.serialize(body);
|
|
345
|
+
} catch {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
246
349
|
|
|
247
350
|
// src/instrumentation/console.ts
|
|
248
351
|
var import_console = require("console");
|
|
@@ -576,7 +679,8 @@ var AppExplorer = class {
|
|
|
576
679
|
const w = wrapper;
|
|
577
680
|
const name = w?.metatype?.name ?? (typeof id === "string" ? id.replace(/^[A-Z_0-9]+:/, "") : "unknown");
|
|
578
681
|
const type = classify(name, w?.instance);
|
|
579
|
-
|
|
682
|
+
const routes = type === "controller" ? extractControllerRoutes(w?.metatype) : void 0;
|
|
683
|
+
return { name, type, routes };
|
|
580
684
|
}
|
|
581
685
|
};
|
|
582
686
|
function classify(name, instance) {
|
|
@@ -591,33 +695,610 @@ function classify(name, instance) {
|
|
|
591
695
|
if (instance && typeof instance.catch === "function") return "filter";
|
|
592
696
|
return "provider";
|
|
593
697
|
}
|
|
698
|
+
function extractControllerRoutes(metatype) {
|
|
699
|
+
if (!metatype) return [];
|
|
700
|
+
try {
|
|
701
|
+
const proto = metatype.prototype;
|
|
702
|
+
if (!proto) return [];
|
|
703
|
+
const props = Object.getOwnPropertyNames(proto).filter((name) => name !== "constructor");
|
|
704
|
+
const routes = [];
|
|
705
|
+
for (const key of props) {
|
|
706
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
707
|
+
if (!desc?.value) continue;
|
|
708
|
+
const value = desc.value;
|
|
709
|
+
const routesForMethod = gatherHttpRoutes(value);
|
|
710
|
+
for (const r of routesForMethod) {
|
|
711
|
+
if (typeof r === "string" && r.length > 0) routes.push(r);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return routes;
|
|
715
|
+
} catch {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function gatherHttpRoutes(value) {
|
|
720
|
+
try {
|
|
721
|
+
const decorators = Reflect.getMetadata?.("design:http:method", {}, void 0, void 0) ?? [];
|
|
722
|
+
const out = [];
|
|
723
|
+
if (decorators.length > 0) {
|
|
724
|
+
for (const d of decorators) {
|
|
725
|
+
if (d && typeof d === "object" && d.method && d.path) {
|
|
726
|
+
out.push(`${String(d.method).toUpperCase()} ${String(d.path)}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (typeof value === "function") {
|
|
731
|
+
const fn = value;
|
|
732
|
+
const path = fn.PATH ?? void 0;
|
|
733
|
+
const method = fn.METHOD ?? void 0;
|
|
734
|
+
if (typeof path === "string" && typeof method === "string") {
|
|
735
|
+
out.push(`${method.toUpperCase()} ${path}`);
|
|
736
|
+
} else if (typeof path === "string") {
|
|
737
|
+
out.push(path);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
} catch {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
594
745
|
|
|
595
746
|
// src/instrumentation/websockets.ts
|
|
747
|
+
var import_devtools_core7 = require("@angelitosystems/devtools-core");
|
|
748
|
+
var import_devtools_protocol4 = require("@angelitosystems/devtools-protocol");
|
|
596
749
|
var WebsocketInstrumentation = class {
|
|
750
|
+
redactor;
|
|
751
|
+
config;
|
|
752
|
+
projectId;
|
|
753
|
+
ctxRaw;
|
|
597
754
|
constructor(ctx) {
|
|
598
|
-
this.
|
|
755
|
+
this.ctxRaw = ctx;
|
|
756
|
+
this.config = ctx.config;
|
|
757
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
758
|
+
this.redactor = new import_devtools_protocol4.Redactor({
|
|
759
|
+
redact: this.config.redact,
|
|
760
|
+
allow: this.config.allow,
|
|
761
|
+
maxBytes: this.config.maxPayloadBytes
|
|
762
|
+
});
|
|
599
763
|
}
|
|
600
|
-
|
|
601
|
-
/** No-op for now. Returns a no-op cleanup. */
|
|
764
|
+
/** Attach gateway hooks. Returns cleanup. */
|
|
602
765
|
attach() {
|
|
603
|
-
|
|
604
|
-
return () =>
|
|
766
|
+
const cleanup = [];
|
|
767
|
+
if (!this.config.capture.websockets) return () => cleanup.forEach((fn) => fn());
|
|
768
|
+
const gateways = this.detectGateways();
|
|
769
|
+
for (const gw of gateways) {
|
|
770
|
+
cleanup.push(this.wrapGateway(gw));
|
|
771
|
+
}
|
|
772
|
+
return () => cleanup.forEach((fn) => fn());
|
|
773
|
+
}
|
|
774
|
+
detectGateways() {
|
|
775
|
+
try {
|
|
776
|
+
const app = this.getApp();
|
|
777
|
+
if (!app) return [];
|
|
778
|
+
const container = app.container;
|
|
779
|
+
if (!container) return [];
|
|
780
|
+
const modules = container.getModules?.() ?? /* @__PURE__ */ new Map();
|
|
781
|
+
const out = [];
|
|
782
|
+
for (const [id, node] of modules) {
|
|
783
|
+
const providers = node.providers;
|
|
784
|
+
if (!providers || !(providers instanceof Map)) continue;
|
|
785
|
+
for (const [key, wrapper] of providers) {
|
|
786
|
+
const w = wrapper;
|
|
787
|
+
if (!w || !w.instance) continue;
|
|
788
|
+
const subtype = w.subtype ?? (node.subtype ?? null);
|
|
789
|
+
if (subtype === "gateway" || w.instance && w.instance.constructor?.name?.endsWith("Gateway")) {
|
|
790
|
+
out.push({ name: w.name ?? key, instance: w.instance });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return out;
|
|
795
|
+
} catch {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
wrapGateway(gw) {
|
|
800
|
+
const instance = gw.instance;
|
|
801
|
+
if (!instance || typeof instance !== "object") return () => {
|
|
605
802
|
};
|
|
803
|
+
const hooks = [];
|
|
804
|
+
const methods = [
|
|
805
|
+
"handleConnection",
|
|
806
|
+
"handleDisconnect",
|
|
807
|
+
"handleEvent",
|
|
808
|
+
"handleMessage",
|
|
809
|
+
"afterInit",
|
|
810
|
+
"beforeHandle"
|
|
811
|
+
];
|
|
812
|
+
for (const method of methods) {
|
|
813
|
+
if (typeof instance[method] !== "function") continue;
|
|
814
|
+
const orig = instance[method];
|
|
815
|
+
const wrapped = this.buildGatewayWrapper(instance, method, orig);
|
|
816
|
+
instance[method] = wrapped;
|
|
817
|
+
hooks.push(() => {
|
|
818
|
+
instance[method] = orig;
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return () => hooks.forEach((fn) => fn());
|
|
822
|
+
}
|
|
823
|
+
buildGatewayWrapper(instance, method, original) {
|
|
824
|
+
return async (...args) => {
|
|
825
|
+
const started = Date.now();
|
|
826
|
+
const requestId = (0, import_devtools_core7.requestContext)().requestId() ?? (0, import_devtools_protocol4.randomId)("ws");
|
|
827
|
+
if (method === "handleConnection" || method === "afterInit") {
|
|
828
|
+
emit("websocket.connected", {
|
|
829
|
+
projectId: this.projectId,
|
|
830
|
+
gateway: instance.constructor?.name ?? this.estimateGatewayName(instance),
|
|
831
|
+
namespace: this.estimateNamespace(args),
|
|
832
|
+
connections: this.countConnections(instance),
|
|
833
|
+
timestamp: Date.now()
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
837
|
+
const payload = this.extractPayload(args);
|
|
838
|
+
recordSpan(requestId, "websocket", `ws.receive:${this.estimateEvent(args)}`, {
|
|
839
|
+
detail: `gateway=${instance.constructor?.name} payload=${this.redactor.serialize(payload)}`
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
const result = await Promise.resolve(original.call(instance, ...args));
|
|
844
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
845
|
+
const payload = this.extractPayload(args);
|
|
846
|
+
recordSpan(requestId, "websocket", `ws.send:${this.estimateEvent(args)}`, {
|
|
847
|
+
detail: `gateway=${instance.constructor?.name} payload=${this.redactor.serialize(payload)}`,
|
|
848
|
+
status: "ok"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return result;
|
|
852
|
+
} catch (err) {
|
|
853
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
854
|
+
recordSpan(requestId, "websocket", `ws.send:${this.estimateEvent(args)}`, {
|
|
855
|
+
detail: `gateway=${instance.constructor?.name} error=${this.redactor.serialize(err)}`,
|
|
856
|
+
status: "error"
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
extractPayload(args) {
|
|
864
|
+
for (const arg of args) {
|
|
865
|
+
if (arg && typeof arg === "object" && !Buffer.isBuffer(arg) && !ArrayBuffer.isView(arg)) {
|
|
866
|
+
return arg;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return args[0];
|
|
870
|
+
}
|
|
871
|
+
estimateEvent(args) {
|
|
872
|
+
const payload = this.extractPayload(args);
|
|
873
|
+
if (!payload || typeof payload !== "object") return "message";
|
|
874
|
+
return payload.event ?? payload.type ?? "message";
|
|
875
|
+
}
|
|
876
|
+
estimateNamespace(args) {
|
|
877
|
+
if (args.length > 0) {
|
|
878
|
+
const first = args[0];
|
|
879
|
+
if (first && typeof first === "object" && first.namespace) return first.namespace;
|
|
880
|
+
}
|
|
881
|
+
return "/";
|
|
882
|
+
}
|
|
883
|
+
countConnections(instance) {
|
|
884
|
+
try {
|
|
885
|
+
if (instance.connections && typeof instance.connections === "function") return instance.connections();
|
|
886
|
+
if (instance.clients && typeof instance.clients === "function") return instance.clients().size ?? 0;
|
|
887
|
+
return 0;
|
|
888
|
+
} catch {
|
|
889
|
+
return 0;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
estimateGatewayName(instance) {
|
|
893
|
+
return instance.constructor?.name ?? "Gateway";
|
|
894
|
+
}
|
|
895
|
+
getApp() {
|
|
896
|
+
try {
|
|
897
|
+
const ctx = this.ctxRaw;
|
|
898
|
+
if (ctx.getHttpAdapter) return ctx;
|
|
899
|
+
if (ctx.app) return ctx.app;
|
|
900
|
+
return null;
|
|
901
|
+
} catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
606
904
|
}
|
|
607
905
|
};
|
|
608
906
|
|
|
609
907
|
// src/instrumentation/database.ts
|
|
908
|
+
var import_devtools_core8 = require("@angelitosystems/devtools-core");
|
|
909
|
+
var import_devtools_protocol5 = require("@angelitosystems/devtools-protocol");
|
|
610
910
|
var DatabaseInstrumentation = class {
|
|
911
|
+
redactor;
|
|
912
|
+
config;
|
|
913
|
+
projectId;
|
|
914
|
+
ctxRaw;
|
|
611
915
|
constructor(ctx) {
|
|
612
|
-
this.
|
|
916
|
+
this.ctxRaw = ctx;
|
|
917
|
+
this.config = ctx.config;
|
|
918
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
919
|
+
this.redactor = new import_devtools_protocol5.Redactor({
|
|
920
|
+
redact: this.config.redact,
|
|
921
|
+
allow: this.config.allow,
|
|
922
|
+
maxBytes: this.config.maxPayloadBytes
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
/** Attach database hooks for detected clients. Returns cleanup. */
|
|
926
|
+
attach() {
|
|
927
|
+
const cleanup = [];
|
|
928
|
+
if (!this.config.capture.database) return () => cleanup.forEach((fn) => fn());
|
|
929
|
+
if (this.tryAttachTypeOrm()) cleanup.push(this.detachTypeOrm());
|
|
930
|
+
if (this.tryAttachPrisma()) cleanup.push(this.detachPrisma());
|
|
931
|
+
if (this.tryAttachMongoose()) cleanup.push(this.detachMongoose());
|
|
932
|
+
return () => cleanup.forEach((fn) => fn());
|
|
613
933
|
}
|
|
934
|
+
tryAttachTypeOrm() {
|
|
935
|
+
try {
|
|
936
|
+
const app = this.getApp();
|
|
937
|
+
if (!app) return false;
|
|
938
|
+
const dataSource = this.resolveTypeOrmDataSource(app);
|
|
939
|
+
if (!dataSource) return false;
|
|
940
|
+
const listener = {
|
|
941
|
+
beforeQuery: (query, parameters) => {
|
|
942
|
+
this.startQuery(query, parameters);
|
|
943
|
+
},
|
|
944
|
+
afterQuery: (query, parameters, durationMs) => {
|
|
945
|
+
this.endQuery(query, parameters, durationMs);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const sub = dataSource.subscriber;
|
|
949
|
+
if (sub) {
|
|
950
|
+
const before = sub.beforeQuery;
|
|
951
|
+
const after = sub.afterQuery;
|
|
952
|
+
sub.beforeQuery = (q, p) => {
|
|
953
|
+
try {
|
|
954
|
+
before?.(q, p);
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
listener.beforeQuery(q, p);
|
|
958
|
+
};
|
|
959
|
+
sub.afterQuery = (q, p, d) => {
|
|
960
|
+
try {
|
|
961
|
+
after?.(q, p, d);
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
listener.afterQuery(q, p, d);
|
|
965
|
+
};
|
|
966
|
+
} else {
|
|
967
|
+
dataSource.subscriber = listener;
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
} catch {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
detachTypeOrm() {
|
|
975
|
+
return () => {
|
|
976
|
+
try {
|
|
977
|
+
const app = this.getApp();
|
|
978
|
+
if (!app) return;
|
|
979
|
+
const ds = this.resolveTypeOrmDataSource(app);
|
|
980
|
+
if (!ds || !ds.subscriber) return;
|
|
981
|
+
ds.subscriber = null;
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
tryAttachPrisma() {
|
|
987
|
+
try {
|
|
988
|
+
const app = this.getApp();
|
|
989
|
+
if (!app) return false;
|
|
990
|
+
const prisma = this.resolvePrismaClient(app);
|
|
991
|
+
if (!prisma) return false;
|
|
992
|
+
const original = prisma.$queryRaw ?? prisma.$executeRaw ?? null;
|
|
993
|
+
if (!original) return false;
|
|
994
|
+
const wrapped = (query, parameters) => {
|
|
995
|
+
const started = Date.now();
|
|
996
|
+
this.startQuery(String(query), parameters ? [parameters] : []);
|
|
997
|
+
try {
|
|
998
|
+
const result = original.call(prisma, query, parameters);
|
|
999
|
+
if (result instanceof Promise) {
|
|
1000
|
+
return result.then((r) => {
|
|
1001
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started);
|
|
1002
|
+
return r;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started);
|
|
1006
|
+
return result;
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started, err);
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
prisma.$queryRaw = wrapped;
|
|
1013
|
+
prisma.$executeRaw = wrapped;
|
|
1014
|
+
return true;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
detachPrisma() {
|
|
1020
|
+
return () => {
|
|
1021
|
+
try {
|
|
1022
|
+
const app = this.getApp();
|
|
1023
|
+
if (!app) return;
|
|
1024
|
+
const prisma = this.resolvePrismaClient(app);
|
|
1025
|
+
if (!prisma) return;
|
|
1026
|
+
if (typeof prisma.$extends === "function") prisma.$extends(prisma);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
tryAttachMongoose() {
|
|
1032
|
+
try {
|
|
1033
|
+
const app = this.getApp();
|
|
1034
|
+
if (!app) return false;
|
|
1035
|
+
const mongoose = this.resolveMongoose(app);
|
|
1036
|
+
if (!mongoose) return false;
|
|
1037
|
+
const instrumentation = this;
|
|
1038
|
+
const hook = mongoose.plugin((schema, _name) => {
|
|
1039
|
+
if (!schema || typeof schema !== "object") return;
|
|
1040
|
+
const hooks = schema.pre ?? null;
|
|
1041
|
+
if (typeof hooks !== "function") return;
|
|
1042
|
+
const bound = hooks.bind(schema);
|
|
1043
|
+
if (!bound) return;
|
|
1044
|
+
const originalPre = bound;
|
|
1045
|
+
schema.pre = function(method, fn) {
|
|
1046
|
+
if (method === "find" || method === "findOne" || method === "findById" || method === "aggregate" || method === "countDocuments" || method === "count") {
|
|
1047
|
+
const wrapped = async function(...args) {
|
|
1048
|
+
const started = Date.now();
|
|
1049
|
+
const sql = `[Mongoose:${method}]`;
|
|
1050
|
+
instrumentation.startQuery(sql, args);
|
|
1051
|
+
try {
|
|
1052
|
+
const handler = fn;
|
|
1053
|
+
if (!handler) return;
|
|
1054
|
+
const result = await handler.apply(this, args);
|
|
1055
|
+
instrumentation.endQuery(sql, args, Date.now() - started);
|
|
1056
|
+
return result;
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
instrumentation.endQuery(sql, args, Date.now() - started, err);
|
|
1059
|
+
throw err;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
originalPre(method, wrapped);
|
|
1063
|
+
} else {
|
|
1064
|
+
originalPre(method, fn);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
});
|
|
1068
|
+
if (typeof mongoose.plugin === "function") mongoose.plugin(hook);
|
|
1069
|
+
return true;
|
|
1070
|
+
} catch {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
detachMongoose() {
|
|
1075
|
+
return () => {
|
|
1076
|
+
try {
|
|
1077
|
+
const app = this.getApp();
|
|
1078
|
+
if (!app) return;
|
|
1079
|
+
const mongoose = this.resolveMongoose(app);
|
|
1080
|
+
if (!mongoose || typeof mongoose.plugin !== "function") return;
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
startQuery(sql, parameters) {
|
|
1086
|
+
if (!this.config.capture.database) return;
|
|
1087
|
+
const requestId = (0, import_devtools_core8.requestContext)().requestId() ?? (0, import_devtools_protocol5.randomId)("db");
|
|
1088
|
+
recordSpan(requestId, "database", sql.slice(0, 120), {
|
|
1089
|
+
detail: `provider=${this.providerLabel()} parameters=${this.redactor.serialize(parameters)}`
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
endQuery(sql, parameters, durationMs, error) {
|
|
1093
|
+
if (!this.config.capture.database) return;
|
|
1094
|
+
const requestId = (0, import_devtools_core8.requestContext)().requestId() ?? (0, import_devtools_protocol5.randomId)("db");
|
|
1095
|
+
const payload = {
|
|
1096
|
+
requestId,
|
|
1097
|
+
projectId: this.projectId,
|
|
1098
|
+
provider: this.providerLabel(),
|
|
1099
|
+
sql: sql.slice(0, 2e3),
|
|
1100
|
+
duration: durationMs,
|
|
1101
|
+
parameters: this.redactor.redact(parameters),
|
|
1102
|
+
timestamp: Date.now()
|
|
1103
|
+
};
|
|
1104
|
+
emit("query.executed", payload);
|
|
1105
|
+
}
|
|
1106
|
+
providerLabel() {
|
|
1107
|
+
if (this.isTypeOrm()) return "typeorm";
|
|
1108
|
+
if (this.isPrisma()) return "prisma";
|
|
1109
|
+
if (this.isMongoose()) return "mongoose";
|
|
1110
|
+
return "other";
|
|
1111
|
+
}
|
|
1112
|
+
isTypeOrm() {
|
|
1113
|
+
return !!this.resolveTypeOrmDataSource(this.getApp());
|
|
1114
|
+
}
|
|
1115
|
+
isPrisma() {
|
|
1116
|
+
return !!this.resolvePrismaClient(this.getApp());
|
|
1117
|
+
}
|
|
1118
|
+
isMongoose() {
|
|
1119
|
+
return !!this.resolveMongoose(this.getApp());
|
|
1120
|
+
}
|
|
1121
|
+
getApp() {
|
|
1122
|
+
try {
|
|
1123
|
+
const ctx = this.ctxRaw;
|
|
1124
|
+
if (ctx && typeof ctx === "object" && ctx.app) return ctx.app;
|
|
1125
|
+
if (ctx && typeof ctx === "object" && ctx.getHttpAdapter) {
|
|
1126
|
+
return ctx;
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
resolveTypeOrmDataSource(app) {
|
|
1134
|
+
try {
|
|
1135
|
+
const dataSource = app.get(require("@nestjs/typeorm").TypeOrmModule)?.options?.DataSource ?? null;
|
|
1136
|
+
if (dataSource && typeof dataSource.query === "function") return dataSource;
|
|
1137
|
+
return null;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
resolvePrismaClient(app) {
|
|
1143
|
+
try {
|
|
1144
|
+
const prisma = app.get("PrismaService") ?? app.get("PrismaClient") ?? app.get("prisma") ?? null;
|
|
1145
|
+
if (prisma && typeof prisma.$queryRaw === "function") return prisma;
|
|
1146
|
+
return null;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
resolveMongoose(app) {
|
|
1152
|
+
try {
|
|
1153
|
+
const mongoose = app.get("MongooseService") ?? app.get("Mongoose") ?? app.get("mongoose") ?? null;
|
|
1154
|
+
if (mongoose && typeof mongoose.plugin === "function") return mongoose;
|
|
1155
|
+
return null;
|
|
1156
|
+
} catch {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/instrumentation/queue.ts
|
|
1163
|
+
var import_devtools_core9 = require("@angelitosystems/devtools-core");
|
|
1164
|
+
var import_devtools_protocol6 = require("@angelitosystems/devtools-protocol");
|
|
1165
|
+
var import_devtools_core10 = require("@angelitosystems/devtools-core");
|
|
1166
|
+
var QueueEventInstrumentation = class _QueueEventInstrumentation {
|
|
1167
|
+
redactor;
|
|
1168
|
+
config;
|
|
1169
|
+
projectId;
|
|
614
1170
|
ctx;
|
|
615
|
-
|
|
1171
|
+
constructor(ctx) {
|
|
1172
|
+
this.ctx = ctx;
|
|
1173
|
+
this.config = ctx.config;
|
|
1174
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
1175
|
+
this.redactor = new import_devtools_protocol6.Redactor({
|
|
1176
|
+
redact: this.config.redact,
|
|
1177
|
+
allow: this.config.allow,
|
|
1178
|
+
maxBytes: this.config.maxPayloadBytes
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
/** Attach queue hooks if a supported client is detected. Returns cleanup. */
|
|
616
1182
|
attach() {
|
|
617
|
-
|
|
1183
|
+
const cleanup = [];
|
|
1184
|
+
if (!this.config.capture.requests && !this.config.capture.logs && !this.config.capture.errors) {
|
|
1185
|
+
return () => cleanup.forEach((fn) => fn());
|
|
1186
|
+
}
|
|
1187
|
+
if (this.tryAttachBull()) cleanup.push(this.detachBull());
|
|
1188
|
+
return () => cleanup.forEach((fn) => fn());
|
|
1189
|
+
}
|
|
1190
|
+
/** Register a queue span correlated with the active request, if any. */
|
|
1191
|
+
static recordJobSpan(queueName, jobId, label, options) {
|
|
1192
|
+
const requestId = (0, import_devtools_core9.requestContext)().requestId() ?? (0, import_devtools_protocol6.randomId)("queue");
|
|
1193
|
+
recordSpan(requestId, "queue", `${queueName}:${label} (${jobId})`, {
|
|
1194
|
+
detail: `queue=${queueName} job=${jobId}`,
|
|
1195
|
+
source: options?.source,
|
|
1196
|
+
status: options?.status
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
/** Emit a log for queue events, correlated with the active request if any. */
|
|
1200
|
+
static emitJobLog(queueName, jobId, level, message, options) {
|
|
1201
|
+
const requestId = (0, import_devtools_core9.requestContext)().requestId();
|
|
1202
|
+
emit("log.created", {
|
|
1203
|
+
requestId,
|
|
1204
|
+
projectId: void 0,
|
|
1205
|
+
level,
|
|
1206
|
+
message: `[${queueName}] ${message} (${jobId})`,
|
|
1207
|
+
source: options?.source,
|
|
1208
|
+
context: queueName,
|
|
1209
|
+
processId: process.pid,
|
|
1210
|
+
timestamp: Date.now()
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
tryAttachBull() {
|
|
1214
|
+
try {
|
|
1215
|
+
const app = this.getApp();
|
|
1216
|
+
if (!app) return false;
|
|
1217
|
+
const bull = this.resolveBull(app);
|
|
1218
|
+
if (!bull) return false;
|
|
1219
|
+
const origProcess = bull.prototype?.process ?? bull.process;
|
|
1220
|
+
if (typeof origProcess === "function") {
|
|
1221
|
+
const wrapped = this.wrapProcess(origProcess, bull);
|
|
1222
|
+
if (bull.prototype) bull.prototype.process = wrapped;
|
|
1223
|
+
bull.process = wrapped;
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
detachBull() {
|
|
618
1231
|
return () => {
|
|
1232
|
+
try {
|
|
1233
|
+
const app = this.getApp();
|
|
1234
|
+
if (!app) return;
|
|
1235
|
+
const bull = this.resolveBull(app);
|
|
1236
|
+
if (!bull) return;
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
wrapProcess(original, bull) {
|
|
1242
|
+
return async function(...args) {
|
|
1243
|
+
const handler = args[0];
|
|
1244
|
+
if (!handler || typeof handler !== "function") return original.apply(this, args);
|
|
1245
|
+
const wrapped = async (...handlerArgs) => {
|
|
1246
|
+
const job = handlerArgs[0];
|
|
1247
|
+
if (!job || typeof job !== "object") return handler.apply(this, handlerArgs);
|
|
1248
|
+
const jobId = job.id ?? job.data?.jobId ?? (0, import_devtools_protocol6.randomId)("job");
|
|
1249
|
+
const queueName = job.queueName ?? this.estimateQueueName(bull);
|
|
1250
|
+
const started = Date.now();
|
|
1251
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", { status: "ok" });
|
|
1252
|
+
try {
|
|
1253
|
+
const result = await Promise.resolve(handler.apply(this, handlerArgs));
|
|
1254
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", {
|
|
1255
|
+
status: "ok"
|
|
1256
|
+
});
|
|
1257
|
+
_QueueEventInstrumentation.emitJobLog(queueName, jobId, "info", "job completed", {
|
|
1258
|
+
source: (0, import_devtools_core10.resolveSourceLocation)(new Error(), { skip: 2 })
|
|
1259
|
+
});
|
|
1260
|
+
return result;
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", {
|
|
1263
|
+
status: "error"
|
|
1264
|
+
});
|
|
1265
|
+
_QueueEventInstrumentation.emitJobLog(queueName, jobId, "error", String(err), {
|
|
1266
|
+
source: (0, import_devtools_core10.resolveSourceLocation)(err instanceof Error ? err : new Error(), { skip: 2 })
|
|
1267
|
+
});
|
|
1268
|
+
throw err;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
return original.apply(this, [wrapped, ...args.slice(1)]);
|
|
619
1272
|
};
|
|
620
1273
|
}
|
|
1274
|
+
estimateQueueName(bull) {
|
|
1275
|
+
try {
|
|
1276
|
+
if (bull.name) return bull.name;
|
|
1277
|
+
if (typeof bull === "function") return bull.name;
|
|
1278
|
+
return "queue";
|
|
1279
|
+
} catch {
|
|
1280
|
+
return "queue";
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
resolveBull(app) {
|
|
1284
|
+
try {
|
|
1285
|
+
const bull = app.get("BullService") ?? app.get("Bull") ?? app.get("bull") ?? app.get("Queue") ?? null;
|
|
1286
|
+
if (bull && (typeof bull.process === "function" || typeof bull.prototype?.process === "function")) return bull;
|
|
1287
|
+
return null;
|
|
1288
|
+
} catch {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
getApp() {
|
|
1293
|
+
try {
|
|
1294
|
+
const ctx = this.ctx;
|
|
1295
|
+
if (ctx.getHttpAdapter) return ctx;
|
|
1296
|
+
if (ctx.app) return ctx.app;
|
|
1297
|
+
return null;
|
|
1298
|
+
} catch {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
621
1302
|
};
|
|
622
1303
|
|
|
623
1304
|
// src/detect.ts
|
|
@@ -700,7 +1381,7 @@ var NestDevTools = class {
|
|
|
700
1381
|
static initialized = false;
|
|
701
1382
|
/** Initialize DevTools for a NestJS application. One active instance per process. */
|
|
702
1383
|
static init(app, userConfig) {
|
|
703
|
-
const config = (0,
|
|
1384
|
+
const config = (0, import_devtools_core11.resolveConfig)(userConfig);
|
|
704
1385
|
const disabled = {
|
|
705
1386
|
enabled: false,
|
|
706
1387
|
reason: "disabled-by-config",
|
|
@@ -726,7 +1407,7 @@ var NestDevTools = class {
|
|
|
726
1407
|
sdkVersion: SDK_VERSION
|
|
727
1408
|
};
|
|
728
1409
|
let hasWarnedOffline = false;
|
|
729
|
-
|
|
1410
|
+
import_devtools_core11.devtools.initialize({
|
|
730
1411
|
config,
|
|
731
1412
|
projectInfo,
|
|
732
1413
|
registerCleanup,
|
|
@@ -754,6 +1435,7 @@ var NestDevTools = class {
|
|
|
754
1435
|
if (config.capture.database) {
|
|
755
1436
|
registerCleanup(new DatabaseInstrumentation({ config, projectInfo }).attach());
|
|
756
1437
|
}
|
|
1438
|
+
registerCleanup(new QueueEventInstrumentation({ config, projectInfo }).attach());
|
|
757
1439
|
printStartupBanner({
|
|
758
1440
|
endpoint: config.server,
|
|
759
1441
|
dashboard: dashboardUrl(config.server),
|
|
@@ -776,7 +1458,7 @@ var NestDevTools = class {
|
|
|
776
1458
|
}
|
|
777
1459
|
}
|
|
778
1460
|
this.initialized = false;
|
|
779
|
-
|
|
1461
|
+
import_devtools_core11.devtools.shutdown();
|
|
780
1462
|
}
|
|
781
1463
|
/** Whether the SDK is currently active in this process. */
|
|
782
1464
|
static isActive() {
|
|
@@ -821,7 +1503,7 @@ function dashboardUrl(server) {
|
|
|
821
1503
|
}
|
|
822
1504
|
|
|
823
1505
|
// src/index.ts
|
|
824
|
-
var
|
|
1506
|
+
var import_devtools_core12 = require("@angelitosystems/devtools-core");
|
|
825
1507
|
// Annotate the CommonJS export names for ESM import in node:
|
|
826
1508
|
0 && (module.exports = {
|
|
827
1509
|
NestDevTools,
|