@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.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/sdk.ts
|
|
2
9
|
import { hostname } from "os";
|
|
3
10
|
import { devtools as coreDevtools2, resolveConfig } from "@angelitosystems/devtools-core";
|
|
@@ -16,14 +23,37 @@ function emit(event, payload) {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
// src/instrumentation/timeline.ts
|
|
26
|
+
function startSpan(spans, layer, label, options) {
|
|
27
|
+
const span = {
|
|
28
|
+
layer,
|
|
29
|
+
label,
|
|
30
|
+
duration: 0,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
status: options?.status ?? "ok",
|
|
33
|
+
...options?.detail !== void 0 ? { detail: options.detail } : {},
|
|
34
|
+
...options?.source !== void 0 ? { source: options.source } : {}
|
|
35
|
+
};
|
|
36
|
+
spans.push(span);
|
|
37
|
+
return span;
|
|
38
|
+
}
|
|
39
|
+
function endSpan(span, status) {
|
|
40
|
+
span.duration = Math.max(0, Date.now() - span.startedAt);
|
|
41
|
+
if (status) span.status = status;
|
|
42
|
+
}
|
|
19
43
|
var recorders = /* @__PURE__ */ new Map();
|
|
20
44
|
function trackSpans(requestId, spans) {
|
|
21
45
|
recorders.set(requestId, spans);
|
|
22
46
|
return () => recorders.delete(requestId);
|
|
23
47
|
}
|
|
48
|
+
function recordSpan(requestId, layer, label, options) {
|
|
49
|
+
const spans = recorders.get(requestId);
|
|
50
|
+
if (!spans) return;
|
|
51
|
+
const span = startSpan(spans, layer, label, options);
|
|
52
|
+
endSpan(span, options?.status);
|
|
53
|
+
}
|
|
24
54
|
|
|
25
55
|
// src/instrumentation/http.ts
|
|
26
|
-
var
|
|
56
|
+
var REQUEST_HEADERS = [
|
|
27
57
|
"content-type",
|
|
28
58
|
"content-length",
|
|
29
59
|
"accept",
|
|
@@ -31,7 +61,16 @@ var INTERESTING_HEADERS = [
|
|
|
31
61
|
"referer",
|
|
32
62
|
"user-agent",
|
|
33
63
|
"x-forwarded-for",
|
|
34
|
-
"x-request-id"
|
|
64
|
+
"x-request-id",
|
|
65
|
+
"x-forwarded-proto"
|
|
66
|
+
];
|
|
67
|
+
var RESPONSE_HEADERS = [
|
|
68
|
+
"content-type",
|
|
69
|
+
"content-length",
|
|
70
|
+
"location",
|
|
71
|
+
"set-cookie",
|
|
72
|
+
"x-request-id",
|
|
73
|
+
"x-response-time"
|
|
35
74
|
];
|
|
36
75
|
var WRAPPED = /* @__PURE__ */ Symbol("nest-devtools.wrapped");
|
|
37
76
|
var HttpInstrumentation = class {
|
|
@@ -114,7 +153,8 @@ var HttpInstrumentation = class {
|
|
|
114
153
|
projectId: this.ctx.projectInfo.projectId,
|
|
115
154
|
method,
|
|
116
155
|
url,
|
|
117
|
-
|
|
156
|
+
route: routeFromReq(req),
|
|
157
|
+
headers: this.collectRequestHeaders(req),
|
|
118
158
|
query: parseQuery(rawUrl),
|
|
119
159
|
ip: clientIp(req),
|
|
120
160
|
userAgent: asString(req.headers["user-agent"]) || void 0,
|
|
@@ -128,6 +168,7 @@ var HttpInstrumentation = class {
|
|
|
128
168
|
status: "ok"
|
|
129
169
|
};
|
|
130
170
|
spans.push(middlewareSpan);
|
|
171
|
+
const capturedBody = this.ctx.config.capture.requests && this.ctx.config.maxPayloadBytes > 0 ? captureRequestPreview(req, this.redactor) : void 0;
|
|
131
172
|
const onFinished = () => {
|
|
132
173
|
const finishedAt = Date.now();
|
|
133
174
|
middlewareSpan.duration = Math.max(0, finishedAt - middlewareSpan.startedAt);
|
|
@@ -137,20 +178,26 @@ var HttpInstrumentation = class {
|
|
|
137
178
|
label: "response",
|
|
138
179
|
duration: 0,
|
|
139
180
|
startedAt: finishedAt,
|
|
140
|
-
status: "ok"
|
|
181
|
+
status: res.statusCode >= 400 ? "error" : "ok"
|
|
141
182
|
};
|
|
142
183
|
spans.push(responseSpan);
|
|
184
|
+
const responseHeaders = this.collectResponseHeaders(res);
|
|
185
|
+
const responsePreview = this.ctx.config.capture.requests && this.ctx.config.maxPayloadBytes > 0 ? captureResponsePreview(res, this.redactor) : void 0;
|
|
143
186
|
const errored = res.statusCode >= 500;
|
|
144
187
|
const completed = {
|
|
145
188
|
requestId,
|
|
146
189
|
projectId: this.ctx.projectInfo.projectId,
|
|
147
190
|
method,
|
|
148
191
|
url,
|
|
192
|
+
route: routeFromReq(req),
|
|
149
193
|
statusCode: res.statusCode,
|
|
150
194
|
duration: finishedAt - startedAt,
|
|
151
195
|
startedAt,
|
|
152
196
|
timeline: spans,
|
|
153
197
|
query: parseQuery(rawUrl),
|
|
198
|
+
headers: responseHeaders,
|
|
199
|
+
responsePreview,
|
|
200
|
+
requestBody: capturedBody,
|
|
154
201
|
errored
|
|
155
202
|
};
|
|
156
203
|
emit("request.completed", completed);
|
|
@@ -161,7 +208,7 @@ var HttpInstrumentation = class {
|
|
|
161
208
|
name: "HttpError",
|
|
162
209
|
message: `${method} ${url} failed with status ${res.statusCode}`,
|
|
163
210
|
fingerprint: httpErrorFingerprint(method, url, res.statusCode),
|
|
164
|
-
request: { method, url, statusCode: res.statusCode },
|
|
211
|
+
request: { method, url: String(url), statusCode: res.statusCode },
|
|
165
212
|
timestamp: finishedAt
|
|
166
213
|
});
|
|
167
214
|
}
|
|
@@ -173,15 +220,30 @@ var HttpInstrumentation = class {
|
|
|
173
220
|
next();
|
|
174
221
|
});
|
|
175
222
|
}
|
|
176
|
-
|
|
223
|
+
collectRequestHeaders(req) {
|
|
177
224
|
const out = {};
|
|
178
|
-
for (const key of
|
|
225
|
+
for (const key of REQUEST_HEADERS) {
|
|
179
226
|
const value = req.headers[key];
|
|
180
227
|
if (value === void 0) continue;
|
|
181
228
|
out[key] = this.redactor.isSensitive(key) ? "[REDACTED]" : asString(value).slice(0, 200);
|
|
182
229
|
}
|
|
183
230
|
return out;
|
|
184
231
|
}
|
|
232
|
+
collectResponseHeaders(res) {
|
|
233
|
+
const out = {};
|
|
234
|
+
const headers = res._headers;
|
|
235
|
+
if (!headers || typeof headers !== "object") return out;
|
|
236
|
+
for (const rawKey of Object.keys(headers)) {
|
|
237
|
+
const key = rawKey.toLowerCase();
|
|
238
|
+
const ok = RESPONSE_HEADERS.some((h) => h.toLowerCase() === key);
|
|
239
|
+
if (!ok) continue;
|
|
240
|
+
const raw = headers[rawKey] ?? "";
|
|
241
|
+
const value = String(raw);
|
|
242
|
+
if (value === "") continue;
|
|
243
|
+
out[key] = this.redactor.isSensitive(key) ? "[REDACTED]" : value.slice(0, 200);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
185
247
|
};
|
|
186
248
|
function asString(value) {
|
|
187
249
|
if (Array.isArray(value)) return value.join(", ");
|
|
@@ -214,6 +276,54 @@ function clientIp(req) {
|
|
|
214
276
|
if (typeof forwarded === "string") return forwarded.split(",")[0]?.trim();
|
|
215
277
|
return req.socket?.remoteAddress ?? void 0;
|
|
216
278
|
}
|
|
279
|
+
function routeFromReq(req) {
|
|
280
|
+
try {
|
|
281
|
+
const http = req;
|
|
282
|
+
const out = {
|
|
283
|
+
requestId: "",
|
|
284
|
+
projectId: "",
|
|
285
|
+
method: "",
|
|
286
|
+
url: "",
|
|
287
|
+
route: void 0,
|
|
288
|
+
headers: {},
|
|
289
|
+
query: {},
|
|
290
|
+
ip: void 0,
|
|
291
|
+
userAgent: void 0,
|
|
292
|
+
startedAt: 0
|
|
293
|
+
};
|
|
294
|
+
const routeRaw = req.route;
|
|
295
|
+
if (routeRaw && typeof routeRaw === "object" && routeRaw !== null) {
|
|
296
|
+
const route = routeRaw;
|
|
297
|
+
const name = route.name;
|
|
298
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
299
|
+
}
|
|
300
|
+
const path = req.baseUrl ?? req.path;
|
|
301
|
+
if (typeof path === "string" && path.length > 0) return path;
|
|
302
|
+
return void 0;
|
|
303
|
+
} catch {
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function captureRequestPreview(req, redactor) {
|
|
308
|
+
try {
|
|
309
|
+
const body = req.body;
|
|
310
|
+
if (body === void 0 || body === null) return void 0;
|
|
311
|
+
return redactor.redact(body);
|
|
312
|
+
} catch {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function captureResponsePreview(res, redactor) {
|
|
317
|
+
try {
|
|
318
|
+
const resLike = res;
|
|
319
|
+
const body = resLike.body;
|
|
320
|
+
if (body === void 0 || body === null) return void 0;
|
|
321
|
+
if (typeof body === "string") return redactor.redactString(body);
|
|
322
|
+
return redactor.serialize(body);
|
|
323
|
+
} catch {
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
217
327
|
|
|
218
328
|
// src/instrumentation/console.ts
|
|
219
329
|
import { Console } from "console";
|
|
@@ -547,7 +657,8 @@ var AppExplorer = class {
|
|
|
547
657
|
const w = wrapper;
|
|
548
658
|
const name = w?.metatype?.name ?? (typeof id === "string" ? id.replace(/^[A-Z_0-9]+:/, "") : "unknown");
|
|
549
659
|
const type = classify(name, w?.instance);
|
|
550
|
-
|
|
660
|
+
const routes = type === "controller" ? extractControllerRoutes(w?.metatype) : void 0;
|
|
661
|
+
return { name, type, routes };
|
|
551
662
|
}
|
|
552
663
|
};
|
|
553
664
|
function classify(name, instance) {
|
|
@@ -562,33 +673,610 @@ function classify(name, instance) {
|
|
|
562
673
|
if (instance && typeof instance.catch === "function") return "filter";
|
|
563
674
|
return "provider";
|
|
564
675
|
}
|
|
676
|
+
function extractControllerRoutes(metatype) {
|
|
677
|
+
if (!metatype) return [];
|
|
678
|
+
try {
|
|
679
|
+
const proto = metatype.prototype;
|
|
680
|
+
if (!proto) return [];
|
|
681
|
+
const props = Object.getOwnPropertyNames(proto).filter((name) => name !== "constructor");
|
|
682
|
+
const routes = [];
|
|
683
|
+
for (const key of props) {
|
|
684
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
685
|
+
if (!desc?.value) continue;
|
|
686
|
+
const value = desc.value;
|
|
687
|
+
const routesForMethod = gatherHttpRoutes(value);
|
|
688
|
+
for (const r of routesForMethod) {
|
|
689
|
+
if (typeof r === "string" && r.length > 0) routes.push(r);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return routes;
|
|
693
|
+
} catch {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function gatherHttpRoutes(value) {
|
|
698
|
+
try {
|
|
699
|
+
const decorators = Reflect.getMetadata?.("design:http:method", {}, void 0, void 0) ?? [];
|
|
700
|
+
const out = [];
|
|
701
|
+
if (decorators.length > 0) {
|
|
702
|
+
for (const d of decorators) {
|
|
703
|
+
if (d && typeof d === "object" && d.method && d.path) {
|
|
704
|
+
out.push(`${String(d.method).toUpperCase()} ${String(d.path)}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (typeof value === "function") {
|
|
709
|
+
const fn = value;
|
|
710
|
+
const path = fn.PATH ?? void 0;
|
|
711
|
+
const method = fn.METHOD ?? void 0;
|
|
712
|
+
if (typeof path === "string" && typeof method === "string") {
|
|
713
|
+
out.push(`${method.toUpperCase()} ${path}`);
|
|
714
|
+
} else if (typeof path === "string") {
|
|
715
|
+
out.push(path);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
} catch {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
}
|
|
565
723
|
|
|
566
724
|
// src/instrumentation/websockets.ts
|
|
725
|
+
import { requestContext as requestContext5 } from "@angelitosystems/devtools-core";
|
|
726
|
+
import { Redactor as Redactor4, randomId as randomId2 } from "@angelitosystems/devtools-protocol";
|
|
567
727
|
var WebsocketInstrumentation = class {
|
|
728
|
+
redactor;
|
|
729
|
+
config;
|
|
730
|
+
projectId;
|
|
731
|
+
ctxRaw;
|
|
568
732
|
constructor(ctx) {
|
|
569
|
-
this.
|
|
733
|
+
this.ctxRaw = ctx;
|
|
734
|
+
this.config = ctx.config;
|
|
735
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
736
|
+
this.redactor = new Redactor4({
|
|
737
|
+
redact: this.config.redact,
|
|
738
|
+
allow: this.config.allow,
|
|
739
|
+
maxBytes: this.config.maxPayloadBytes
|
|
740
|
+
});
|
|
570
741
|
}
|
|
571
|
-
|
|
572
|
-
/** No-op for now. Returns a no-op cleanup. */
|
|
742
|
+
/** Attach gateway hooks. Returns cleanup. */
|
|
573
743
|
attach() {
|
|
574
|
-
|
|
575
|
-
return () =>
|
|
744
|
+
const cleanup = [];
|
|
745
|
+
if (!this.config.capture.websockets) return () => cleanup.forEach((fn) => fn());
|
|
746
|
+
const gateways = this.detectGateways();
|
|
747
|
+
for (const gw of gateways) {
|
|
748
|
+
cleanup.push(this.wrapGateway(gw));
|
|
749
|
+
}
|
|
750
|
+
return () => cleanup.forEach((fn) => fn());
|
|
751
|
+
}
|
|
752
|
+
detectGateways() {
|
|
753
|
+
try {
|
|
754
|
+
const app = this.getApp();
|
|
755
|
+
if (!app) return [];
|
|
756
|
+
const container = app.container;
|
|
757
|
+
if (!container) return [];
|
|
758
|
+
const modules = container.getModules?.() ?? /* @__PURE__ */ new Map();
|
|
759
|
+
const out = [];
|
|
760
|
+
for (const [id, node] of modules) {
|
|
761
|
+
const providers = node.providers;
|
|
762
|
+
if (!providers || !(providers instanceof Map)) continue;
|
|
763
|
+
for (const [key, wrapper] of providers) {
|
|
764
|
+
const w = wrapper;
|
|
765
|
+
if (!w || !w.instance) continue;
|
|
766
|
+
const subtype = w.subtype ?? (node.subtype ?? null);
|
|
767
|
+
if (subtype === "gateway" || w.instance && w.instance.constructor?.name?.endsWith("Gateway")) {
|
|
768
|
+
out.push({ name: w.name ?? key, instance: w.instance });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
} catch {
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
wrapGateway(gw) {
|
|
778
|
+
const instance = gw.instance;
|
|
779
|
+
if (!instance || typeof instance !== "object") return () => {
|
|
576
780
|
};
|
|
781
|
+
const hooks = [];
|
|
782
|
+
const methods = [
|
|
783
|
+
"handleConnection",
|
|
784
|
+
"handleDisconnect",
|
|
785
|
+
"handleEvent",
|
|
786
|
+
"handleMessage",
|
|
787
|
+
"afterInit",
|
|
788
|
+
"beforeHandle"
|
|
789
|
+
];
|
|
790
|
+
for (const method of methods) {
|
|
791
|
+
if (typeof instance[method] !== "function") continue;
|
|
792
|
+
const orig = instance[method];
|
|
793
|
+
const wrapped = this.buildGatewayWrapper(instance, method, orig);
|
|
794
|
+
instance[method] = wrapped;
|
|
795
|
+
hooks.push(() => {
|
|
796
|
+
instance[method] = orig;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return () => hooks.forEach((fn) => fn());
|
|
800
|
+
}
|
|
801
|
+
buildGatewayWrapper(instance, method, original) {
|
|
802
|
+
return async (...args) => {
|
|
803
|
+
const started = Date.now();
|
|
804
|
+
const requestId = requestContext5().requestId() ?? randomId2("ws");
|
|
805
|
+
if (method === "handleConnection" || method === "afterInit") {
|
|
806
|
+
emit("websocket.connected", {
|
|
807
|
+
projectId: this.projectId,
|
|
808
|
+
gateway: instance.constructor?.name ?? this.estimateGatewayName(instance),
|
|
809
|
+
namespace: this.estimateNamespace(args),
|
|
810
|
+
connections: this.countConnections(instance),
|
|
811
|
+
timestamp: Date.now()
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
815
|
+
const payload = this.extractPayload(args);
|
|
816
|
+
recordSpan(requestId, "websocket", `ws.receive:${this.estimateEvent(args)}`, {
|
|
817
|
+
detail: `gateway=${instance.constructor?.name} payload=${this.redactor.serialize(payload)}`
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const result = await Promise.resolve(original.call(instance, ...args));
|
|
822
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
823
|
+
const payload = this.extractPayload(args);
|
|
824
|
+
recordSpan(requestId, "websocket", `ws.send:${this.estimateEvent(args)}`, {
|
|
825
|
+
detail: `gateway=${instance.constructor?.name} payload=${this.redactor.serialize(payload)}`,
|
|
826
|
+
status: "ok"
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
} catch (err) {
|
|
831
|
+
if (method === "handleMessage" || method === "handleEvent") {
|
|
832
|
+
recordSpan(requestId, "websocket", `ws.send:${this.estimateEvent(args)}`, {
|
|
833
|
+
detail: `gateway=${instance.constructor?.name} error=${this.redactor.serialize(err)}`,
|
|
834
|
+
status: "error"
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
throw err;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
extractPayload(args) {
|
|
842
|
+
for (const arg of args) {
|
|
843
|
+
if (arg && typeof arg === "object" && !Buffer.isBuffer(arg) && !ArrayBuffer.isView(arg)) {
|
|
844
|
+
return arg;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return args[0];
|
|
848
|
+
}
|
|
849
|
+
estimateEvent(args) {
|
|
850
|
+
const payload = this.extractPayload(args);
|
|
851
|
+
if (!payload || typeof payload !== "object") return "message";
|
|
852
|
+
return payload.event ?? payload.type ?? "message";
|
|
853
|
+
}
|
|
854
|
+
estimateNamespace(args) {
|
|
855
|
+
if (args.length > 0) {
|
|
856
|
+
const first = args[0];
|
|
857
|
+
if (first && typeof first === "object" && first.namespace) return first.namespace;
|
|
858
|
+
}
|
|
859
|
+
return "/";
|
|
860
|
+
}
|
|
861
|
+
countConnections(instance) {
|
|
862
|
+
try {
|
|
863
|
+
if (instance.connections && typeof instance.connections === "function") return instance.connections();
|
|
864
|
+
if (instance.clients && typeof instance.clients === "function") return instance.clients().size ?? 0;
|
|
865
|
+
return 0;
|
|
866
|
+
} catch {
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
estimateGatewayName(instance) {
|
|
871
|
+
return instance.constructor?.name ?? "Gateway";
|
|
872
|
+
}
|
|
873
|
+
getApp() {
|
|
874
|
+
try {
|
|
875
|
+
const ctx = this.ctxRaw;
|
|
876
|
+
if (ctx.getHttpAdapter) return ctx;
|
|
877
|
+
if (ctx.app) return ctx.app;
|
|
878
|
+
return null;
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
577
882
|
}
|
|
578
883
|
};
|
|
579
884
|
|
|
580
885
|
// src/instrumentation/database.ts
|
|
886
|
+
import { requestContext as requestContext6 } from "@angelitosystems/devtools-core";
|
|
887
|
+
import { Redactor as Redactor5, randomId as randomId3 } from "@angelitosystems/devtools-protocol";
|
|
581
888
|
var DatabaseInstrumentation = class {
|
|
889
|
+
redactor;
|
|
890
|
+
config;
|
|
891
|
+
projectId;
|
|
892
|
+
ctxRaw;
|
|
582
893
|
constructor(ctx) {
|
|
583
|
-
this.
|
|
894
|
+
this.ctxRaw = ctx;
|
|
895
|
+
this.config = ctx.config;
|
|
896
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
897
|
+
this.redactor = new Redactor5({
|
|
898
|
+
redact: this.config.redact,
|
|
899
|
+
allow: this.config.allow,
|
|
900
|
+
maxBytes: this.config.maxPayloadBytes
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/** Attach database hooks for detected clients. Returns cleanup. */
|
|
904
|
+
attach() {
|
|
905
|
+
const cleanup = [];
|
|
906
|
+
if (!this.config.capture.database) return () => cleanup.forEach((fn) => fn());
|
|
907
|
+
if (this.tryAttachTypeOrm()) cleanup.push(this.detachTypeOrm());
|
|
908
|
+
if (this.tryAttachPrisma()) cleanup.push(this.detachPrisma());
|
|
909
|
+
if (this.tryAttachMongoose()) cleanup.push(this.detachMongoose());
|
|
910
|
+
return () => cleanup.forEach((fn) => fn());
|
|
584
911
|
}
|
|
912
|
+
tryAttachTypeOrm() {
|
|
913
|
+
try {
|
|
914
|
+
const app = this.getApp();
|
|
915
|
+
if (!app) return false;
|
|
916
|
+
const dataSource = this.resolveTypeOrmDataSource(app);
|
|
917
|
+
if (!dataSource) return false;
|
|
918
|
+
const listener = {
|
|
919
|
+
beforeQuery: (query, parameters) => {
|
|
920
|
+
this.startQuery(query, parameters);
|
|
921
|
+
},
|
|
922
|
+
afterQuery: (query, parameters, durationMs) => {
|
|
923
|
+
this.endQuery(query, parameters, durationMs);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
const sub = dataSource.subscriber;
|
|
927
|
+
if (sub) {
|
|
928
|
+
const before = sub.beforeQuery;
|
|
929
|
+
const after = sub.afterQuery;
|
|
930
|
+
sub.beforeQuery = (q, p) => {
|
|
931
|
+
try {
|
|
932
|
+
before?.(q, p);
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
listener.beforeQuery(q, p);
|
|
936
|
+
};
|
|
937
|
+
sub.afterQuery = (q, p, d) => {
|
|
938
|
+
try {
|
|
939
|
+
after?.(q, p, d);
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
listener.afterQuery(q, p, d);
|
|
943
|
+
};
|
|
944
|
+
} else {
|
|
945
|
+
dataSource.subscriber = listener;
|
|
946
|
+
}
|
|
947
|
+
return true;
|
|
948
|
+
} catch {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
detachTypeOrm() {
|
|
953
|
+
return () => {
|
|
954
|
+
try {
|
|
955
|
+
const app = this.getApp();
|
|
956
|
+
if (!app) return;
|
|
957
|
+
const ds = this.resolveTypeOrmDataSource(app);
|
|
958
|
+
if (!ds || !ds.subscriber) return;
|
|
959
|
+
ds.subscriber = null;
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
tryAttachPrisma() {
|
|
965
|
+
try {
|
|
966
|
+
const app = this.getApp();
|
|
967
|
+
if (!app) return false;
|
|
968
|
+
const prisma = this.resolvePrismaClient(app);
|
|
969
|
+
if (!prisma) return false;
|
|
970
|
+
const original = prisma.$queryRaw ?? prisma.$executeRaw ?? null;
|
|
971
|
+
if (!original) return false;
|
|
972
|
+
const wrapped = (query, parameters) => {
|
|
973
|
+
const started = Date.now();
|
|
974
|
+
this.startQuery(String(query), parameters ? [parameters] : []);
|
|
975
|
+
try {
|
|
976
|
+
const result = original.call(prisma, query, parameters);
|
|
977
|
+
if (result instanceof Promise) {
|
|
978
|
+
return result.then((r) => {
|
|
979
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started);
|
|
980
|
+
return r;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started);
|
|
984
|
+
return result;
|
|
985
|
+
} catch (err) {
|
|
986
|
+
this.endQuery(String(query), parameters ? [parameters] : [], Date.now() - started, err);
|
|
987
|
+
throw err;
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
prisma.$queryRaw = wrapped;
|
|
991
|
+
prisma.$executeRaw = wrapped;
|
|
992
|
+
return true;
|
|
993
|
+
} catch {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
detachPrisma() {
|
|
998
|
+
return () => {
|
|
999
|
+
try {
|
|
1000
|
+
const app = this.getApp();
|
|
1001
|
+
if (!app) return;
|
|
1002
|
+
const prisma = this.resolvePrismaClient(app);
|
|
1003
|
+
if (!prisma) return;
|
|
1004
|
+
if (typeof prisma.$extends === "function") prisma.$extends(prisma);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
tryAttachMongoose() {
|
|
1010
|
+
try {
|
|
1011
|
+
const app = this.getApp();
|
|
1012
|
+
if (!app) return false;
|
|
1013
|
+
const mongoose = this.resolveMongoose(app);
|
|
1014
|
+
if (!mongoose) return false;
|
|
1015
|
+
const instrumentation = this;
|
|
1016
|
+
const hook = mongoose.plugin((schema, _name) => {
|
|
1017
|
+
if (!schema || typeof schema !== "object") return;
|
|
1018
|
+
const hooks = schema.pre ?? null;
|
|
1019
|
+
if (typeof hooks !== "function") return;
|
|
1020
|
+
const bound = hooks.bind(schema);
|
|
1021
|
+
if (!bound) return;
|
|
1022
|
+
const originalPre = bound;
|
|
1023
|
+
schema.pre = function(method, fn) {
|
|
1024
|
+
if (method === "find" || method === "findOne" || method === "findById" || method === "aggregate" || method === "countDocuments" || method === "count") {
|
|
1025
|
+
const wrapped = async function(...args) {
|
|
1026
|
+
const started = Date.now();
|
|
1027
|
+
const sql = `[Mongoose:${method}]`;
|
|
1028
|
+
instrumentation.startQuery(sql, args);
|
|
1029
|
+
try {
|
|
1030
|
+
const handler = fn;
|
|
1031
|
+
if (!handler) return;
|
|
1032
|
+
const result = await handler.apply(this, args);
|
|
1033
|
+
instrumentation.endQuery(sql, args, Date.now() - started);
|
|
1034
|
+
return result;
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
instrumentation.endQuery(sql, args, Date.now() - started, err);
|
|
1037
|
+
throw err;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
originalPre(method, wrapped);
|
|
1041
|
+
} else {
|
|
1042
|
+
originalPre(method, fn);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
if (typeof mongoose.plugin === "function") mongoose.plugin(hook);
|
|
1047
|
+
return true;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
detachMongoose() {
|
|
1053
|
+
return () => {
|
|
1054
|
+
try {
|
|
1055
|
+
const app = this.getApp();
|
|
1056
|
+
if (!app) return;
|
|
1057
|
+
const mongoose = this.resolveMongoose(app);
|
|
1058
|
+
if (!mongoose || typeof mongoose.plugin !== "function") return;
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
startQuery(sql, parameters) {
|
|
1064
|
+
if (!this.config.capture.database) return;
|
|
1065
|
+
const requestId = requestContext6().requestId() ?? randomId3("db");
|
|
1066
|
+
recordSpan(requestId, "database", sql.slice(0, 120), {
|
|
1067
|
+
detail: `provider=${this.providerLabel()} parameters=${this.redactor.serialize(parameters)}`
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
endQuery(sql, parameters, durationMs, error) {
|
|
1071
|
+
if (!this.config.capture.database) return;
|
|
1072
|
+
const requestId = requestContext6().requestId() ?? randomId3("db");
|
|
1073
|
+
const payload = {
|
|
1074
|
+
requestId,
|
|
1075
|
+
projectId: this.projectId,
|
|
1076
|
+
provider: this.providerLabel(),
|
|
1077
|
+
sql: sql.slice(0, 2e3),
|
|
1078
|
+
duration: durationMs,
|
|
1079
|
+
parameters: this.redactor.redact(parameters),
|
|
1080
|
+
timestamp: Date.now()
|
|
1081
|
+
};
|
|
1082
|
+
emit("query.executed", payload);
|
|
1083
|
+
}
|
|
1084
|
+
providerLabel() {
|
|
1085
|
+
if (this.isTypeOrm()) return "typeorm";
|
|
1086
|
+
if (this.isPrisma()) return "prisma";
|
|
1087
|
+
if (this.isMongoose()) return "mongoose";
|
|
1088
|
+
return "other";
|
|
1089
|
+
}
|
|
1090
|
+
isTypeOrm() {
|
|
1091
|
+
return !!this.resolveTypeOrmDataSource(this.getApp());
|
|
1092
|
+
}
|
|
1093
|
+
isPrisma() {
|
|
1094
|
+
return !!this.resolvePrismaClient(this.getApp());
|
|
1095
|
+
}
|
|
1096
|
+
isMongoose() {
|
|
1097
|
+
return !!this.resolveMongoose(this.getApp());
|
|
1098
|
+
}
|
|
1099
|
+
getApp() {
|
|
1100
|
+
try {
|
|
1101
|
+
const ctx = this.ctxRaw;
|
|
1102
|
+
if (ctx && typeof ctx === "object" && ctx.app) return ctx.app;
|
|
1103
|
+
if (ctx && typeof ctx === "object" && ctx.getHttpAdapter) {
|
|
1104
|
+
return ctx;
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
resolveTypeOrmDataSource(app) {
|
|
1112
|
+
try {
|
|
1113
|
+
const dataSource = app.get(__require("@nestjs/typeorm").TypeOrmModule)?.options?.DataSource ?? null;
|
|
1114
|
+
if (dataSource && typeof dataSource.query === "function") return dataSource;
|
|
1115
|
+
return null;
|
|
1116
|
+
} catch {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
resolvePrismaClient(app) {
|
|
1121
|
+
try {
|
|
1122
|
+
const prisma = app.get("PrismaService") ?? app.get("PrismaClient") ?? app.get("prisma") ?? null;
|
|
1123
|
+
if (prisma && typeof prisma.$queryRaw === "function") return prisma;
|
|
1124
|
+
return null;
|
|
1125
|
+
} catch {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
resolveMongoose(app) {
|
|
1130
|
+
try {
|
|
1131
|
+
const mongoose = app.get("MongooseService") ?? app.get("Mongoose") ?? app.get("mongoose") ?? null;
|
|
1132
|
+
if (mongoose && typeof mongoose.plugin === "function") return mongoose;
|
|
1133
|
+
return null;
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// src/instrumentation/queue.ts
|
|
1141
|
+
import { requestContext as requestContext7 } from "@angelitosystems/devtools-core";
|
|
1142
|
+
import { Redactor as Redactor6, randomId as randomId4 } from "@angelitosystems/devtools-protocol";
|
|
1143
|
+
import { resolveSourceLocation as resolveSourceLocation4 } from "@angelitosystems/devtools-core";
|
|
1144
|
+
var QueueEventInstrumentation = class _QueueEventInstrumentation {
|
|
1145
|
+
redactor;
|
|
1146
|
+
config;
|
|
1147
|
+
projectId;
|
|
585
1148
|
ctx;
|
|
586
|
-
|
|
1149
|
+
constructor(ctx) {
|
|
1150
|
+
this.ctx = ctx;
|
|
1151
|
+
this.config = ctx.config;
|
|
1152
|
+
this.projectId = ctx.projectInfo.projectId;
|
|
1153
|
+
this.redactor = new Redactor6({
|
|
1154
|
+
redact: this.config.redact,
|
|
1155
|
+
allow: this.config.allow,
|
|
1156
|
+
maxBytes: this.config.maxPayloadBytes
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
/** Attach queue hooks if a supported client is detected. Returns cleanup. */
|
|
587
1160
|
attach() {
|
|
588
|
-
|
|
1161
|
+
const cleanup = [];
|
|
1162
|
+
if (!this.config.capture.requests && !this.config.capture.logs && !this.config.capture.errors) {
|
|
1163
|
+
return () => cleanup.forEach((fn) => fn());
|
|
1164
|
+
}
|
|
1165
|
+
if (this.tryAttachBull()) cleanup.push(this.detachBull());
|
|
1166
|
+
return () => cleanup.forEach((fn) => fn());
|
|
1167
|
+
}
|
|
1168
|
+
/** Register a queue span correlated with the active request, if any. */
|
|
1169
|
+
static recordJobSpan(queueName, jobId, label, options) {
|
|
1170
|
+
const requestId = requestContext7().requestId() ?? randomId4("queue");
|
|
1171
|
+
recordSpan(requestId, "queue", `${queueName}:${label} (${jobId})`, {
|
|
1172
|
+
detail: `queue=${queueName} job=${jobId}`,
|
|
1173
|
+
source: options?.source,
|
|
1174
|
+
status: options?.status
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
/** Emit a log for queue events, correlated with the active request if any. */
|
|
1178
|
+
static emitJobLog(queueName, jobId, level, message, options) {
|
|
1179
|
+
const requestId = requestContext7().requestId();
|
|
1180
|
+
emit("log.created", {
|
|
1181
|
+
requestId,
|
|
1182
|
+
projectId: void 0,
|
|
1183
|
+
level,
|
|
1184
|
+
message: `[${queueName}] ${message} (${jobId})`,
|
|
1185
|
+
source: options?.source,
|
|
1186
|
+
context: queueName,
|
|
1187
|
+
processId: process.pid,
|
|
1188
|
+
timestamp: Date.now()
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
tryAttachBull() {
|
|
1192
|
+
try {
|
|
1193
|
+
const app = this.getApp();
|
|
1194
|
+
if (!app) return false;
|
|
1195
|
+
const bull = this.resolveBull(app);
|
|
1196
|
+
if (!bull) return false;
|
|
1197
|
+
const origProcess = bull.prototype?.process ?? bull.process;
|
|
1198
|
+
if (typeof origProcess === "function") {
|
|
1199
|
+
const wrapped = this.wrapProcess(origProcess, bull);
|
|
1200
|
+
if (bull.prototype) bull.prototype.process = wrapped;
|
|
1201
|
+
bull.process = wrapped;
|
|
1202
|
+
}
|
|
1203
|
+
return true;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
detachBull() {
|
|
589
1209
|
return () => {
|
|
1210
|
+
try {
|
|
1211
|
+
const app = this.getApp();
|
|
1212
|
+
if (!app) return;
|
|
1213
|
+
const bull = this.resolveBull(app);
|
|
1214
|
+
if (!bull) return;
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
wrapProcess(original, bull) {
|
|
1220
|
+
return async function(...args) {
|
|
1221
|
+
const handler = args[0];
|
|
1222
|
+
if (!handler || typeof handler !== "function") return original.apply(this, args);
|
|
1223
|
+
const wrapped = async (...handlerArgs) => {
|
|
1224
|
+
const job = handlerArgs[0];
|
|
1225
|
+
if (!job || typeof job !== "object") return handler.apply(this, handlerArgs);
|
|
1226
|
+
const jobId = job.id ?? job.data?.jobId ?? randomId4("job");
|
|
1227
|
+
const queueName = job.queueName ?? this.estimateQueueName(bull);
|
|
1228
|
+
const started = Date.now();
|
|
1229
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", { status: "ok" });
|
|
1230
|
+
try {
|
|
1231
|
+
const result = await Promise.resolve(handler.apply(this, handlerArgs));
|
|
1232
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", {
|
|
1233
|
+
status: "ok"
|
|
1234
|
+
});
|
|
1235
|
+
_QueueEventInstrumentation.emitJobLog(queueName, jobId, "info", "job completed", {
|
|
1236
|
+
source: resolveSourceLocation4(new Error(), { skip: 2 })
|
|
1237
|
+
});
|
|
1238
|
+
return result;
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
_QueueEventInstrumentation.recordJobSpan(queueName, jobId, "process", {
|
|
1241
|
+
status: "error"
|
|
1242
|
+
});
|
|
1243
|
+
_QueueEventInstrumentation.emitJobLog(queueName, jobId, "error", String(err), {
|
|
1244
|
+
source: resolveSourceLocation4(err instanceof Error ? err : new Error(), { skip: 2 })
|
|
1245
|
+
});
|
|
1246
|
+
throw err;
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
return original.apply(this, [wrapped, ...args.slice(1)]);
|
|
590
1250
|
};
|
|
591
1251
|
}
|
|
1252
|
+
estimateQueueName(bull) {
|
|
1253
|
+
try {
|
|
1254
|
+
if (bull.name) return bull.name;
|
|
1255
|
+
if (typeof bull === "function") return bull.name;
|
|
1256
|
+
return "queue";
|
|
1257
|
+
} catch {
|
|
1258
|
+
return "queue";
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
resolveBull(app) {
|
|
1262
|
+
try {
|
|
1263
|
+
const bull = app.get("BullService") ?? app.get("Bull") ?? app.get("bull") ?? app.get("Queue") ?? null;
|
|
1264
|
+
if (bull && (typeof bull.process === "function" || typeof bull.prototype?.process === "function")) return bull;
|
|
1265
|
+
return null;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
getApp() {
|
|
1271
|
+
try {
|
|
1272
|
+
const ctx = this.ctx;
|
|
1273
|
+
if (ctx.getHttpAdapter) return ctx;
|
|
1274
|
+
if (ctx.app) return ctx.app;
|
|
1275
|
+
return null;
|
|
1276
|
+
} catch {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
592
1280
|
};
|
|
593
1281
|
|
|
594
1282
|
// src/detect.ts
|
|
@@ -725,6 +1413,7 @@ var NestDevTools = class {
|
|
|
725
1413
|
if (config.capture.database) {
|
|
726
1414
|
registerCleanup(new DatabaseInstrumentation({ config, projectInfo }).attach());
|
|
727
1415
|
}
|
|
1416
|
+
registerCleanup(new QueueEventInstrumentation({ config, projectInfo }).attach());
|
|
728
1417
|
printStartupBanner({
|
|
729
1418
|
endpoint: config.server,
|
|
730
1419
|
dashboard: dashboardUrl(config.server),
|
|
@@ -792,11 +1481,11 @@ function dashboardUrl(server) {
|
|
|
792
1481
|
}
|
|
793
1482
|
|
|
794
1483
|
// src/index.ts
|
|
795
|
-
import { requestContext as
|
|
1484
|
+
import { requestContext as requestContext8 } from "@angelitosystems/devtools-core";
|
|
796
1485
|
export {
|
|
797
1486
|
NestDevTools,
|
|
798
1487
|
captureError,
|
|
799
1488
|
devtools,
|
|
800
|
-
|
|
1489
|
+
requestContext8 as requestContext
|
|
801
1490
|
};
|
|
802
1491
|
//# sourceMappingURL=index.js.map
|