@angelitosystems/nest-devtools 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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 INTERESTING_HEADERS = [
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
- headers: this.collectHeaders(req),
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
- collectHeaders(req) {
223
+ collectRequestHeaders(req) {
177
224
  const out = {};
178
- for (const key of INTERESTING_HEADERS) {
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
- return { name, type };
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.ctx = ctx;
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
- ctx;
572
- /** No-op for now. Returns a no-op cleanup. */
742
+ /** Attach gateway hooks. Returns cleanup. */
573
743
  attach() {
574
- void this.ctx;
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.ctx = ctx;
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
- /** No-op for now. Returns a no-op cleanup. */
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
- void this.ctx;
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
@@ -659,7 +1347,9 @@ function printConnectionStatus(state, dashboard) {
659
1347
  return;
660
1348
  }
661
1349
  writeOriginalConsole("log", ` ${paint(YELLOW, "\u26A0")} No se pudo conectar al dashboard todav\xEDa.`);
662
- writeOriginalConsole("log", ` ${paint(DIM, "Lev\xE1ntalo en otra terminal con:")} ${paint(BOLD, "npx nest-devtools start")}`);
1350
+ writeOriginalConsole("log", ` ${paint(DIM, "\xBFA\xFAn no lo instalaste? \u2192")} ${paint(BOLD, "bun add -g @angelitosystems/nest-devtools-cli")}`);
1351
+ writeOriginalConsole("log", ` ${paint(DIM, "Luego, en otra terminal: \u2192")} ${paint(BOLD, "nest-devtools start")}`);
1352
+ writeOriginalConsole("log", ` ${paint(DIM, "(sin instalar, una sola vez: npx -p @angelitosystems/nest-devtools-cli nest-devtools start)")}`);
663
1353
  writeOriginalConsole("log", ` ${paint(DIM, "Reintentando en segundo plano \u2014 se conectar\xE1 solo cuando el dashboard est\xE9 arriba.")}`);
664
1354
  }
665
1355
 
@@ -723,6 +1413,7 @@ var NestDevTools = class {
723
1413
  if (config.capture.database) {
724
1414
  registerCleanup(new DatabaseInstrumentation({ config, projectInfo }).attach());
725
1415
  }
1416
+ registerCleanup(new QueueEventInstrumentation({ config, projectInfo }).attach());
726
1417
  printStartupBanner({
727
1418
  endpoint: config.server,
728
1419
  dashboard: dashboardUrl(config.server),
@@ -790,11 +1481,11 @@ function dashboardUrl(server) {
790
1481
  }
791
1482
 
792
1483
  // src/index.ts
793
- import { requestContext as requestContext5 } from "@angelitosystems/devtools-core";
1484
+ import { requestContext as requestContext8 } from "@angelitosystems/devtools-core";
794
1485
  export {
795
1486
  NestDevTools,
796
1487
  captureError,
797
1488
  devtools,
798
- requestContext5 as requestContext
1489
+ requestContext8 as requestContext
799
1490
  };
800
1491
  //# sourceMappingURL=index.js.map