autotel-devtools 8.0.0 → 8.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/README.md +37 -0
- package/dist/cli.cjs +54 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +54 -5
- 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 +54 -4
- 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 +54 -4
- 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 +54 -2
- 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 +51 -3
- 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
|
});
|
|
@@ -1011,7 +1050,8 @@ function getWidgetJs() {
|
|
|
1011
1050
|
}
|
|
1012
1051
|
return cachedWidgetJs;
|
|
1013
1052
|
}
|
|
1014
|
-
function attachDevtoolsRoutes(httpServer, devtools) {
|
|
1053
|
+
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1054
|
+
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1015
1055
|
httpServer.on("request", async (req, res) => {
|
|
1016
1056
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1017
1057
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
@@ -1045,11 +1085,19 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1045
1085
|
return;
|
|
1046
1086
|
}
|
|
1047
1087
|
if (req.method === "GET" && url === "/v1/traces") {
|
|
1088
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1089
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1048
1092
|
const data = devtools.getCurrentData();
|
|
1049
1093
|
sendJson(res, 200, { traces: data.traces, count: data.traces.length });
|
|
1050
1094
|
return;
|
|
1051
1095
|
}
|
|
1052
1096
|
if (req.method === "DELETE" && url === "/v1/traces") {
|
|
1097
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1098
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1053
1101
|
devtools.clearData();
|
|
1054
1102
|
sendJson(res, 200, { cleared: true });
|
|
1055
1103
|
return;
|
|
@@ -1290,13 +1338,14 @@ async function main() {
|
|
|
1290
1338
|
process.exit(0);
|
|
1291
1339
|
}
|
|
1292
1340
|
const httpServer = http.createServer();
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1341
|
+
const loopbackOnly = hostHeaderIsLoopback(options.host);
|
|
1342
|
+
const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true });
|
|
1343
|
+
attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
|
|
1295
1344
|
const listeners = listenLoopbackDualStack({
|
|
1296
1345
|
primary: httpServer,
|
|
1297
1346
|
port: options.port,
|
|
1298
1347
|
host: options.host,
|
|
1299
|
-
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1348
|
+
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
|
|
1300
1349
|
});
|
|
1301
1350
|
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
1302
1351
|
if (boundPort !== options.port) {
|