@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.cjs CHANGED
@@ -23,13 +23,13 @@ __export(index_exports, {
23
23
  NestDevTools: () => NestDevTools,
24
24
  captureError: () => captureError,
25
25
  devtools: () => devtools,
26
- requestContext: () => import_devtools_core8.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 import_devtools_core7 = require("@angelitosystems/devtools-core");
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 INTERESTING_HEADERS = [
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
- headers: this.collectHeaders(req),
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
- collectHeaders(req) {
245
+ collectRequestHeaders(req) {
206
246
  const out = {};
207
- for (const key of INTERESTING_HEADERS) {
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
- return { name, type };
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.ctx = ctx;
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
- ctx;
601
- /** No-op for now. Returns a no-op cleanup. */
764
+ /** Attach gateway hooks. Returns cleanup. */
602
765
  attach() {
603
- void this.ctx;
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.ctx = ctx;
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
- /** No-op for now. Returns a no-op cleanup. */
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
- void this.ctx;
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
@@ -688,7 +1369,9 @@ function printConnectionStatus(state, dashboard) {
688
1369
  return;
689
1370
  }
690
1371
  writeOriginalConsole("log", ` ${paint(YELLOW, "\u26A0")} No se pudo conectar al dashboard todav\xEDa.`);
691
- writeOriginalConsole("log", ` ${paint(DIM, "Lev\xE1ntalo en otra terminal con:")} ${paint(BOLD, "npx nest-devtools start")}`);
1372
+ writeOriginalConsole("log", ` ${paint(DIM, "\xBFA\xFAn no lo instalaste? \u2192")} ${paint(BOLD, "bun add -g @angelitosystems/nest-devtools-cli")}`);
1373
+ writeOriginalConsole("log", ` ${paint(DIM, "Luego, en otra terminal: \u2192")} ${paint(BOLD, "nest-devtools start")}`);
1374
+ writeOriginalConsole("log", ` ${paint(DIM, "(sin instalar, una sola vez: npx -p @angelitosystems/nest-devtools-cli nest-devtools start)")}`);
692
1375
  writeOriginalConsole("log", ` ${paint(DIM, "Reintentando en segundo plano \u2014 se conectar\xE1 solo cuando el dashboard est\xE9 arriba.")}`);
693
1376
  }
694
1377
 
@@ -698,7 +1381,7 @@ var NestDevTools = class {
698
1381
  static initialized = false;
699
1382
  /** Initialize DevTools for a NestJS application. One active instance per process. */
700
1383
  static init(app, userConfig) {
701
- const config = (0, import_devtools_core7.resolveConfig)(userConfig);
1384
+ const config = (0, import_devtools_core11.resolveConfig)(userConfig);
702
1385
  const disabled = {
703
1386
  enabled: false,
704
1387
  reason: "disabled-by-config",
@@ -724,7 +1407,7 @@ var NestDevTools = class {
724
1407
  sdkVersion: SDK_VERSION
725
1408
  };
726
1409
  let hasWarnedOffline = false;
727
- import_devtools_core7.devtools.initialize({
1410
+ import_devtools_core11.devtools.initialize({
728
1411
  config,
729
1412
  projectInfo,
730
1413
  registerCleanup,
@@ -752,6 +1435,7 @@ var NestDevTools = class {
752
1435
  if (config.capture.database) {
753
1436
  registerCleanup(new DatabaseInstrumentation({ config, projectInfo }).attach());
754
1437
  }
1438
+ registerCleanup(new QueueEventInstrumentation({ config, projectInfo }).attach());
755
1439
  printStartupBanner({
756
1440
  endpoint: config.server,
757
1441
  dashboard: dashboardUrl(config.server),
@@ -774,7 +1458,7 @@ var NestDevTools = class {
774
1458
  }
775
1459
  }
776
1460
  this.initialized = false;
777
- import_devtools_core7.devtools.shutdown();
1461
+ import_devtools_core11.devtools.shutdown();
778
1462
  }
779
1463
  /** Whether the SDK is currently active in this process. */
780
1464
  static isActive() {
@@ -819,7 +1503,7 @@ function dashboardUrl(server) {
819
1503
  }
820
1504
 
821
1505
  // src/index.ts
822
- var import_devtools_core8 = require("@angelitosystems/devtools-core");
1506
+ var import_devtools_core12 = require("@angelitosystems/devtools-core");
823
1507
  // Annotate the CommonJS export names for ESM import in node:
824
1508
  0 && (module.exports = {
825
1509
  NestDevTools,