autotel-devtools 5.1.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -367,12 +367,17 @@ var DevtoolsServer = class {
367
367
  limits;
368
368
  verbose;
369
369
  _port;
370
+ onData;
370
371
  constructor(options = {}) {
371
372
  this.limits = resolveTelemetryLimits(options);
372
373
  this.verbose = options.verbose ?? false;
373
374
  this._port = options.port ?? 4318;
375
+ this.onData = options.onData;
374
376
  this.httpServer = options.server ?? http.createServer();
375
377
  this.wss = new ws.WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
378
+ this.wss.on("error", (err) => {
379
+ if (this.httpServer.listening) throw err;
380
+ });
376
381
  this.wss.on("connection", (ws) => {
377
382
  this.clients.add(ws);
378
383
  this.log(`Client connected (${this.clients.size} total)`);
@@ -464,6 +469,12 @@ var DevtoolsServer = class {
464
469
  client.send(msg);
465
470
  }
466
471
  }
472
+ if (this.onData) {
473
+ try {
474
+ this.onData(data);
475
+ } catch {
476
+ }
477
+ }
467
478
  }
468
479
  log(message) {
469
480
  if (this.verbose) console.log(`[autotel-devtools] ${message}`);
@@ -517,7 +528,10 @@ function flattenAttributes(attrs) {
517
528
  }
518
529
  function nanoToMs(nano) {
519
530
  if (!nano) return 0;
520
- return Number(BigInt(nano) / 1000000n);
531
+ const ns = BigInt(nano);
532
+ const ms = ns / 1000000n;
533
+ const remNs = ns % 1000000n;
534
+ return Number(ms) + Number(remNs) / 1e6;
521
535
  }
522
536
  var SPAN_KIND_MAP = {
523
537
  0: "INTERNAL",
@@ -555,6 +569,7 @@ function parseOtlpTraces(payload) {
555
569
  const service = String(resourceAttrs["service.name"] || "unknown");
556
570
  const scopeSpans = rs.scopeSpans || [];
557
571
  for (const ss of scopeSpans) {
572
+ const scope = ss.scope?.name ? { name: ss.scope.name, version: ss.scope.version || void 0 } : void 0;
558
573
  for (const span of ss.spans || []) {
559
574
  const traceId = normalizeHexId(span.traceId);
560
575
  if (!traceId) continue;
@@ -579,7 +594,13 @@ function parseOtlpTraces(payload) {
579
594
  name: e.name || "",
580
595
  timestamp: nanoToMs(e.timeUnixNano),
581
596
  attributes: flattenAttributes(e.attributes)
582
- }))
597
+ })),
598
+ links: (span.links || []).map((l) => ({
599
+ traceId: normalizeHexId(l.traceId),
600
+ spanId: normalizeHexId(l.spanId),
601
+ attributes: flattenAttributes(l.attributes)
602
+ })),
603
+ scope
583
604
  };
584
605
  const existing = traceMap.get(traceId);
585
606
  if (existing) {
@@ -1018,43 +1039,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1018
1039
  });
1019
1040
  }
1020
1041
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
1042
+ var DEFAULT_MAX_PORT_TRIES = 20;
1021
1043
  function formatAddress(host, port) {
1022
1044
  return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
1023
1045
  }
1024
1046
  function listenLoopbackDualStack(args) {
1025
- const { primary, port, host, attachSecondary } = args;
1047
+ const { primary, port, host, attachSecondary, maxTries } = args;
1048
+ const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
1026
1049
  let sibling;
1027
1050
  const ready = new Promise(
1028
- (resolve2) => {
1051
+ (resolve2, reject) => {
1029
1052
  const addresses = [];
1030
1053
  const warnings = [];
1031
1054
  const primaryHost = host === "localhost" ? "127.0.0.1" : host;
1032
- primary.listen(port, primaryHost, () => {
1055
+ let candidate = port;
1056
+ let attempt = 0;
1057
+ const bindFailed = (atPort, msg) => reject(
1058
+ new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
1059
+ );
1060
+ const onError = (e) => {
1061
+ if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
1062
+ if (++attempt >= maxAttempts) {
1063
+ reject(
1064
+ new Error(
1065
+ `could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
1066
+ )
1067
+ );
1068
+ return;
1069
+ }
1070
+ candidate++;
1071
+ listen();
1072
+ };
1073
+ const onListening = () => {
1074
+ primary.removeListener("error", onError);
1075
+ if (candidate !== port) {
1076
+ warnings.push(`port ${port} was busy; using ${candidate} instead`);
1077
+ }
1033
1078
  const addr = primary.address();
1034
- const resolvedPort = addr && typeof addr === "object" ? addr.port : port;
1079
+ const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
1035
1080
  addresses.push(formatAddress(primaryHost, resolvedPort));
1036
1081
  if (!LOOPBACK.has(host)) {
1037
- resolve2({ addresses, warnings });
1082
+ resolve2({ addresses, port: resolvedPort, warnings });
1038
1083
  return;
1039
1084
  }
1040
1085
  const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
1041
1086
  const s = http.createServer();
1042
1087
  attachSecondary(s);
1043
- const onError = (e) => {
1088
+ const onSiblingError = (se) => {
1044
1089
  s.close();
1045
1090
  warnings.push(
1046
- `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${e.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1091
+ `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1047
1092
  );
1048
- resolve2({ addresses, warnings });
1093
+ resolve2({ addresses, port: resolvedPort, warnings });
1049
1094
  };
1050
- s.once("error", onError);
1095
+ s.once("error", onSiblingError);
1051
1096
  s.listen(resolvedPort, siblingHost, () => {
1052
- s.off("error", onError);
1097
+ s.off("error", onSiblingError);
1053
1098
  sibling = s;
1054
1099
  addresses.push(formatAddress(siblingHost, resolvedPort));
1055
- resolve2({ addresses, warnings });
1100
+ resolve2({ addresses, port: resolvedPort, warnings });
1056
1101
  });
1057
- });
1102
+ };
1103
+ const listen = () => {
1104
+ try {
1105
+ primary.listen(candidate, primaryHost);
1106
+ } catch (e) {
1107
+ primary.removeListener("error", onError);
1108
+ primary.removeListener("listening", onListening);
1109
+ bindFailed(candidate, e.message);
1110
+ }
1111
+ };
1112
+ primary.on("error", onError);
1113
+ primary.once("listening", onListening);
1114
+ listen();
1058
1115
  }
1059
1116
  );
1060
1117
  return {
@@ -1171,6 +1228,11 @@ var DevtoolsSpanExporter = class {
1171
1228
  timestamp: event.time[0] * 1e3 + event.time[1] / 1e6,
1172
1229
  attributes: event.attributes ? Object.fromEntries(Object.entries(event.attributes)) : void 0
1173
1230
  }));
1231
+ const links = span.links.map((link) => ({
1232
+ traceId: link.context.traceId,
1233
+ spanId: link.context.spanId,
1234
+ attributes: link.attributes ? Object.fromEntries(Object.entries(link.attributes)) : void 0
1235
+ }));
1174
1236
  return {
1175
1237
  traceId: spanContext.traceId,
1176
1238
  spanId: spanContext.spanId,
@@ -1185,9 +1247,15 @@ var DevtoolsSpanExporter = class {
1185
1247
  code: status,
1186
1248
  message: span.status.message
1187
1249
  },
1188
- events: events.length > 0 ? events : void 0
1250
+ events: events.length > 0 ? events : void 0,
1251
+ links: links.length > 0 ? links : void 0,
1252
+ scope: this.convertScope(span)
1189
1253
  };
1190
1254
  }
1255
+ convertScope(span) {
1256
+ const s = span.instrumentationScope ?? span.instrumentationLibrary;
1257
+ return s?.name ? { name: s.name, version: s.version || void 0 } : void 0;
1258
+ }
1191
1259
  /**
1192
1260
  * Convert OpenTelemetry SpanKind to string
1193
1261
  */