autotel-devtools 19.1.0 → 20.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 +1 -0
- package/dist/cli.cjs +6 -3
- package/dist/cli.js +6 -3
- package/dist/{http-53ldVU8k.cjs → http-D44dITGS.cjs} +127 -2
- package/dist/{http-B8rl56bF.js → http-Ddn7gT0R.js} +123 -5
- package/dist/index.cjs +10 -3
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -3
- package/dist/server/index.cjs +1 -1
- package/dist/server/index.d.cts +9 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +1 -1
- package/dist/widget.global.js +15 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -214,6 +214,7 @@ AUTOTEL_MAX_METRIC_COUNT=50000 # Max metrics to keep (default: 100)
|
|
|
214
214
|
AUTOTEL_DEVTOOLS_PORT=4318 # Server port (default: 4318)
|
|
215
215
|
AUTOTEL_DEVTOOLS_HOST=127.0.0.1 # Bind host (default: 127.0.0.1)
|
|
216
216
|
AUTOTEL_DEVTOOLS_TITLE="My App" # Dashboard title (optional)
|
|
217
|
+
AUTOTEL_DEVTOOLS_SOURCE_ROOT=. # Root GET /source may read (default: cwd on a loopback bind, else off; `false` disables)
|
|
217
218
|
```
|
|
218
219
|
|
|
219
220
|
### CLI Options
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_http = require('./http-
|
|
2
|
+
const require_http = require('./http-D44dITGS.cjs');
|
|
3
3
|
const require_listen = require('./listen-DGzVi7DE.cjs');
|
|
4
4
|
let node_http = require("node:http");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
@@ -79,9 +79,11 @@ async function startReceiver(options) {
|
|
|
79
79
|
host: options.host,
|
|
80
80
|
verbose: true
|
|
81
81
|
});
|
|
82
|
+
const sourceRoot = require_http.resolveSourceRoot(process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
|
|
82
83
|
require_http.attachDevtoolsRoutes(httpServer, wsServer, {
|
|
83
84
|
loopbackOnly,
|
|
84
|
-
title: options.title
|
|
85
|
+
title: options.title,
|
|
86
|
+
sourceRoot
|
|
85
87
|
});
|
|
86
88
|
const listeners = require_listen.listenLoopbackDualStack({
|
|
87
89
|
primary: httpServer,
|
|
@@ -89,7 +91,8 @@ async function startReceiver(options) {
|
|
|
89
91
|
host: options.host,
|
|
90
92
|
attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, {
|
|
91
93
|
loopbackOnly,
|
|
92
|
-
title: options.title
|
|
94
|
+
title: options.title,
|
|
95
|
+
sourceRoot
|
|
93
96
|
})
|
|
94
97
|
});
|
|
95
98
|
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as probePortHolder, f as DevtoolsServer, m as hostHeaderIsLoopback, r as resolveSourceRoot, t as attachDevtoolsRoutes } from "./http-Ddn7gT0R.js";
|
|
3
3
|
import { t as listenLoopbackDualStack } from "./listen-NVMbBWHw.js";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
@@ -79,9 +79,11 @@ async function startReceiver(options) {
|
|
|
79
79
|
host: options.host,
|
|
80
80
|
verbose: true
|
|
81
81
|
});
|
|
82
|
+
const sourceRoot = resolveSourceRoot(process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
|
|
82
83
|
attachDevtoolsRoutes(httpServer, wsServer, {
|
|
83
84
|
loopbackOnly,
|
|
84
|
-
title: options.title
|
|
85
|
+
title: options.title,
|
|
86
|
+
sourceRoot
|
|
85
87
|
});
|
|
86
88
|
const listeners = listenLoopbackDualStack({
|
|
87
89
|
primary: httpServer,
|
|
@@ -89,7 +91,8 @@ async function startReceiver(options) {
|
|
|
89
91
|
host: options.host,
|
|
90
92
|
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, {
|
|
91
93
|
loopbackOnly,
|
|
92
|
-
title: options.title
|
|
94
|
+
title: options.title,
|
|
95
|
+
sourceRoot
|
|
93
96
|
})
|
|
94
97
|
});
|
|
95
98
|
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
@@ -32,6 +32,7 @@ let ws = require("ws");
|
|
|
32
32
|
let autotel_agents = require("autotel-agents");
|
|
33
33
|
let node_fs = require("node:fs");
|
|
34
34
|
let node_path = require("node:path");
|
|
35
|
+
node_path = __toESM(node_path, 1);
|
|
35
36
|
let node_url = require("node:url");
|
|
36
37
|
let protobufjs = require("protobufjs");
|
|
37
38
|
protobufjs = __toESM(protobufjs, 1);
|
|
@@ -105,7 +106,7 @@ var ErrorAggregator = class {
|
|
|
105
106
|
const exceptionEvent = span.events?.find((e) => e.name === "exception");
|
|
106
107
|
const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
|
|
107
108
|
const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
|
|
108
|
-
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
|
|
109
|
+
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || span.attributes["error.stack"] || this.extractStackFromEvents(span);
|
|
109
110
|
return {
|
|
110
111
|
traceId: trace.traceId,
|
|
111
112
|
spanId: span.spanId,
|
|
@@ -201,7 +202,7 @@ var ErrorAggregator = class {
|
|
|
201
202
|
* Normalize error message by removing dynamic parts
|
|
202
203
|
*/
|
|
203
204
|
normalizeMessage(message) {
|
|
204
|
-
return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\
|
|
205
|
+
return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
|
|
205
206
|
}
|
|
206
207
|
/**
|
|
207
208
|
* Normalize stack trace for display
|
|
@@ -1149,6 +1150,98 @@ async function probePortHolder(host, port, timeoutMs = 500) {
|
|
|
1149
1150
|
}
|
|
1150
1151
|
}
|
|
1151
1152
|
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/server/source-file.ts
|
|
1155
|
+
/** Refuse to slurp something huge just because a span named it. */
|
|
1156
|
+
const MAX_BYTES = 2e6;
|
|
1157
|
+
const DISABLED = /* @__PURE__ */ new Set([
|
|
1158
|
+
"false",
|
|
1159
|
+
"0",
|
|
1160
|
+
"off",
|
|
1161
|
+
"no",
|
|
1162
|
+
""
|
|
1163
|
+
]);
|
|
1164
|
+
/**
|
|
1165
|
+
* Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
|
|
1166
|
+
*
|
|
1167
|
+
* Defaults **on**, at the working directory: devtools is a local tool whose
|
|
1168
|
+
* whole point is showing you your own code, and requiring a flag for that would
|
|
1169
|
+
* mean nobody ever sees the feature. The blast radius stays small because two
|
|
1170
|
+
* other things still hold — the receiver is bound to loopback, and nothing
|
|
1171
|
+
* outside this directory is reachable. Set the variable to `false` to turn it
|
|
1172
|
+
* off outright.
|
|
1173
|
+
*
|
|
1174
|
+
* A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
|
|
1175
|
+
* Origin guard does not replace it: a request with no `Origin` at all — any
|
|
1176
|
+
* `curl` on the network — passes. The root holds whatever else lives in the
|
|
1177
|
+
* project, `.env` included, so the default flips to **off** there. An explicit
|
|
1178
|
+
* root is still honoured: exposing it on purpose is the caller's call.
|
|
1179
|
+
*/
|
|
1180
|
+
function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
|
|
1181
|
+
if (configured === void 0) return loopbackOnly ? cwd : void 0;
|
|
1182
|
+
if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
|
|
1183
|
+
return configured;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Resolve `requested` against `root`, or return `null` if it escapes.
|
|
1187
|
+
*
|
|
1188
|
+
* Containment is judged on **real** paths so a symlink inside the root that
|
|
1189
|
+
* points outside it is rejected — lexical `..` stripping alone cannot see that.
|
|
1190
|
+
* The value returned is the *lexical* resolution, because the real one differs
|
|
1191
|
+
* from the caller's path whenever an ancestor is a symlink (on macOS both
|
|
1192
|
+
* `/tmp` and `/var` are), and a caller comparing paths should not have to know.
|
|
1193
|
+
*/
|
|
1194
|
+
function resolveWithinRoot(root, requested) {
|
|
1195
|
+
const lexicalRoot = node_path.default.resolve(root);
|
|
1196
|
+
const realRoot = safeRealpath(lexicalRoot);
|
|
1197
|
+
if (realRoot === null) return null;
|
|
1198
|
+
const lexicalTarget = node_path.default.resolve(lexicalRoot, requested);
|
|
1199
|
+
const realTarget = safeRealpath(lexicalTarget);
|
|
1200
|
+
if (realTarget === null) return null;
|
|
1201
|
+
if (!isInside(realRoot, realTarget)) return null;
|
|
1202
|
+
return lexicalTarget;
|
|
1203
|
+
}
|
|
1204
|
+
/** True when `target` is `root` itself or sits beneath it. */
|
|
1205
|
+
function isInside(root, target) {
|
|
1206
|
+
if (target === root) return true;
|
|
1207
|
+
return target.startsWith(root.endsWith(node_path.default.sep) ? root : root + node_path.default.sep);
|
|
1208
|
+
}
|
|
1209
|
+
function safeRealpath(p) {
|
|
1210
|
+
try {
|
|
1211
|
+
return (0, node_fs.realpathSync)(p);
|
|
1212
|
+
} catch {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Read `context` lines either side of `line` from a file inside `root`.
|
|
1218
|
+
* Returns `null` when the path escapes the root, is not a readable file, or is
|
|
1219
|
+
* too large — the caller cannot distinguish those, which is the point.
|
|
1220
|
+
*/
|
|
1221
|
+
function readSourceWindow(root, requested, line, context) {
|
|
1222
|
+
const resolved = resolveWithinRoot(root, requested);
|
|
1223
|
+
if (resolved === null) return null;
|
|
1224
|
+
let text;
|
|
1225
|
+
try {
|
|
1226
|
+
const stat = (0, node_fs.statSync)(resolved);
|
|
1227
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) return null;
|
|
1228
|
+
text = (0, node_fs.readFileSync)(resolved, "utf8");
|
|
1229
|
+
} catch {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
const all = text.split("\n");
|
|
1233
|
+
if (all.at(-1) === "") all.pop();
|
|
1234
|
+
const startLine = Math.max(1, line - context);
|
|
1235
|
+
const endLine = Math.min(all.length, line + context);
|
|
1236
|
+
if (startLine > all.length) return null;
|
|
1237
|
+
return {
|
|
1238
|
+
file: node_path.default.relative(node_path.default.resolve(root), resolved),
|
|
1239
|
+
line,
|
|
1240
|
+
startLine,
|
|
1241
|
+
lines: all.slice(startLine - 1, endLine)
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1152
1245
|
//#endregion
|
|
1153
1246
|
//#region src/server/http.ts
|
|
1154
1247
|
function sendOtlpError(res, req, e) {
|
|
@@ -1212,6 +1305,7 @@ function getWidgetJs() {
|
|
|
1212
1305
|
}
|
|
1213
1306
|
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1214
1307
|
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1308
|
+
const sourceRoot = options.sourceRoot;
|
|
1215
1309
|
const fullpageHtml = renderFullpageHtml(options.title);
|
|
1216
1310
|
httpServer.on("request", async (req, res) => {
|
|
1217
1311
|
if (req.headers.upgrade?.toLowerCase() === "websocket") return;
|
|
@@ -1252,6 +1346,31 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
|
1252
1346
|
res.end(DEVTOOLS_FAVICON_SVG);
|
|
1253
1347
|
return;
|
|
1254
1348
|
}
|
|
1349
|
+
if (req.method === "GET" && url.split("?")[0] === "/source") {
|
|
1350
|
+
if (!sourceRoot) {
|
|
1351
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1355
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const query = new URL(url, "http://localhost").searchParams;
|
|
1359
|
+
const file = query.get("file");
|
|
1360
|
+
const line = Number(query.get("line"));
|
|
1361
|
+
const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
|
|
1362
|
+
if (!file || !Number.isInteger(line) || line < 1) {
|
|
1363
|
+
sendJson(res, 400, { error: "file and a positive integer line are required" });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const window = readSourceWindow(sourceRoot, file, line, context);
|
|
1367
|
+
if (window === null) {
|
|
1368
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
sendJson(res, 200, { ...window });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1255
1374
|
if (req.method === "GET" && url === "/healthz") {
|
|
1256
1375
|
sendJson(res, 200, {
|
|
1257
1376
|
ok: true,
|
|
@@ -1438,6 +1557,12 @@ Object.defineProperty(exports, 'probePortHolder', {
|
|
|
1438
1557
|
return probePortHolder;
|
|
1439
1558
|
}
|
|
1440
1559
|
});
|
|
1560
|
+
Object.defineProperty(exports, 'resolveSourceRoot', {
|
|
1561
|
+
enumerable: true,
|
|
1562
|
+
get: function () {
|
|
1563
|
+
return resolveSourceRoot;
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1441
1566
|
Object.defineProperty(exports, 'resolveTelemetryLimits', {
|
|
1442
1567
|
enumerable: true,
|
|
1443
1568
|
get: function () {
|
|
@@ -3,8 +3,8 @@ import { t as getResourceName } from "./resource-utils-B4UVvfnH.js";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { WebSocket, WebSocketServer } from "ws";
|
|
5
5
|
import { ingestAgentEvents, ingestAgentMetrics } from "autotel-agents";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
7
|
+
import path, { dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import protobuf from "protobufjs";
|
|
10
10
|
|
|
@@ -77,7 +77,7 @@ var ErrorAggregator = class {
|
|
|
77
77
|
const exceptionEvent = span.events?.find((e) => e.name === "exception");
|
|
78
78
|
const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
|
|
79
79
|
const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
|
|
80
|
-
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
|
|
80
|
+
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || span.attributes["error.stack"] || this.extractStackFromEvents(span);
|
|
81
81
|
return {
|
|
82
82
|
traceId: trace.traceId,
|
|
83
83
|
spanId: span.spanId,
|
|
@@ -173,7 +173,7 @@ var ErrorAggregator = class {
|
|
|
173
173
|
* Normalize error message by removing dynamic parts
|
|
174
174
|
*/
|
|
175
175
|
normalizeMessage(message) {
|
|
176
|
-
return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\
|
|
176
|
+
return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* Normalize stack trace for display
|
|
@@ -1121,6 +1121,98 @@ async function probePortHolder(host, port, timeoutMs = 500) {
|
|
|
1121
1121
|
}
|
|
1122
1122
|
}
|
|
1123
1123
|
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/server/source-file.ts
|
|
1126
|
+
/** Refuse to slurp something huge just because a span named it. */
|
|
1127
|
+
const MAX_BYTES = 2e6;
|
|
1128
|
+
const DISABLED = /* @__PURE__ */ new Set([
|
|
1129
|
+
"false",
|
|
1130
|
+
"0",
|
|
1131
|
+
"off",
|
|
1132
|
+
"no",
|
|
1133
|
+
""
|
|
1134
|
+
]);
|
|
1135
|
+
/**
|
|
1136
|
+
* Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
|
|
1137
|
+
*
|
|
1138
|
+
* Defaults **on**, at the working directory: devtools is a local tool whose
|
|
1139
|
+
* whole point is showing you your own code, and requiring a flag for that would
|
|
1140
|
+
* mean nobody ever sees the feature. The blast radius stays small because two
|
|
1141
|
+
* other things still hold — the receiver is bound to loopback, and nothing
|
|
1142
|
+
* outside this directory is reachable. Set the variable to `false` to turn it
|
|
1143
|
+
* off outright.
|
|
1144
|
+
*
|
|
1145
|
+
* A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
|
|
1146
|
+
* Origin guard does not replace it: a request with no `Origin` at all — any
|
|
1147
|
+
* `curl` on the network — passes. The root holds whatever else lives in the
|
|
1148
|
+
* project, `.env` included, so the default flips to **off** there. An explicit
|
|
1149
|
+
* root is still honoured: exposing it on purpose is the caller's call.
|
|
1150
|
+
*/
|
|
1151
|
+
function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
|
|
1152
|
+
if (configured === void 0) return loopbackOnly ? cwd : void 0;
|
|
1153
|
+
if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
|
|
1154
|
+
return configured;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Resolve `requested` against `root`, or return `null` if it escapes.
|
|
1158
|
+
*
|
|
1159
|
+
* Containment is judged on **real** paths so a symlink inside the root that
|
|
1160
|
+
* points outside it is rejected — lexical `..` stripping alone cannot see that.
|
|
1161
|
+
* The value returned is the *lexical* resolution, because the real one differs
|
|
1162
|
+
* from the caller's path whenever an ancestor is a symlink (on macOS both
|
|
1163
|
+
* `/tmp` and `/var` are), and a caller comparing paths should not have to know.
|
|
1164
|
+
*/
|
|
1165
|
+
function resolveWithinRoot(root, requested) {
|
|
1166
|
+
const lexicalRoot = path.resolve(root);
|
|
1167
|
+
const realRoot = safeRealpath(lexicalRoot);
|
|
1168
|
+
if (realRoot === null) return null;
|
|
1169
|
+
const lexicalTarget = path.resolve(lexicalRoot, requested);
|
|
1170
|
+
const realTarget = safeRealpath(lexicalTarget);
|
|
1171
|
+
if (realTarget === null) return null;
|
|
1172
|
+
if (!isInside(realRoot, realTarget)) return null;
|
|
1173
|
+
return lexicalTarget;
|
|
1174
|
+
}
|
|
1175
|
+
/** True when `target` is `root` itself or sits beneath it. */
|
|
1176
|
+
function isInside(root, target) {
|
|
1177
|
+
if (target === root) return true;
|
|
1178
|
+
return target.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
|
|
1179
|
+
}
|
|
1180
|
+
function safeRealpath(p) {
|
|
1181
|
+
try {
|
|
1182
|
+
return realpathSync(p);
|
|
1183
|
+
} catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Read `context` lines either side of `line` from a file inside `root`.
|
|
1189
|
+
* Returns `null` when the path escapes the root, is not a readable file, or is
|
|
1190
|
+
* too large — the caller cannot distinguish those, which is the point.
|
|
1191
|
+
*/
|
|
1192
|
+
function readSourceWindow(root, requested, line, context) {
|
|
1193
|
+
const resolved = resolveWithinRoot(root, requested);
|
|
1194
|
+
if (resolved === null) return null;
|
|
1195
|
+
let text;
|
|
1196
|
+
try {
|
|
1197
|
+
const stat = statSync(resolved);
|
|
1198
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) return null;
|
|
1199
|
+
text = readFileSync(resolved, "utf8");
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
const all = text.split("\n");
|
|
1204
|
+
if (all.at(-1) === "") all.pop();
|
|
1205
|
+
const startLine = Math.max(1, line - context);
|
|
1206
|
+
const endLine = Math.min(all.length, line + context);
|
|
1207
|
+
if (startLine > all.length) return null;
|
|
1208
|
+
return {
|
|
1209
|
+
file: path.relative(path.resolve(root), resolved),
|
|
1210
|
+
line,
|
|
1211
|
+
startLine,
|
|
1212
|
+
lines: all.slice(startLine - 1, endLine)
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1124
1216
|
//#endregion
|
|
1125
1217
|
//#region src/server/http.ts
|
|
1126
1218
|
function sendOtlpError(res, req, e) {
|
|
@@ -1184,6 +1276,7 @@ function getWidgetJs() {
|
|
|
1184
1276
|
}
|
|
1185
1277
|
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1186
1278
|
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1279
|
+
const sourceRoot = options.sourceRoot;
|
|
1187
1280
|
const fullpageHtml = renderFullpageHtml(options.title);
|
|
1188
1281
|
httpServer.on("request", async (req, res) => {
|
|
1189
1282
|
if (req.headers.upgrade?.toLowerCase() === "websocket") return;
|
|
@@ -1224,6 +1317,31 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
|
1224
1317
|
res.end(DEVTOOLS_FAVICON_SVG);
|
|
1225
1318
|
return;
|
|
1226
1319
|
}
|
|
1320
|
+
if (req.method === "GET" && url.split("?")[0] === "/source") {
|
|
1321
|
+
if (!sourceRoot) {
|
|
1322
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1326
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const query = new URL(url, "http://localhost").searchParams;
|
|
1330
|
+
const file = query.get("file");
|
|
1331
|
+
const line = Number(query.get("line"));
|
|
1332
|
+
const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
|
|
1333
|
+
if (!file || !Number.isInteger(line) || line < 1) {
|
|
1334
|
+
sendJson(res, 400, { error: "file and a positive integer line are required" });
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const window = readSourceWindow(sourceRoot, file, line, context);
|
|
1338
|
+
if (window === null) {
|
|
1339
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
sendJson(res, 200, { ...window });
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1227
1345
|
if (req.method === "GET" && url === "/healthz") {
|
|
1228
1346
|
sendJson(res, 200, {
|
|
1229
1347
|
ok: true,
|
|
@@ -1296,4 +1414,4 @@ function createDevtoolsHttpServer(devtools, _options = {}) {
|
|
|
1296
1414
|
}
|
|
1297
1415
|
|
|
1298
1416
|
//#endregion
|
|
1299
|
-
export {
|
|
1417
|
+
export { appendManyWithLimit as _, probePortHolder as a, resolveTelemetryLimits as b, decodeOtlpTraceRequest as c, parseOtlpTraces as d, DevtoolsServer as f, originIsLoopback as g, isLoopbackHostname as h, DEVTOOLS_IDENTITY as i, isProtobufContentType as l, hostHeaderIsLoopback as m, createDevtoolsHttpServer as n, decodeOtlpLogsRequest as o, allowSensitiveRequest as p, resolveSourceRoot as r, decodeOtlpMetricsRequest as s, attachDevtoolsRoutes as t, parseOtlpLogs as u, appendWithLimit as v, ErrorAggregator as x, applyTelemetryLimits as y };
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_http = require('./http-
|
|
2
|
+
const require_http = require('./http-D44dITGS.cjs');
|
|
3
3
|
const require_listen = require('./listen-DGzVi7DE.cjs');
|
|
4
4
|
const require_server_exporter = require('./server/exporter.cjs');
|
|
5
5
|
const require_server_log_exporter = require('./server/log-exporter.cjs');
|
|
@@ -21,12 +21,19 @@ function createDevtools(options = {}) {
|
|
|
21
21
|
maxLogCount: options.maxLogCount,
|
|
22
22
|
maxMetricCount: options.maxMetricCount
|
|
23
23
|
});
|
|
24
|
-
require_http.
|
|
24
|
+
const sourceRoot = require_http.resolveSourceRoot(options.sourceRoot === false ? "false" : options.sourceRoot ?? process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
|
|
25
|
+
require_http.attachDevtoolsRoutes(httpServer, wsServer, {
|
|
26
|
+
loopbackOnly,
|
|
27
|
+
sourceRoot
|
|
28
|
+
});
|
|
25
29
|
const listeners = require_listen.listenLoopbackDualStack({
|
|
26
30
|
primary: httpServer,
|
|
27
31
|
port,
|
|
28
32
|
host,
|
|
29
|
-
attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, {
|
|
33
|
+
attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, {
|
|
34
|
+
loopbackOnly,
|
|
35
|
+
sourceRoot
|
|
36
|
+
})
|
|
30
37
|
});
|
|
31
38
|
if (options.verbose) listeners.ready.then(({ warnings }) => {
|
|
32
39
|
for (const w of warnings) console.warn(`[autotel-devtools] ${w}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,14 @@ interface CreateDevtoolsOptions {
|
|
|
12
12
|
maxTraceCount?: number;
|
|
13
13
|
maxLogCount?: number;
|
|
14
14
|
maxMetricCount?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Project root the Errors tab may read source from, so a stack frame can show
|
|
17
|
+
* the line that threw.
|
|
18
|
+
*
|
|
19
|
+
* Same default as the CLI: the working directory on a loopback bind, off
|
|
20
|
+
* otherwise. `false` disables it. See `resolveSourceRoot`.
|
|
21
|
+
*/
|
|
22
|
+
sourceRoot?: string | false;
|
|
15
23
|
}
|
|
16
24
|
interface DevtoolsInstance {
|
|
17
25
|
server: DevtoolsServer;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ interface CreateDevtoolsOptions {
|
|
|
12
12
|
maxTraceCount?: number;
|
|
13
13
|
maxLogCount?: number;
|
|
14
14
|
maxMetricCount?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Project root the Errors tab may read source from, so a stack frame can show
|
|
17
|
+
* the line that threw.
|
|
18
|
+
*
|
|
19
|
+
* Same default as the CLI: the working directory on a loopback bind, off
|
|
20
|
+
* otherwise. `false` disables it. See `resolveSourceRoot`.
|
|
21
|
+
*/
|
|
22
|
+
sourceRoot?: string | false;
|
|
15
23
|
}
|
|
16
24
|
interface DevtoolsInstance {
|
|
17
25
|
server: DevtoolsServer;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as DevtoolsServer, m as hostHeaderIsLoopback, r as resolveSourceRoot, t as attachDevtoolsRoutes, x as ErrorAggregator } from "./http-Ddn7gT0R.js";
|
|
2
2
|
import { t as listenLoopbackDualStack } from "./listen-NVMbBWHw.js";
|
|
3
3
|
import { DevtoolsSpanExporter } from "./server/exporter.js";
|
|
4
4
|
import { DevtoolsLogExporter } from "./server/log-exporter.js";
|
|
@@ -20,12 +20,19 @@ function createDevtools(options = {}) {
|
|
|
20
20
|
maxLogCount: options.maxLogCount,
|
|
21
21
|
maxMetricCount: options.maxMetricCount
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
const sourceRoot = resolveSourceRoot(options.sourceRoot === false ? "false" : options.sourceRoot ?? process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
|
|
24
|
+
attachDevtoolsRoutes(httpServer, wsServer, {
|
|
25
|
+
loopbackOnly,
|
|
26
|
+
sourceRoot
|
|
27
|
+
});
|
|
24
28
|
const listeners = listenLoopbackDualStack({
|
|
25
29
|
primary: httpServer,
|
|
26
30
|
port,
|
|
27
31
|
host,
|
|
28
|
-
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, {
|
|
32
|
+
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, {
|
|
33
|
+
loopbackOnly,
|
|
34
|
+
sourceRoot
|
|
35
|
+
})
|
|
29
36
|
});
|
|
30
37
|
if (options.verbose) listeners.ready.then(({ warnings }) => {
|
|
31
38
|
for (const w of warnings) console.warn(`[autotel-devtools] ${w}`);
|
package/dist/server/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_http = require('../http-
|
|
2
|
+
const require_http = require('../http-D44dITGS.cjs');
|
|
3
3
|
const require_server_exporter = require('./exporter.cjs');
|
|
4
4
|
const require_server_log_exporter = require('./log-exporter.cjs');
|
|
5
5
|
const require_server_remote_exporter = require('./remote-exporter.cjs');
|
package/dist/server/index.d.cts
CHANGED
|
@@ -16,6 +16,15 @@ interface DevtoolsRoutesOptions {
|
|
|
16
16
|
/** Browser tab title for the fullpage UI. Without it every dashboard reads
|
|
17
17
|
* `autotel-devtools`, which is unhelpful with several running at once. */
|
|
18
18
|
title?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Project root that `GET /source` may read from, so a stack frame can show
|
|
21
|
+
* the line that threw.
|
|
22
|
+
*
|
|
23
|
+
* Opt-in and absent by default: without it the route 404s and devtools never
|
|
24
|
+
* touches the filesystem. Reading is confined to this directory, symlinks
|
|
25
|
+
* included — see `resolveWithinRoot`.
|
|
26
|
+
*/
|
|
27
|
+
sourceRoot?: string;
|
|
19
28
|
}
|
|
20
29
|
declare function attachDevtoolsRoutes(httpServer: Server, devtools: DevtoolsServer, options?: DevtoolsRoutesOptions): void;
|
|
21
30
|
declare function createDevtoolsHttpServer(devtools: DevtoolsServer, _options?: HttpServerOptions): Server;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ interface DevtoolsRoutesOptions {
|
|
|
16
16
|
/** Browser tab title for the fullpage UI. Without it every dashboard reads
|
|
17
17
|
* `autotel-devtools`, which is unhelpful with several running at once. */
|
|
18
18
|
title?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Project root that `GET /source` may read from, so a stack frame can show
|
|
21
|
+
* the line that threw.
|
|
22
|
+
*
|
|
23
|
+
* Opt-in and absent by default: without it the route 404s and devtools never
|
|
24
|
+
* touches the filesystem. Reading is confined to this directory, symlinks
|
|
25
|
+
* included — see `resolveWithinRoot`.
|
|
26
|
+
*/
|
|
27
|
+
sourceRoot?: string;
|
|
19
28
|
}
|
|
20
29
|
declare function attachDevtoolsRoutes(httpServer: Server, devtools: DevtoolsServer, options?: DevtoolsRoutesOptions): void;
|
|
21
30
|
declare function createDevtoolsHttpServer(devtools: DevtoolsServer, _options?: HttpServerOptions): Server;
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as appendManyWithLimit, a as probePortHolder, b as resolveTelemetryLimits, c as decodeOtlpTraceRequest, d as parseOtlpTraces, f as DevtoolsServer, g as originIsLoopback, h as isLoopbackHostname, i as DEVTOOLS_IDENTITY, l as isProtobufContentType, m as hostHeaderIsLoopback, n as createDevtoolsHttpServer, o as decodeOtlpLogsRequest, p as allowSensitiveRequest, s as decodeOtlpMetricsRequest, t as attachDevtoolsRoutes, u as parseOtlpLogs, v as appendWithLimit, x as ErrorAggregator, y as applyTelemetryLimits } from "../http-Ddn7gT0R.js";
|
|
2
2
|
import { DevtoolsSpanExporter } from "./exporter.js";
|
|
3
3
|
import { DevtoolsLogExporter } from "./log-exporter.js";
|
|
4
4
|
import { DevtoolsRemoteExporter } from "./remote-exporter.js";
|