autotel-devtools 6.0.0 → 6.1.0

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
@@ -375,6 +375,9 @@ var DevtoolsServer = class {
375
375
  this.onData = options.onData;
376
376
  this.httpServer = options.server ?? http.createServer();
377
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
+ });
378
381
  this.wss.on("connection", (ws) => {
379
382
  this.clients.add(ws);
380
383
  this.log(`Client connected (${this.clients.size} total)`);
@@ -922,7 +925,17 @@ function decodeOtlpMetricsRequest(body) {
922
925
  return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
923
926
  }
924
927
 
928
+ // src/server/identity.ts
929
+ var DEVTOOLS_IDENTITY = "autotel-devtools";
930
+
925
931
  // src/server/http.ts
932
+ function sendOtlpError(res, req, e) {
933
+ sendJson(res, 400, {
934
+ error: "Invalid OTLP payload",
935
+ message: e instanceof Error ? e.message : String(e),
936
+ contentType: req.headers["content-type"] ?? null
937
+ });
938
+ }
926
939
  var PROTOBUF_DECODERS = {
927
940
  traces: decodeOtlpTraceRequest,
928
941
  logs: decodeOtlpLogsRequest,
@@ -943,6 +956,18 @@ function findPackageRoot() {
943
956
  return dir;
944
957
  }
945
958
  var FULLPAGE_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>autotel-devtools</title><style>*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;width:100%;overflow:hidden}</style></head><body><script src="/widget.js?mode=fullpage"></script></body></html>`;
959
+ var cachedVersion = null;
960
+ function getVersion() {
961
+ if (cachedVersion !== null) return cachedVersion;
962
+ let version = "unknown";
963
+ try {
964
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(findPackageRoot(), "package.json"), "utf8"));
965
+ if (typeof pkg.version === "string") version = pkg.version;
966
+ } catch {
967
+ }
968
+ cachedVersion = version;
969
+ return version;
970
+ }
946
971
  var cachedWidgetJs = null;
947
972
  function getWidgetJs() {
948
973
  if (!cachedWidgetJs) {
@@ -969,6 +994,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
969
994
  res.setHeader("Access-Control-Allow-Origin", "*");
970
995
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
971
996
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
997
+ res.setHeader("x-autotel-devtools", getVersion());
998
+ res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
972
999
  if (req.method === "OPTIONS") {
973
1000
  res.writeHead(204);
974
1001
  res.end();
@@ -987,7 +1014,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
987
1014
  return;
988
1015
  }
989
1016
  if (req.method === "GET" && url === "/healthz") {
990
- sendJson(res, 200, { ok: true, clients: devtools.clientCount });
1017
+ sendJson(res, 200, {
1018
+ ok: true,
1019
+ service: DEVTOOLS_IDENTITY,
1020
+ version: getVersion(),
1021
+ clients: devtools.clientCount
1022
+ });
991
1023
  return;
992
1024
  }
993
1025
  if (req.method === "GET" && url === "/v1/traces") {
@@ -1007,7 +1039,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1007
1039
  devtools.addTraces(traces);
1008
1040
  sendJson(res, 200, { acceptedTraces: traces.length });
1009
1041
  } catch (e) {
1010
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1042
+ sendOtlpError(res, req, e);
1011
1043
  }
1012
1044
  return;
1013
1045
  }
@@ -1018,7 +1050,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1018
1050
  devtools.addLogs(logs);
1019
1051
  sendJson(res, 200, { acceptedLogs: logs.length });
1020
1052
  } catch (e) {
1021
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1053
+ sendOtlpError(res, req, e);
1022
1054
  }
1023
1055
  return;
1024
1056
  }
@@ -1028,7 +1060,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1028
1060
  const count = countOtlpMetrics(payload);
1029
1061
  sendJson(res, 200, { acceptedMetrics: count });
1030
1062
  } catch (e) {
1031
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1063
+ sendOtlpError(res, req, e);
1032
1064
  }
1033
1065
  return;
1034
1066
  }
@@ -1036,43 +1068,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1036
1068
  });
1037
1069
  }
1038
1070
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
1071
+ var DEFAULT_MAX_PORT_TRIES = 20;
1039
1072
  function formatAddress(host, port) {
1040
1073
  return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
1041
1074
  }
1042
1075
  function listenLoopbackDualStack(args) {
1043
- const { primary, port, host, attachSecondary } = args;
1076
+ const { primary, port, host, attachSecondary, maxTries } = args;
1077
+ const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
1044
1078
  let sibling;
1045
1079
  const ready = new Promise(
1046
- (resolve2) => {
1080
+ (resolve2, reject) => {
1047
1081
  const addresses = [];
1048
1082
  const warnings = [];
1049
1083
  const primaryHost = host === "localhost" ? "127.0.0.1" : host;
1050
- primary.listen(port, primaryHost, () => {
1084
+ let candidate = port;
1085
+ let attempt = 0;
1086
+ const bindFailed = (atPort, msg) => reject(
1087
+ new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
1088
+ );
1089
+ const onError = (e) => {
1090
+ if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
1091
+ if (++attempt >= maxAttempts) {
1092
+ reject(
1093
+ new Error(
1094
+ `could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
1095
+ )
1096
+ );
1097
+ return;
1098
+ }
1099
+ candidate++;
1100
+ listen();
1101
+ };
1102
+ const onListening = () => {
1103
+ primary.removeListener("error", onError);
1104
+ if (candidate !== port) {
1105
+ warnings.push(`port ${port} was busy; using ${candidate} instead`);
1106
+ }
1051
1107
  const addr = primary.address();
1052
- const resolvedPort = addr && typeof addr === "object" ? addr.port : port;
1108
+ const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
1053
1109
  addresses.push(formatAddress(primaryHost, resolvedPort));
1054
1110
  if (!LOOPBACK.has(host)) {
1055
- resolve2({ addresses, warnings });
1111
+ resolve2({ addresses, port: resolvedPort, warnings });
1056
1112
  return;
1057
1113
  }
1058
1114
  const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
1059
1115
  const s = http.createServer();
1060
1116
  attachSecondary(s);
1061
- const onError = (e) => {
1117
+ const onSiblingError = (se) => {
1062
1118
  s.close();
1063
1119
  warnings.push(
1064
- `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${e.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1120
+ `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1065
1121
  );
1066
- resolve2({ addresses, warnings });
1122
+ resolve2({ addresses, port: resolvedPort, warnings });
1067
1123
  };
1068
- s.once("error", onError);
1124
+ s.once("error", onSiblingError);
1069
1125
  s.listen(resolvedPort, siblingHost, () => {
1070
- s.off("error", onError);
1126
+ s.off("error", onSiblingError);
1071
1127
  sibling = s;
1072
1128
  addresses.push(formatAddress(siblingHost, resolvedPort));
1073
- resolve2({ addresses, warnings });
1129
+ resolve2({ addresses, port: resolvedPort, warnings });
1074
1130
  });
1075
- });
1131
+ };
1132
+ const listen = () => {
1133
+ try {
1134
+ primary.listen(candidate, primaryHost);
1135
+ } catch (e) {
1136
+ primary.removeListener("error", onError);
1137
+ primary.removeListener("listening", onListening);
1138
+ bindFailed(candidate, e.message);
1139
+ }
1140
+ };
1141
+ primary.on("error", onError);
1142
+ primary.once("listening", onListening);
1143
+ listen();
1076
1144
  }
1077
1145
  );
1078
1146
  return {