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.js CHANGED
@@ -368,6 +368,9 @@ var DevtoolsServer = class {
368
368
  this.onData = options.onData;
369
369
  this.httpServer = options.server ?? createServer();
370
370
  this.wss = new WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
371
+ this.wss.on("error", (err) => {
372
+ if (this.httpServer.listening) throw err;
373
+ });
371
374
  this.wss.on("connection", (ws) => {
372
375
  this.clients.add(ws);
373
376
  this.log(`Client connected (${this.clients.size} total)`);
@@ -915,7 +918,17 @@ function decodeOtlpMetricsRequest(body) {
915
918
  return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
916
919
  }
917
920
 
921
+ // src/server/identity.ts
922
+ var DEVTOOLS_IDENTITY = "autotel-devtools";
923
+
918
924
  // src/server/http.ts
925
+ function sendOtlpError(res, req, e) {
926
+ sendJson(res, 400, {
927
+ error: "Invalid OTLP payload",
928
+ message: e instanceof Error ? e.message : String(e),
929
+ contentType: req.headers["content-type"] ?? null
930
+ });
931
+ }
919
932
  var PROTOBUF_DECODERS = {
920
933
  traces: decodeOtlpTraceRequest,
921
934
  logs: decodeOtlpLogsRequest,
@@ -936,6 +949,18 @@ function findPackageRoot() {
936
949
  return dir;
937
950
  }
938
951
  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>`;
952
+ var cachedVersion = null;
953
+ function getVersion() {
954
+ if (cachedVersion !== null) return cachedVersion;
955
+ let version = "unknown";
956
+ try {
957
+ const pkg = JSON.parse(readFileSync(resolve(findPackageRoot(), "package.json"), "utf8"));
958
+ if (typeof pkg.version === "string") version = pkg.version;
959
+ } catch {
960
+ }
961
+ cachedVersion = version;
962
+ return version;
963
+ }
939
964
  var cachedWidgetJs = null;
940
965
  function getWidgetJs() {
941
966
  if (!cachedWidgetJs) {
@@ -962,6 +987,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
962
987
  res.setHeader("Access-Control-Allow-Origin", "*");
963
988
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
964
989
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
990
+ res.setHeader("x-autotel-devtools", getVersion());
991
+ res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
965
992
  if (req.method === "OPTIONS") {
966
993
  res.writeHead(204);
967
994
  res.end();
@@ -980,7 +1007,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
980
1007
  return;
981
1008
  }
982
1009
  if (req.method === "GET" && url === "/healthz") {
983
- sendJson(res, 200, { ok: true, clients: devtools.clientCount });
1010
+ sendJson(res, 200, {
1011
+ ok: true,
1012
+ service: DEVTOOLS_IDENTITY,
1013
+ version: getVersion(),
1014
+ clients: devtools.clientCount
1015
+ });
984
1016
  return;
985
1017
  }
986
1018
  if (req.method === "GET" && url === "/v1/traces") {
@@ -1000,7 +1032,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1000
1032
  devtools.addTraces(traces);
1001
1033
  sendJson(res, 200, { acceptedTraces: traces.length });
1002
1034
  } catch (e) {
1003
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1035
+ sendOtlpError(res, req, e);
1004
1036
  }
1005
1037
  return;
1006
1038
  }
@@ -1011,7 +1043,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1011
1043
  devtools.addLogs(logs);
1012
1044
  sendJson(res, 200, { acceptedLogs: logs.length });
1013
1045
  } catch (e) {
1014
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1046
+ sendOtlpError(res, req, e);
1015
1047
  }
1016
1048
  return;
1017
1049
  }
@@ -1021,7 +1053,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1021
1053
  const count = countOtlpMetrics(payload);
1022
1054
  sendJson(res, 200, { acceptedMetrics: count });
1023
1055
  } catch (e) {
1024
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1056
+ sendOtlpError(res, req, e);
1025
1057
  }
1026
1058
  return;
1027
1059
  }
@@ -1029,43 +1061,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1029
1061
  });
1030
1062
  }
1031
1063
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
1064
+ var DEFAULT_MAX_PORT_TRIES = 20;
1032
1065
  function formatAddress(host, port) {
1033
1066
  return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
1034
1067
  }
1035
1068
  function listenLoopbackDualStack(args) {
1036
- const { primary, port, host, attachSecondary } = args;
1069
+ const { primary, port, host, attachSecondary, maxTries } = args;
1070
+ const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
1037
1071
  let sibling;
1038
1072
  const ready = new Promise(
1039
- (resolve2) => {
1073
+ (resolve2, reject) => {
1040
1074
  const addresses = [];
1041
1075
  const warnings = [];
1042
1076
  const primaryHost = host === "localhost" ? "127.0.0.1" : host;
1043
- primary.listen(port, primaryHost, () => {
1077
+ let candidate = port;
1078
+ let attempt = 0;
1079
+ const bindFailed = (atPort, msg) => reject(
1080
+ new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
1081
+ );
1082
+ const onError = (e) => {
1083
+ if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
1084
+ if (++attempt >= maxAttempts) {
1085
+ reject(
1086
+ new Error(
1087
+ `could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
1088
+ )
1089
+ );
1090
+ return;
1091
+ }
1092
+ candidate++;
1093
+ listen();
1094
+ };
1095
+ const onListening = () => {
1096
+ primary.removeListener("error", onError);
1097
+ if (candidate !== port) {
1098
+ warnings.push(`port ${port} was busy; using ${candidate} instead`);
1099
+ }
1044
1100
  const addr = primary.address();
1045
- const resolvedPort = addr && typeof addr === "object" ? addr.port : port;
1101
+ const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
1046
1102
  addresses.push(formatAddress(primaryHost, resolvedPort));
1047
1103
  if (!LOOPBACK.has(host)) {
1048
- resolve2({ addresses, warnings });
1104
+ resolve2({ addresses, port: resolvedPort, warnings });
1049
1105
  return;
1050
1106
  }
1051
1107
  const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
1052
1108
  const s = createServer();
1053
1109
  attachSecondary(s);
1054
- const onError = (e) => {
1110
+ const onSiblingError = (se) => {
1055
1111
  s.close();
1056
1112
  warnings.push(
1057
- `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${e.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1113
+ `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1058
1114
  );
1059
- resolve2({ addresses, warnings });
1115
+ resolve2({ addresses, port: resolvedPort, warnings });
1060
1116
  };
1061
- s.once("error", onError);
1117
+ s.once("error", onSiblingError);
1062
1118
  s.listen(resolvedPort, siblingHost, () => {
1063
- s.off("error", onError);
1119
+ s.off("error", onSiblingError);
1064
1120
  sibling = s;
1065
1121
  addresses.push(formatAddress(siblingHost, resolvedPort));
1066
- resolve2({ addresses, warnings });
1122
+ resolve2({ addresses, port: resolvedPort, warnings });
1067
1123
  });
1068
- });
1124
+ };
1125
+ const listen = () => {
1126
+ try {
1127
+ primary.listen(candidate, primaryHost);
1128
+ } catch (e) {
1129
+ primary.removeListener("error", onError);
1130
+ primary.removeListener("listening", onListening);
1131
+ bindFailed(candidate, e.message);
1132
+ }
1133
+ };
1134
+ primary.on("error", onError);
1135
+ primary.once("listening", onListening);
1136
+ listen();
1069
1137
  }
1070
1138
  );
1071
1139
  return {