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/README.md CHANGED
@@ -111,6 +111,25 @@ const myFunction = trace((ctx) => async () => {
111
111
 
112
112
  - **DevtoolsServer** - WebSocket server + in-memory data store
113
113
  - **HTTP Routes** - OTLP receivers for traces/logs/metrics (JSON + protobuf)
114
+
115
+ #### Detecting the receiver
116
+
117
+ Every response carries an `x-autotel-devtools: <version>` header, and `GET /healthz`
118
+ returns `{ ok, service: "autotel-devtools", version, clients }`. Use either to confirm
119
+ you are talking to autotel-devtools rather than another OTLP collector that happens to
120
+ share the port — for example before pointing an exporter at `:4318`:
121
+
122
+ ```ts
123
+ import { probePortHolder } from 'autotel-devtools/server'
124
+
125
+ // 'autotel-devtools' | 'foreign' | 'none'
126
+ const holder = await probePortHolder('127.0.0.1', 4318)
127
+ ```
128
+
129
+ If you start the receiver and the requested port is held by a *foreign* process (some
130
+ IDEs run their own OTLP collector on `:4318`), the CLI falls forward to the next free
131
+ port and warns that exporters still aimed at the busy port are reaching that other
132
+ process — point them at the bound port, or free the original.
114
133
  - **Exporters** - OpenTelemetry span/log exporters
115
134
 
116
135
  ### Widget (Svelte 5)
package/dist/cli.cjs CHANGED
@@ -430,13 +430,16 @@ var DevtoolsServer = class {
430
430
  addTraces(traces) {
431
431
  for (const trace of traces) this.addTrace(trace);
432
432
  }
433
+ // `errors` is full-state on every broadcast (the client replaces, not appends),
434
+ // so non-trace broadcasts must echo the current error groups rather than `[]` —
435
+ // otherwise a log/metric arriving after an error would wipe it from the UI.
433
436
  addLog(log) {
434
437
  this.logs = appendWithLimit(this.logs, log, this.limits.maxLogCount);
435
- this.broadcast({ traces: [], metrics: [], logs: [log], errors: [] });
438
+ this.broadcast({ traces: [], metrics: [], logs: [log], errors: this.errorAggregator.getErrorGroups() });
436
439
  }
437
440
  addLogs(logs) {
438
441
  this.logs = appendManyWithLimit(this.logs, logs, this.limits.maxLogCount);
439
- this.broadcast({ traces: [], metrics: [], logs, errors: [] });
442
+ this.broadcast({ traces: [], metrics: [], logs, errors: this.errorAggregator.getErrorGroups() });
440
443
  }
441
444
  addMetric(metric) {
442
445
  this.metrics = appendWithLimit(
@@ -444,7 +447,7 @@ var DevtoolsServer = class {
444
447
  metric,
445
448
  this.limits.maxMetricCount
446
449
  );
447
- this.broadcast({ traces: [], metrics: [metric], logs: [], errors: [] });
450
+ this.broadcast({ traces: [], metrics: [metric], logs: [], errors: this.errorAggregator.getErrorGroups() });
448
451
  }
449
452
  getCurrentData() {
450
453
  return {
@@ -923,7 +926,38 @@ function decodeOtlpMetricsRequest(body) {
923
926
  return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
924
927
  }
925
928
 
929
+ // src/server/identity.ts
930
+ var DEVTOOLS_IDENTITY = "autotel-devtools";
931
+ async function probePortHolder(host, port, timeoutMs = 500) {
932
+ const authority = host.includes(":") ? `[${host}]` : host;
933
+ const controller = new AbortController();
934
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
935
+ try {
936
+ const res = await fetch(`http://${authority}:${port}/healthz`, {
937
+ signal: controller.signal
938
+ });
939
+ if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
940
+ try {
941
+ const body = await res.json();
942
+ if (body && body.service === DEVTOOLS_IDENTITY) return "autotel-devtools";
943
+ } catch {
944
+ }
945
+ return "foreign";
946
+ } catch {
947
+ return "none";
948
+ } finally {
949
+ clearTimeout(timer);
950
+ }
951
+ }
952
+
926
953
  // src/server/http.ts
954
+ function sendOtlpError(res, req, e) {
955
+ sendJson(res, 400, {
956
+ error: "Invalid OTLP payload",
957
+ message: e instanceof Error ? e.message : String(e),
958
+ contentType: req.headers["content-type"] ?? null
959
+ });
960
+ }
927
961
  var PROTOBUF_DECODERS = {
928
962
  traces: decodeOtlpTraceRequest,
929
963
  logs: decodeOtlpLogsRequest,
@@ -944,6 +978,18 @@ function findPackageRoot() {
944
978
  return dir;
945
979
  }
946
980
  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>`;
981
+ var cachedVersion = null;
982
+ function getVersion() {
983
+ if (cachedVersion !== null) return cachedVersion;
984
+ let version = "unknown";
985
+ try {
986
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(findPackageRoot(), "package.json"), "utf8"));
987
+ if (typeof pkg.version === "string") version = pkg.version;
988
+ } catch {
989
+ }
990
+ cachedVersion = version;
991
+ return version;
992
+ }
947
993
  var cachedWidgetJs = null;
948
994
  function getWidgetJs() {
949
995
  if (!cachedWidgetJs) {
@@ -970,6 +1016,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
970
1016
  res.setHeader("Access-Control-Allow-Origin", "*");
971
1017
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
972
1018
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1019
+ res.setHeader("x-autotel-devtools", getVersion());
1020
+ res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
973
1021
  if (req.method === "OPTIONS") {
974
1022
  res.writeHead(204);
975
1023
  res.end();
@@ -988,7 +1036,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
988
1036
  return;
989
1037
  }
990
1038
  if (req.method === "GET" && url === "/healthz") {
991
- sendJson(res, 200, { ok: true, clients: devtools.clientCount });
1039
+ sendJson(res, 200, {
1040
+ ok: true,
1041
+ service: DEVTOOLS_IDENTITY,
1042
+ version: getVersion(),
1043
+ clients: devtools.clientCount
1044
+ });
992
1045
  return;
993
1046
  }
994
1047
  if (req.method === "GET" && url === "/v1/traces") {
@@ -1008,7 +1061,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1008
1061
  devtools.addTraces(traces);
1009
1062
  sendJson(res, 200, { acceptedTraces: traces.length });
1010
1063
  } catch (e) {
1011
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1064
+ sendOtlpError(res, req, e);
1012
1065
  }
1013
1066
  return;
1014
1067
  }
@@ -1019,7 +1072,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1019
1072
  devtools.addLogs(logs);
1020
1073
  sendJson(res, 200, { acceptedLogs: logs.length });
1021
1074
  } catch (e) {
1022
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1075
+ sendOtlpError(res, req, e);
1023
1076
  }
1024
1077
  return;
1025
1078
  }
@@ -1029,7 +1082,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1029
1082
  const count = countOtlpMetrics(payload);
1030
1083
  sendJson(res, 200, { acceptedMetrics: count });
1031
1084
  } catch (e) {
1032
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1085
+ sendOtlpError(res, req, e);
1033
1086
  }
1034
1087
  return;
1035
1088
  }
@@ -1246,6 +1299,18 @@ async function main() {
1246
1299
  attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1247
1300
  });
1248
1301
  const { addresses, warnings, port: boundPort } = await listeners.ready;
1302
+ if (boundPort !== options.port) {
1303
+ const holder = await probePortHolder(options.host, options.port);
1304
+ if (holder === "autotel-devtools") {
1305
+ warnings.push(
1306
+ `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.`
1307
+ );
1308
+ } else {
1309
+ warnings.push(
1310
+ `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.`
1311
+ );
1312
+ }
1313
+ }
1249
1314
  const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
1250
1315
  const title = options.title || "autotel-devtools";
1251
1316
  process.stdout.write(`