autotel-devtools 6.0.1 → 6.1.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/cli.js CHANGED
@@ -423,13 +423,16 @@ var DevtoolsServer = class {
423
423
  addTraces(traces) {
424
424
  for (const trace of traces) this.addTrace(trace);
425
425
  }
426
+ // `errors` is full-state on every broadcast (the client replaces, not appends),
427
+ // so non-trace broadcasts must echo the current error groups rather than `[]` —
428
+ // otherwise a log/metric arriving after an error would wipe it from the UI.
426
429
  addLog(log) {
427
430
  this.logs = appendWithLimit(this.logs, log, this.limits.maxLogCount);
428
- this.broadcast({ traces: [], metrics: [], logs: [log], errors: [] });
431
+ this.broadcast({ traces: [], metrics: [], logs: [log], errors: this.errorAggregator.getErrorGroups() });
429
432
  }
430
433
  addLogs(logs) {
431
434
  this.logs = appendManyWithLimit(this.logs, logs, this.limits.maxLogCount);
432
- this.broadcast({ traces: [], metrics: [], logs, errors: [] });
435
+ this.broadcast({ traces: [], metrics: [], logs, errors: this.errorAggregator.getErrorGroups() });
433
436
  }
434
437
  addMetric(metric) {
435
438
  this.metrics = appendWithLimit(
@@ -437,7 +440,7 @@ var DevtoolsServer = class {
437
440
  metric,
438
441
  this.limits.maxMetricCount
439
442
  );
440
- this.broadcast({ traces: [], metrics: [metric], logs: [], errors: [] });
443
+ this.broadcast({ traces: [], metrics: [metric], logs: [], errors: this.errorAggregator.getErrorGroups() });
441
444
  }
442
445
  getCurrentData() {
443
446
  return {
@@ -916,7 +919,38 @@ function decodeOtlpMetricsRequest(body) {
916
919
  return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
917
920
  }
918
921
 
922
+ // src/server/identity.ts
923
+ var DEVTOOLS_IDENTITY = "autotel-devtools";
924
+ async function probePortHolder(host, port, timeoutMs = 500) {
925
+ const authority = host.includes(":") ? `[${host}]` : host;
926
+ const controller = new AbortController();
927
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
928
+ try {
929
+ const res = await fetch(`http://${authority}:${port}/healthz`, {
930
+ signal: controller.signal
931
+ });
932
+ if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
933
+ try {
934
+ const body = await res.json();
935
+ if (body && body.service === DEVTOOLS_IDENTITY) return "autotel-devtools";
936
+ } catch {
937
+ }
938
+ return "foreign";
939
+ } catch {
940
+ return "none";
941
+ } finally {
942
+ clearTimeout(timer);
943
+ }
944
+ }
945
+
919
946
  // src/server/http.ts
947
+ function sendOtlpError(res, req, e) {
948
+ sendJson(res, 400, {
949
+ error: "Invalid OTLP payload",
950
+ message: e instanceof Error ? e.message : String(e),
951
+ contentType: req.headers["content-type"] ?? null
952
+ });
953
+ }
920
954
  var PROTOBUF_DECODERS = {
921
955
  traces: decodeOtlpTraceRequest,
922
956
  logs: decodeOtlpLogsRequest,
@@ -937,6 +971,18 @@ function findPackageRoot() {
937
971
  return dir;
938
972
  }
939
973
  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>`;
974
+ var cachedVersion = null;
975
+ function getVersion() {
976
+ if (cachedVersion !== null) return cachedVersion;
977
+ let version = "unknown";
978
+ try {
979
+ const pkg = JSON.parse(readFileSync(resolve(findPackageRoot(), "package.json"), "utf8"));
980
+ if (typeof pkg.version === "string") version = pkg.version;
981
+ } catch {
982
+ }
983
+ cachedVersion = version;
984
+ return version;
985
+ }
940
986
  var cachedWidgetJs = null;
941
987
  function getWidgetJs() {
942
988
  if (!cachedWidgetJs) {
@@ -963,6 +1009,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
963
1009
  res.setHeader("Access-Control-Allow-Origin", "*");
964
1010
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
965
1011
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1012
+ res.setHeader("x-autotel-devtools", getVersion());
1013
+ res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
966
1014
  if (req.method === "OPTIONS") {
967
1015
  res.writeHead(204);
968
1016
  res.end();
@@ -981,7 +1029,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
981
1029
  return;
982
1030
  }
983
1031
  if (req.method === "GET" && url === "/healthz") {
984
- sendJson(res, 200, { ok: true, clients: devtools.clientCount });
1032
+ sendJson(res, 200, {
1033
+ ok: true,
1034
+ service: DEVTOOLS_IDENTITY,
1035
+ version: getVersion(),
1036
+ clients: devtools.clientCount
1037
+ });
985
1038
  return;
986
1039
  }
987
1040
  if (req.method === "GET" && url === "/v1/traces") {
@@ -1001,7 +1054,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1001
1054
  devtools.addTraces(traces);
1002
1055
  sendJson(res, 200, { acceptedTraces: traces.length });
1003
1056
  } catch (e) {
1004
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1057
+ sendOtlpError(res, req, e);
1005
1058
  }
1006
1059
  return;
1007
1060
  }
@@ -1012,7 +1065,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1012
1065
  devtools.addLogs(logs);
1013
1066
  sendJson(res, 200, { acceptedLogs: logs.length });
1014
1067
  } catch (e) {
1015
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1068
+ sendOtlpError(res, req, e);
1016
1069
  }
1017
1070
  return;
1018
1071
  }
@@ -1022,7 +1075,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1022
1075
  const count = countOtlpMetrics(payload);
1023
1076
  sendJson(res, 200, { acceptedMetrics: count });
1024
1077
  } catch (e) {
1025
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1078
+ sendOtlpError(res, req, e);
1026
1079
  }
1027
1080
  return;
1028
1081
  }
@@ -1239,6 +1292,18 @@ async function main() {
1239
1292
  attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1240
1293
  });
1241
1294
  const { addresses, warnings, port: boundPort } = await listeners.ready;
1295
+ if (boundPort !== options.port) {
1296
+ const holder = await probePortHolder(options.host, options.port);
1297
+ if (holder === "autotel-devtools") {
1298
+ warnings.push(
1299
+ `another autotel-devtools is already running on port ${options.port}; this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`
1300
+ );
1301
+ } else {
1302
+ warnings.push(
1303
+ `port ${options.port} is held by another process that is NOT autotel-devtools. Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. Point your exporter at :${boundPort}, or free :${options.port} and restart.`
1304
+ );
1305
+ }
1306
+ }
1242
1307
  const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
1243
1308
  const title = options.title || "autotel-devtools";
1244
1309
  process.stdout.write(`