autotel-devtools 8.0.0 → 8.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 +37 -0
- package/dist/cli.cjs +76 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +76 -7
- package/dist/cli.js.map +1 -1
- package/dist/{error-aggregator-CAk_pt3Z.d.ts → error-aggregator-D0Uu5r38.d.ts} +1 -1
- package/dist/{error-aggregator-CbLiuot4.d.cts → error-aggregator-D1Mr221Y.d.cts} +1 -1
- package/dist/{exporter-DjLkU621.d.cts → exporter-De6p4iAD.d.cts} +6 -0
- package/dist/{exporter-DjLkU621.d.ts → exporter-De6p4iAD.d.ts} +6 -0
- package/dist/genai/index.cjs +7 -2
- package/dist/genai/index.cjs.map +1 -1
- package/dist/genai/index.d.cts +6 -2
- package/dist/genai/index.d.ts +6 -2
- package/dist/genai/index.js +7 -2
- package/dist/genai/index.js.map +1 -1
- package/dist/index.cjs +76 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +76 -6
- package/dist/index.js.map +1 -1
- package/dist/server/exporter.d.cts +1 -1
- package/dist/server/exporter.d.ts +1 -1
- package/dist/server/index.cjs +76 -4
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +30 -5
- package/dist/server/index.d.ts +30 -5
- package/dist/server/index.js +73 -5
- package/dist/server/index.js.map +1 -1
- package/dist/widget.global.js +10 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,12 +149,29 @@ process — point them at the bound port, or free the original.
|
|
|
149
149
|
- ✅ Error aggregation and grouping
|
|
150
150
|
- ✅ Service map visualization
|
|
151
151
|
- ✅ Resources view (derived from telemetry)
|
|
152
|
+
- ✅ GenAI run summaries + narrated walkthrough
|
|
152
153
|
- ✅ Search with debounce (300ms)
|
|
153
154
|
- ✅ Configurable telemetry limits (env vars)
|
|
154
155
|
- ✅ Widget position persistence (localStorage)
|
|
155
156
|
- ✅ Export traces as JSON
|
|
156
157
|
- ✅ Custom element support (`<autotel-devtools>`)
|
|
157
158
|
|
|
159
|
+
### GenAI: read an agent run at a glance
|
|
160
|
+
|
|
161
|
+
When your app emits OpenTelemetry GenAI spans (Vercel AI SDK, Pydantic AI, OpenAI
|
|
162
|
+
Agents, Anthropic, Google GenAI, LangChain, …), the **GenAI** tab gives two extras
|
|
163
|
+
on top of the per-span detail:
|
|
164
|
+
|
|
165
|
+
- A **run summary strip** sits above the detail for any multi-span run — total
|
|
166
|
+
cost (table-priced; a trailing `+` marks a lower bound when some calls are
|
|
167
|
+
unpriced), input→output tokens, reasoning tokens, model calls, tool
|
|
168
|
+
executions, sub-agents, duration and errors.
|
|
169
|
+
- An **Explain run** button steps through the run in chronological order with
|
|
170
|
+
plain-language narration of each step. Auto-play or step manually with the
|
|
171
|
+
arrow keys / Space (Esc exits); clicking a span jumps the tour to that step.
|
|
172
|
+
Useful for showing a teammate or a client exactly what the agent did, which
|
|
173
|
+
tools it called, and where the cost went.
|
|
174
|
+
|
|
158
175
|
## Configuration
|
|
159
176
|
|
|
160
177
|
### Environment Variables
|
|
@@ -240,6 +257,26 @@ await expect
|
|
|
240
257
|
.toBeGreaterThan(0);
|
|
241
258
|
```
|
|
242
259
|
|
|
260
|
+
These read-back calls run from Node (no `Origin` header), so they are unaffected
|
|
261
|
+
by the origin guard below.
|
|
262
|
+
|
|
263
|
+
## Read-surface origin guard
|
|
264
|
+
|
|
265
|
+
OTLP **ingestion** (`POST /v1/{traces,logs,metrics}`), `GET /widget.js` and
|
|
266
|
+
`GET /healthz` are open to any origin — browser apps on arbitrary dev origins
|
|
267
|
+
must be able to send telemetry and load the embeddable widget. The **read**
|
|
268
|
+
surface is not: `GET /v1/traces`, `DELETE /v1/traces` and the `/ws` WebSocket are
|
|
269
|
+
origin-checked so a web page you happen to be visiting can't `fetch()` or stream
|
|
270
|
+
your locally captured prompts, responses and tokens.
|
|
271
|
+
|
|
272
|
+
- A non-loopback `Origin` (a cross-origin browser read) is rejected with `403`.
|
|
273
|
+
- When bound to a loopback host (the default), a non-loopback `Host` (DNS
|
|
274
|
+
rebinding) is also rejected. `--host 0.0.0.0` opts into network exposure and
|
|
275
|
+
applies only the `Origin` check.
|
|
276
|
+
|
|
277
|
+
The embedded widget keeps working — it connects from a loopback origin
|
|
278
|
+
(`http://localhost:<your-app-port>`). Server-side reads with no `Origin` pass.
|
|
279
|
+
|
|
243
280
|
## License
|
|
244
281
|
|
|
245
282
|
MIT
|
package/dist/cli.cjs
CHANGED
|
@@ -353,6 +353,40 @@ function appendManyWithLimit(items, incoming, limit) {
|
|
|
353
353
|
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// src/server/origin-guard.ts
|
|
357
|
+
var LOOPBACK_IPV6 = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
|
358
|
+
function isLoopbackHostname(hostname) {
|
|
359
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
360
|
+
return h === "localhost" || /^127\./.test(h) || LOOPBACK_IPV6.has(h);
|
|
361
|
+
}
|
|
362
|
+
function hostnameFromHostHeader(host) {
|
|
363
|
+
const h = host.trim();
|
|
364
|
+
if (h.startsWith("[")) {
|
|
365
|
+
const end = h.indexOf("]");
|
|
366
|
+
return end > 0 ? h.slice(1, end) : h;
|
|
367
|
+
}
|
|
368
|
+
const colon = h.indexOf(":");
|
|
369
|
+
return colon === -1 ? h : h.slice(0, colon);
|
|
370
|
+
}
|
|
371
|
+
function hostHeaderIsLoopback(host) {
|
|
372
|
+
return isLoopbackHostname(hostnameFromHostHeader(host));
|
|
373
|
+
}
|
|
374
|
+
function originIsLoopback(origin) {
|
|
375
|
+
try {
|
|
376
|
+
return isLoopbackHostname(new URL(origin).hostname);
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function allowSensitiveRequest(headers, loopbackOnly) {
|
|
382
|
+
const { origin, host } = headers;
|
|
383
|
+
if (origin && origin.length > 0 && !originIsLoopback(origin)) return false;
|
|
384
|
+
if (loopbackOnly && host && host.length > 0 && !hostHeaderIsLoopback(host)) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
356
390
|
// src/server/server.ts
|
|
357
391
|
var DevtoolsServer = class {
|
|
358
392
|
wss;
|
|
@@ -372,7 +406,12 @@ var DevtoolsServer = class {
|
|
|
372
406
|
this._port = options.port ?? 4318;
|
|
373
407
|
this.onData = options.onData;
|
|
374
408
|
this.httpServer = options.server ?? http.createServer();
|
|
375
|
-
|
|
409
|
+
const loopbackOnly = options.host == null || hostHeaderIsLoopback(options.host);
|
|
410
|
+
this.wss = new ws.WebSocketServer({
|
|
411
|
+
server: this.httpServer,
|
|
412
|
+
path: options.path ?? "/ws",
|
|
413
|
+
verifyClient: ({ origin, req }) => allowSensitiveRequest({ origin, host: req.headers.host }, loopbackOnly)
|
|
414
|
+
});
|
|
376
415
|
this.wss.on("error", (err) => {
|
|
377
416
|
if (this.httpServer.listening) throw err;
|
|
378
417
|
});
|
|
@@ -406,6 +445,7 @@ var DevtoolsServer = class {
|
|
|
406
445
|
}
|
|
407
446
|
addTrace(trace) {
|
|
408
447
|
const existing = this.traces.find((t) => t.traceId === trace.traceId);
|
|
448
|
+
const merged = existing ?? trace;
|
|
409
449
|
if (existing) {
|
|
410
450
|
const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
|
|
411
451
|
for (const span of trace.spans) {
|
|
@@ -417,6 +457,14 @@ var DevtoolsServer = class {
|
|
|
417
457
|
existing.endTime = Math.max(existing.endTime, trace.endTime);
|
|
418
458
|
existing.duration = existing.endTime - existing.startTime;
|
|
419
459
|
if (trace.status === "ERROR") existing.status = "ERROR";
|
|
460
|
+
const root = existing.spans.find((s) => !s.parentSpanId);
|
|
461
|
+
if (root) {
|
|
462
|
+
existing.rootSpan = root;
|
|
463
|
+
const rootService = root.attributes?.["service.name"];
|
|
464
|
+
if (typeof rootService === "string" && rootService.length > 0) {
|
|
465
|
+
existing.service = rootService;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
420
468
|
} else {
|
|
421
469
|
this.traces = appendWithLimit(
|
|
422
470
|
this.traces,
|
|
@@ -425,7 +473,7 @@ var DevtoolsServer = class {
|
|
|
425
473
|
);
|
|
426
474
|
}
|
|
427
475
|
this.errorAggregator.addErrorsFromTrace(trace);
|
|
428
|
-
this.broadcast({ traces: [
|
|
476
|
+
this.broadcast({ traces: [merged], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
|
|
429
477
|
}
|
|
430
478
|
addTraces(traces) {
|
|
431
479
|
for (const trace of traces) this.addTrace(trace);
|
|
@@ -977,7 +1025,8 @@ function findPackageRoot() {
|
|
|
977
1025
|
}
|
|
978
1026
|
return dir;
|
|
979
1027
|
}
|
|
980
|
-
var
|
|
1028
|
+
var DEVTOOLS_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0f172a"/><text x="32" y="41" text-anchor="middle" font-size="32">\u{1F6F0}\uFE0F</text></svg>';
|
|
1029
|
+
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><link rel="icon" href="/favicon.svg" type="image/svg+xml"><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
1030
|
var cachedVersion = null;
|
|
982
1031
|
function getVersion() {
|
|
983
1032
|
if (cachedVersion !== null) return cachedVersion;
|
|
@@ -1011,8 +1060,10 @@ function getWidgetJs() {
|
|
|
1011
1060
|
}
|
|
1012
1061
|
return cachedWidgetJs;
|
|
1013
1062
|
}
|
|
1014
|
-
function attachDevtoolsRoutes(httpServer, devtools) {
|
|
1063
|
+
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1064
|
+
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1015
1065
|
httpServer.on("request", async (req, res) => {
|
|
1066
|
+
if (req.headers.upgrade?.toLowerCase() === "websocket") return;
|
|
1016
1067
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1017
1068
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1018
1069
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -1035,6 +1086,15 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1035
1086
|
res.end(js);
|
|
1036
1087
|
return;
|
|
1037
1088
|
}
|
|
1089
|
+
if (req.method === "GET" && (url === "/favicon.svg" || url === "/favicon.ico")) {
|
|
1090
|
+
res.writeHead(200, {
|
|
1091
|
+
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
1092
|
+
"Cache-Control": "public, max-age=86400",
|
|
1093
|
+
"Content-Length": Buffer.byteLength(DEVTOOLS_FAVICON_SVG)
|
|
1094
|
+
});
|
|
1095
|
+
res.end(DEVTOOLS_FAVICON_SVG);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1038
1098
|
if (req.method === "GET" && url === "/healthz") {
|
|
1039
1099
|
sendJson(res, 200, {
|
|
1040
1100
|
ok: true,
|
|
@@ -1045,11 +1105,19 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1045
1105
|
return;
|
|
1046
1106
|
}
|
|
1047
1107
|
if (req.method === "GET" && url === "/v1/traces") {
|
|
1108
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1109
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1048
1112
|
const data = devtools.getCurrentData();
|
|
1049
1113
|
sendJson(res, 200, { traces: data.traces, count: data.traces.length });
|
|
1050
1114
|
return;
|
|
1051
1115
|
}
|
|
1052
1116
|
if (req.method === "DELETE" && url === "/v1/traces") {
|
|
1117
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1118
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1053
1121
|
devtools.clearData();
|
|
1054
1122
|
sendJson(res, 200, { cleared: true });
|
|
1055
1123
|
return;
|
|
@@ -1290,13 +1358,14 @@ async function main() {
|
|
|
1290
1358
|
process.exit(0);
|
|
1291
1359
|
}
|
|
1292
1360
|
const httpServer = http.createServer();
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1361
|
+
const loopbackOnly = hostHeaderIsLoopback(options.host);
|
|
1362
|
+
const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true });
|
|
1363
|
+
attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
|
|
1295
1364
|
const listeners = listenLoopbackDualStack({
|
|
1296
1365
|
primary: httpServer,
|
|
1297
1366
|
port: options.port,
|
|
1298
1367
|
host: options.host,
|
|
1299
|
-
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1368
|
+
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
|
|
1300
1369
|
});
|
|
1301
1370
|
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
1302
1371
|
if (boundPort !== options.port) {
|