autotel-devtools 20.0.1 → 21.0.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.
@@ -25,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  }) : target, mod));
26
26
 
27
27
  //#endregion
28
+ const require_json_fields = require('./json-fields-DZFlNqT2.cjs');
28
29
  const require_trace_root = require('./trace-root-BMa667jK.cjs');
29
30
  const require_resource_utils = require('./resource-utils-DjHJB6uc.cjs');
30
31
  let node_http = require("node:http");
@@ -32,6 +33,7 @@ let ws = require("ws");
32
33
  let autotel_agents = require("autotel-agents");
33
34
  let node_fs = require("node:fs");
34
35
  let node_path = require("node:path");
36
+ node_path = __toESM(node_path, 1);
35
37
  let node_url = require("node:url");
36
38
  let protobufjs = require("protobufjs");
37
39
  protobufjs = __toESM(protobufjs, 1);
@@ -103,9 +105,9 @@ var ErrorAggregator = class {
103
105
  */
104
106
  extractErrorFromSpan(span, trace) {
105
107
  const exceptionEvent = span.events?.find((e) => e.name === "exception");
106
- const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
107
- 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);
108
+ const errorType = require_json_fields.stringAttr(span.attributes, "exception.type", "error.type") ?? require_json_fields.stringAttr(exceptionEvent?.attributes, "exception.type") ?? "Error";
109
+ const errorMessage = span.status.message || require_json_fields.stringAttr(span.attributes, "exception.message", "error.message") || "Unknown error";
110
+ const stackTrace = require_json_fields.stringAttr(span.attributes, "exception.stacktrace", "exception.stack", "error.stack") ?? this.extractStackFromEvents(span);
109
111
  return {
110
112
  traceId: trace.traceId,
111
113
  spanId: span.spanId,
@@ -115,7 +117,8 @@ var ErrorAggregator = class {
115
117
  error: {
116
118
  type: errorType,
117
119
  message: errorMessage,
118
- stackTrace
120
+ stackTrace,
121
+ fingerprint: require_json_fields.stringAttr(span.attributes, "exception.fingerprint") ?? require_json_fields.stringAttr(exceptionEvent?.attributes, "exception.fingerprint")
119
122
  },
120
123
  attributes: this.extractRelevantAttributes(span.attributes)
121
124
  };
@@ -126,7 +129,7 @@ var ErrorAggregator = class {
126
129
  extractStackFromEvents(span) {
127
130
  if (!span.events) return void 0;
128
131
  const exceptionEvent = span.events.find((e) => e.name === "exception");
129
- if (exceptionEvent?.attributes) return exceptionEvent.attributes["exception.stacktrace"] || exceptionEvent.attributes["exception.stack"];
132
+ if (exceptionEvent?.attributes) return require_json_fields.stringAttr(exceptionEvent.attributes, "exception.stacktrace", "exception.stack");
130
133
  }
131
134
  /**
132
135
  * Extract relevant attributes for error context
@@ -155,6 +158,7 @@ var ErrorAggregator = class {
155
158
  * Uses error type + first N stack frames (normalized)
156
159
  */
157
160
  generateFingerprint(occurrence) {
161
+ if (occurrence.error.fingerprint) return occurrence.error.fingerprint;
158
162
  const parts = [occurrence.error.type];
159
163
  if (occurrence.error.stackTrace) {
160
164
  const frames = this.extractStackFrames(occurrence.error.stackTrace, this.options.stackFramesForFingerprint);
@@ -558,13 +562,28 @@ var DevtoolsServer = class {
558
562
  }
559
563
  };
560
564
 
565
+ //#endregion
566
+ //#region src/server/otlp-types.ts
567
+ /**
568
+ * An exporter's payload, read as the envelope it claims to be.
569
+ *
570
+ * SAFETY: this is the one place the receiver trusts the wire. Every field of
571
+ * every envelope above is optional, so a payload that is not what it claims
572
+ * reads back as empty arrays and undefined fields rather than throwing - which
573
+ * is what the callers below rely on when they find no spans to add.
574
+ */
575
+ function otlpEnvelope(payload) {
576
+ if (typeof payload !== "object" || payload === null) return void 0;
577
+ return payload;
578
+ }
579
+
561
580
  //#endregion
562
581
  //#region src/server/otlp.ts
563
582
  function resolveOtlpValue(v) {
564
583
  if (!v) return void 0;
565
584
  if (v.stringValue !== void 0) return v.stringValue;
566
585
  if (v.boolValue !== void 0) return v.boolValue;
567
- if (v.intValue !== void 0) return typeof v.intValue === "string" ? Number(v.intValue) : v.intValue;
586
+ if (v.intValue !== void 0) return Number(v.intValue);
568
587
  if (v.doubleValue !== void 0) return v.doubleValue;
569
588
  if (v.bytesValue !== void 0) return v.bytesValue;
570
589
  if (v.arrayValue?.values) return v.arrayValue.values.map(resolveOtlpValue);
@@ -576,6 +595,29 @@ function flattenAttributes(attrs) {
576
595
  for (const { key, value } of attrs) out[key] = resolveOtlpValue(value);
577
596
  return out;
578
597
  }
598
+ /**
599
+ * A log record's body: the text it carried, or the structure it carried when
600
+ * the sender used an OTLP kvlist or array rather than a string.
601
+ */
602
+ function logBody(body) {
603
+ const text = require_json_fields.asString(body);
604
+ if (text !== void 0) return text;
605
+ if (body === void 0 || body === null) return "";
606
+ const structured = require_json_fields.asObject(body);
607
+ if (!structured) return String(body);
608
+ return structured;
609
+ }
610
+ /**
611
+ * Attributes handed to the agent layer, whose `Attributes` is OTel's own -
612
+ * scalars and arrays of scalars, nothing nested.
613
+ *
614
+ * SAFETY: a coding agent's metrics and events carry scalar attributes only,
615
+ * so the two shapes agree in practice. A sender that nests one anyway is
616
+ * rendered by the Agents tab as whatever it is rather than being dropped.
617
+ */
618
+ function agentAttributes(attributes) {
619
+ return attributes;
620
+ }
579
621
  function nanoToMs(nano) {
580
622
  if (!nano) return 0;
581
623
  const ns = BigInt(nano);
@@ -583,19 +625,19 @@ function nanoToMs(nano) {
583
625
  const remNs = ns % 1000000n;
584
626
  return Number(ms) + Number(remNs) / 1e6;
585
627
  }
586
- const SPAN_KIND_MAP = {
587
- 0: "INTERNAL",
588
- 1: "INTERNAL",
589
- 2: "SERVER",
590
- 3: "CLIENT",
591
- 4: "PRODUCER",
592
- 5: "CONSUMER",
593
- SPAN_KIND_INTERNAL: "INTERNAL",
594
- SPAN_KIND_SERVER: "SERVER",
595
- SPAN_KIND_CLIENT: "CLIENT",
596
- SPAN_KIND_PRODUCER: "PRODUCER",
597
- SPAN_KIND_CONSUMER: "CONSUMER"
598
- };
628
+ const SPAN_KIND_MAP = /* @__PURE__ */ new Map([
629
+ [0, "INTERNAL"],
630
+ [1, "INTERNAL"],
631
+ [2, "SERVER"],
632
+ [3, "CLIENT"],
633
+ [4, "PRODUCER"],
634
+ [5, "CONSUMER"],
635
+ ["SPAN_KIND_INTERNAL", "INTERNAL"],
636
+ ["SPAN_KIND_SERVER", "SERVER"],
637
+ ["SPAN_KIND_CLIENT", "CLIENT"],
638
+ ["SPAN_KIND_PRODUCER", "PRODUCER"],
639
+ ["SPAN_KIND_CONSUMER", "CONSUMER"]
640
+ ]);
599
641
  function normalizeHexId(id) {
600
642
  if (!id) return "";
601
643
  if (/^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id) && (id.length === 12 || id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48)) try {
@@ -604,15 +646,13 @@ function normalizeHexId(id) {
604
646
  return id;
605
647
  }
606
648
  function parseOtlpTraces(payload) {
607
- if (!payload || typeof payload !== "object") return [];
608
- const { resourceSpans } = payload;
609
- if (!Array.isArray(resourceSpans) || resourceSpans.length === 0) return [];
649
+ const resourceSpans = otlpEnvelope(payload)?.resourceSpans;
650
+ if (!resourceSpans || resourceSpans.length === 0) return [];
610
651
  const traceMap = /* @__PURE__ */ new Map();
611
652
  for (const rs of resourceSpans) {
612
653
  const resourceAttrs = flattenAttributes(rs.resource?.attributes);
613
654
  const service = String(resourceAttrs["service.name"] || "unknown");
614
- const scopeSpans = rs.scopeSpans || [];
615
- for (const ss of scopeSpans) {
655
+ for (const ss of rs.scopeSpans ?? []) {
616
656
  const scope = ss.scope?.name ? {
617
657
  name: ss.scope.name,
618
658
  version: ss.scope.version || void 0
@@ -631,7 +671,7 @@ function parseOtlpTraces(payload) {
631
671
  spanId: normalizeHexId(span.spanId),
632
672
  parentSpanId: normalizeHexId(span.parentSpanId) || void 0,
633
673
  name: span.name || "unknown",
634
- kind: SPAN_KIND_MAP[span.kind ?? 0] || "INTERNAL",
674
+ kind: SPAN_KIND_MAP.get(span.kind ?? 0) ?? "INTERNAL",
635
675
  startTime: startMs,
636
676
  endTime: endMs,
637
677
  duration: endMs - startMs,
@@ -643,12 +683,12 @@ function parseOtlpTraces(payload) {
643
683
  code: status,
644
684
  message: span.status?.message
645
685
  },
646
- events: (span.events || []).map((e) => ({
686
+ events: (span.events ?? []).map((e) => ({
647
687
  name: e.name || "",
648
688
  timestamp: nanoToMs(e.timeUnixNano),
649
689
  attributes: flattenAttributes(e.attributes)
650
690
  })),
651
- links: (span.links || []).map((l) => ({
691
+ links: (span.links ?? []).map((l) => ({
652
692
  traceId: normalizeHexId(l.traceId),
653
693
  spanId: normalizeHexId(l.spanId),
654
694
  attributes: flattenAttributes(l.attributes)
@@ -671,7 +711,7 @@ function parseOtlpTraces(payload) {
671
711
  const startTime = Math.min(...sorted.map((s) => s.startTime));
672
712
  const endTime = Math.max(...sorted.map((s) => s.endTime));
673
713
  const hasError = sorted.some((s) => s.status.code === "ERROR");
674
- traces.push({
714
+ const trace = {
675
715
  traceId,
676
716
  correlationId: traceId.slice(0, 16),
677
717
  rootSpan,
@@ -680,20 +720,20 @@ function parseOtlpTraces(payload) {
680
720
  endTime,
681
721
  duration: endTime - startTime,
682
722
  status: hasError ? "ERROR" : "OK",
683
- service,
684
- ...partial ? { partial: true } : {}
685
- });
723
+ service
724
+ };
725
+ if (partial) trace.partial = true;
726
+ traces.push(trace);
686
727
  }
687
728
  return traces;
688
729
  }
689
730
  function parseOtlpLogs(payload) {
690
- if (!payload || typeof payload !== "object") return [];
691
- const { resourceLogs } = payload;
692
- if (!Array.isArray(resourceLogs)) return [];
731
+ const resourceLogs = otlpEnvelope(payload)?.resourceLogs;
732
+ if (!resourceLogs) return [];
693
733
  const logs = [];
694
734
  for (const rl of resourceLogs) {
695
735
  const resourceAttrs = flattenAttributes(rl.resource?.attributes);
696
- for (const sl of rl.scopeLogs || []) for (const rec of sl.logRecords || []) {
736
+ for (const sl of rl.scopeLogs ?? []) for (const rec of sl.logRecords ?? []) {
697
737
  const timestamp = nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano);
698
738
  const traceId = normalizeHexId(rec.traceId) || void 0;
699
739
  const spanId = normalizeHexId(rec.spanId) || void 0;
@@ -705,7 +745,7 @@ function parseOtlpLogs(payload) {
705
745
  resourceName: require_resource_utils.getResourceName(resourceAttrs),
706
746
  severityText: rec.severityText,
707
747
  severityNumber: rec.severityNumber,
708
- body: typeof body === "string" ? body : body,
748
+ body: logBody(body),
709
749
  timestamp,
710
750
  attributes: flattenAttributes(rec.attributes),
711
751
  resource: resourceAttrs
@@ -715,11 +755,10 @@ function parseOtlpLogs(payload) {
715
755
  return logs;
716
756
  }
717
757
  function countOtlpMetrics(payload) {
718
- if (!payload || typeof payload !== "object") return 0;
719
- const { resourceMetrics } = payload;
720
- if (!Array.isArray(resourceMetrics)) return 0;
758
+ const resourceMetrics = otlpEnvelope(payload)?.resourceMetrics;
759
+ if (!resourceMetrics) return 0;
721
760
  let count = 0;
722
- for (const rm of resourceMetrics) for (const sm of rm.scopeMetrics || []) count += (sm.metrics || []).length;
761
+ for (const rm of resourceMetrics) for (const sm of rm.scopeMetrics ?? []) count += (sm.metrics ?? []).length;
723
762
  return count;
724
763
  }
725
764
  function extractDataPoints(metric) {
@@ -729,14 +768,14 @@ function extractDataPoints(metric) {
729
768
  const value = dp.asDouble !== void 0 ? Number(dp.asDouble) : dp.asInt !== void 0 ? Number(dp.asInt) : 0;
730
769
  points.push({
731
770
  value,
732
- attributes: flattenAttributes(dp.attributes),
771
+ attributes: agentAttributes(flattenAttributes(dp.attributes)),
733
772
  timestamp: nanoToMs(dp.timeUnixNano || dp.startTimeUnixNano)
734
773
  });
735
774
  }
736
775
  const histPoints = metric.histogram?.dataPoints;
737
776
  if (Array.isArray(histPoints)) for (const dp of histPoints) points.push({
738
777
  value: dp.count !== void 0 ? Number(dp.count) : 0,
739
- attributes: flattenAttributes(dp.attributes),
778
+ attributes: agentAttributes(flattenAttributes(dp.attributes)),
740
779
  timestamp: nanoToMs(dp.timeUnixNano || dp.startTimeUnixNano)
741
780
  });
742
781
  return points;
@@ -752,19 +791,18 @@ function readTemporality(metric) {
752
791
  if (raw === 1 || raw === "AGGREGATION_TEMPORALITY_DELTA") return "delta";
753
792
  }
754
793
  function parseOtlpMetrics(payload) {
755
- if (!payload || typeof payload !== "object") return [];
756
- const { resourceMetrics } = payload;
757
- if (!Array.isArray(resourceMetrics)) return [];
794
+ const resourceMetrics = otlpEnvelope(payload)?.resourceMetrics;
795
+ if (!resourceMetrics) return [];
758
796
  const records = [];
759
797
  for (const rm of resourceMetrics) {
760
- const resource = flattenAttributes(rm.resource?.attributes);
761
- for (const sm of rm.scopeMetrics || []) {
798
+ const resource = agentAttributes(flattenAttributes(rm.resource?.attributes));
799
+ for (const sm of rm.scopeMetrics ?? []) {
762
800
  const scope = sm.scope?.name ? {
763
801
  name: sm.scope.name,
764
802
  version: sm.scope.version || void 0
765
803
  } : void 0;
766
- for (const metric of sm.metrics || []) records.push({
767
- name: metric.name,
804
+ for (const metric of sm.metrics ?? []) records.push({
805
+ name: metric.name ?? "",
768
806
  unit: metric.unit || void 0,
769
807
  description: metric.description || void 0,
770
808
  temporality: readTemporality(metric),
@@ -783,20 +821,19 @@ function parseOtlpMetrics(payload) {
783
821
  * `parseOtlpLogs`, which feeds the generic Logs tab.
784
822
  */
785
823
  function parseOtlpAgentEvents(payload) {
786
- if (!payload || typeof payload !== "object") return [];
787
- const { resourceLogs } = payload;
788
- if (!Array.isArray(resourceLogs)) return [];
824
+ const resourceLogs = otlpEnvelope(payload)?.resourceLogs;
825
+ if (!resourceLogs) return [];
789
826
  const events = [];
790
827
  for (const rl of resourceLogs) {
791
- const resource = flattenAttributes(rl.resource?.attributes);
792
- for (const sl of rl.scopeLogs || []) {
828
+ const resource = agentAttributes(flattenAttributes(rl.resource?.attributes));
829
+ for (const sl of rl.scopeLogs ?? []) {
793
830
  const scope = sl.scope?.name ? {
794
831
  name: sl.scope.name,
795
832
  version: sl.scope.version || void 0
796
833
  } : void 0;
797
- for (const rec of sl.logRecords || []) {
798
- const attributes = flattenAttributes(rec.attributes);
799
- const eventName = typeof rec.eventName === "string" && rec.eventName || String(attributes["event.name"] ?? "");
834
+ for (const rec of sl.logRecords ?? []) {
835
+ const attributes = agentAttributes(flattenAttributes(rec.attributes));
836
+ const eventName = rec.eventName || String(attributes["event.name"] ?? "");
800
837
  events.push({
801
838
  eventName,
802
839
  timestamp: nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano),
@@ -1149,6 +1186,98 @@ async function probePortHolder(host, port, timeoutMs = 500) {
1149
1186
  }
1150
1187
  }
1151
1188
 
1189
+ //#endregion
1190
+ //#region src/server/source-file.ts
1191
+ /** Refuse to slurp something huge just because a span named it. */
1192
+ const MAX_BYTES = 2e6;
1193
+ const DISABLED = /* @__PURE__ */ new Set([
1194
+ "false",
1195
+ "0",
1196
+ "off",
1197
+ "no",
1198
+ ""
1199
+ ]);
1200
+ /**
1201
+ * Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
1202
+ *
1203
+ * Defaults **on**, at the working directory: devtools is a local tool whose
1204
+ * whole point is showing you your own code, and requiring a flag for that would
1205
+ * mean nobody ever sees the feature. The blast radius stays small because two
1206
+ * other things still hold — the receiver is bound to loopback, and nothing
1207
+ * outside this directory is reachable. Set the variable to `false` to turn it
1208
+ * off outright.
1209
+ *
1210
+ * A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
1211
+ * Origin guard does not replace it: a request with no `Origin` at all — any
1212
+ * `curl` on the network — passes. The root holds whatever else lives in the
1213
+ * project, `.env` included, so the default flips to **off** there. An explicit
1214
+ * root is still honoured: exposing it on purpose is the caller's call.
1215
+ */
1216
+ function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
1217
+ if (configured === void 0) return loopbackOnly ? cwd : void 0;
1218
+ if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
1219
+ return configured;
1220
+ }
1221
+ /**
1222
+ * Resolve `requested` against `root`, or return `null` if it escapes.
1223
+ *
1224
+ * Containment is judged on **real** paths so a symlink inside the root that
1225
+ * points outside it is rejected — lexical `..` stripping alone cannot see that.
1226
+ * The value returned is the *lexical* resolution, because the real one differs
1227
+ * from the caller's path whenever an ancestor is a symlink (on macOS both
1228
+ * `/tmp` and `/var` are), and a caller comparing paths should not have to know.
1229
+ */
1230
+ function resolveWithinRoot(root, requested) {
1231
+ const lexicalRoot = node_path.default.resolve(root);
1232
+ const realRoot = safeRealpath(lexicalRoot);
1233
+ if (realRoot === null) return null;
1234
+ const lexicalTarget = node_path.default.resolve(lexicalRoot, requested);
1235
+ const realTarget = safeRealpath(lexicalTarget);
1236
+ if (realTarget === null) return null;
1237
+ if (!isInside(realRoot, realTarget)) return null;
1238
+ return lexicalTarget;
1239
+ }
1240
+ /** True when `target` is `root` itself or sits beneath it. */
1241
+ function isInside(root, target) {
1242
+ if (target === root) return true;
1243
+ return target.startsWith(root.endsWith(node_path.default.sep) ? root : root + node_path.default.sep);
1244
+ }
1245
+ function safeRealpath(p) {
1246
+ try {
1247
+ return (0, node_fs.realpathSync)(p);
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Read `context` lines either side of `line` from a file inside `root`.
1254
+ * Returns `null` when the path escapes the root, is not a readable file, or is
1255
+ * too large — the caller cannot distinguish those, which is the point.
1256
+ */
1257
+ function readSourceWindow(root, requested, line, context) {
1258
+ const resolved = resolveWithinRoot(root, requested);
1259
+ if (resolved === null) return null;
1260
+ let text;
1261
+ try {
1262
+ const stat = (0, node_fs.statSync)(resolved);
1263
+ if (!stat.isFile() || stat.size > MAX_BYTES) return null;
1264
+ text = (0, node_fs.readFileSync)(resolved, "utf8");
1265
+ } catch {
1266
+ return null;
1267
+ }
1268
+ const all = text.split("\n");
1269
+ if (all.at(-1) === "") all.pop();
1270
+ const startLine = Math.max(1, line - context);
1271
+ const endLine = Math.min(all.length, line + context);
1272
+ if (startLine > all.length) return null;
1273
+ return {
1274
+ file: node_path.default.relative(node_path.default.resolve(root), resolved),
1275
+ line,
1276
+ startLine,
1277
+ lines: all.slice(startLine - 1, endLine)
1278
+ };
1279
+ }
1280
+
1152
1281
  //#endregion
1153
1282
  //#region src/server/http.ts
1154
1283
  function sendOtlpError(res, req, e) {
@@ -1212,6 +1341,7 @@ function getWidgetJs() {
1212
1341
  }
1213
1342
  function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1214
1343
  const loopbackOnly = options.loopbackOnly ?? true;
1344
+ const sourceRoot = options.sourceRoot;
1215
1345
  const fullpageHtml = renderFullpageHtml(options.title);
1216
1346
  httpServer.on("request", async (req, res) => {
1217
1347
  if (req.headers.upgrade?.toLowerCase() === "websocket") return;
@@ -1252,6 +1382,31 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1252
1382
  res.end(DEVTOOLS_FAVICON_SVG);
1253
1383
  return;
1254
1384
  }
1385
+ if (req.method === "GET" && url.split("?")[0] === "/source") {
1386
+ if (!sourceRoot) {
1387
+ sendJson(res, 404, { error: "Not found" });
1388
+ return;
1389
+ }
1390
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1391
+ sendJson(res, 403, { error: "Forbidden" });
1392
+ return;
1393
+ }
1394
+ const query = new URL(url, "http://localhost").searchParams;
1395
+ const file = query.get("file");
1396
+ const line = Number(query.get("line"));
1397
+ const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
1398
+ if (!file || !Number.isInteger(line) || line < 1) {
1399
+ sendJson(res, 400, { error: "file and a positive integer line are required" });
1400
+ return;
1401
+ }
1402
+ const window = readSourceWindow(sourceRoot, file, line, context);
1403
+ if (window === null) {
1404
+ sendJson(res, 404, { error: "Not found" });
1405
+ return;
1406
+ }
1407
+ sendJson(res, 200, { ...window });
1408
+ return;
1409
+ }
1255
1410
  if (req.method === "GET" && url === "/healthz") {
1256
1411
  sendJson(res, 200, {
1257
1412
  ok: true,
@@ -1438,6 +1593,12 @@ Object.defineProperty(exports, 'probePortHolder', {
1438
1593
  return probePortHolder;
1439
1594
  }
1440
1595
  });
1596
+ Object.defineProperty(exports, 'resolveSourceRoot', {
1597
+ enumerable: true,
1598
+ get: function () {
1599
+ return resolveSourceRoot;
1600
+ }
1601
+ });
1441
1602
  Object.defineProperty(exports, 'resolveTelemetryLimits', {
1442
1603
  enumerable: true,
1443
1604
  get: function () {
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_http = require('./http-j-oBI6r6.cjs');
3
- const require_listen = require('./listen-DGzVi7DE.cjs');
2
+ const require_http = require('./http-CXSzX4ee.cjs');
3
+ const require_listen = require('./listen-CEJ3nYJf.cjs');
4
4
  const require_server_exporter = require('./server/exporter.cjs');
5
5
  const require_server_log_exporter = require('./server/log-exporter.cjs');
6
6
  const require_server_remote_exporter = require('./server/remote-exporter.cjs');
@@ -21,12 +21,19 @@ function createDevtools(options = {}) {
21
21
  maxLogCount: options.maxLogCount,
22
22
  maxMetricCount: options.maxMetricCount
23
23
  });
24
- require_http.attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
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, { loopbackOnly })
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
@@ -1,7 +1,8 @@
1
- import { a as ErrorGroup, c as MetricData, i as DevtoolsData, l as SpanData, n as DevtoolsServer, s as LogData, t as DevtoolsSpanExporter, u as TraceData } from "./exporter-CEKKJTci.cjs";
1
+ import { n as SpanAttributes, t as AttributeValue } from "./types-DHDvZnnn.cjs";
2
+ import { a as ErrorGroup, c as MetricData, i as DevtoolsData, l as SpanData, n as DevtoolsServer, s as LogData, t as DevtoolsSpanExporter, u as TraceData } from "./exporter-Dt4kx128.cjs";
2
3
  import { DevtoolsLogExporter } from "./server/log-exporter.cjs";
3
4
  import { DevtoolsRemoteExporter } from "./server/remote-exporter.cjs";
4
- import { t as ErrorAggregator } from "./error-aggregator-CGgWr2OH.cjs";
5
+ import { t as ErrorAggregator } from "./error-aggregator-DCEKMOm3.cjs";
5
6
  import { Server } from "node:http";
6
7
  //#region src/index.d.ts
7
8
  interface CreateDevtoolsOptions {
@@ -12,6 +13,14 @@ interface CreateDevtoolsOptions {
12
13
  maxTraceCount?: number;
13
14
  maxLogCount?: number;
14
15
  maxMetricCount?: number;
16
+ /**
17
+ * Project root the Errors tab may read source from, so a stack frame can show
18
+ * the line that threw.
19
+ *
20
+ * Same default as the CLI: the working directory on a loopback bind, off
21
+ * otherwise. `false` disables it. See `resolveSourceRoot`.
22
+ */
23
+ sourceRoot?: string | false;
15
24
  }
16
25
  interface DevtoolsInstance {
17
26
  server: DevtoolsServer;
@@ -22,4 +31,4 @@ interface DevtoolsInstance {
22
31
  }
23
32
  declare function createDevtools(options?: CreateDevtoolsOptions): DevtoolsInstance;
24
33
  //#endregion
25
- export { CreateDevtoolsOptions, type DevtoolsData, DevtoolsInstance, DevtoolsLogExporter, DevtoolsRemoteExporter, DevtoolsServer, DevtoolsSpanExporter, ErrorAggregator, type ErrorGroup, type LogData, type MetricData, type SpanData, type TraceData, createDevtools };
34
+ export { type AttributeValue, CreateDevtoolsOptions, type DevtoolsData, DevtoolsInstance, DevtoolsLogExporter, DevtoolsRemoteExporter, DevtoolsServer, DevtoolsSpanExporter, ErrorAggregator, type ErrorGroup, type LogData, type MetricData, type SpanAttributes, type SpanData, type TraceData, createDevtools };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { a as ErrorGroup, c as MetricData, i as DevtoolsData, l as SpanData, n as DevtoolsServer, s as LogData, t as DevtoolsSpanExporter, u as TraceData } from "./exporter-BJwufQwg.js";
1
+ import { n as SpanAttributes, t as AttributeValue } from "./types-DHDvZnnn.js";
2
+ import { a as ErrorGroup, c as MetricData, i as DevtoolsData, l as SpanData, n as DevtoolsServer, s as LogData, t as DevtoolsSpanExporter, u as TraceData } from "./exporter-Due9Rd4s.js";
2
3
  import { DevtoolsLogExporter } from "./server/log-exporter.js";
3
4
  import { DevtoolsRemoteExporter } from "./server/remote-exporter.js";
4
- import { t as ErrorAggregator } from "./error-aggregator-DufjI2IG.js";
5
+ import { t as ErrorAggregator } from "./error-aggregator-D8VKciLY.js";
5
6
  import { Server } from "node:http";
6
7
  //#region src/index.d.ts
7
8
  interface CreateDevtoolsOptions {
@@ -12,6 +13,14 @@ interface CreateDevtoolsOptions {
12
13
  maxTraceCount?: number;
13
14
  maxLogCount?: number;
14
15
  maxMetricCount?: number;
16
+ /**
17
+ * Project root the Errors tab may read source from, so a stack frame can show
18
+ * the line that threw.
19
+ *
20
+ * Same default as the CLI: the working directory on a loopback bind, off
21
+ * otherwise. `false` disables it. See `resolveSourceRoot`.
22
+ */
23
+ sourceRoot?: string | false;
15
24
  }
16
25
  interface DevtoolsInstance {
17
26
  server: DevtoolsServer;
@@ -22,4 +31,4 @@ interface DevtoolsInstance {
22
31
  }
23
32
  declare function createDevtools(options?: CreateDevtoolsOptions): DevtoolsInstance;
24
33
  //#endregion
25
- export { CreateDevtoolsOptions, type DevtoolsData, DevtoolsInstance, DevtoolsLogExporter, DevtoolsRemoteExporter, DevtoolsServer, DevtoolsSpanExporter, ErrorAggregator, type ErrorGroup, type LogData, type MetricData, type SpanData, type TraceData, createDevtools };
34
+ export { type AttributeValue, CreateDevtoolsOptions, type DevtoolsData, DevtoolsInstance, DevtoolsLogExporter, DevtoolsRemoteExporter, DevtoolsServer, DevtoolsSpanExporter, ErrorAggregator, type ErrorGroup, type LogData, type MetricData, type SpanAttributes, type SpanData, type TraceData, createDevtools };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { b as ErrorAggregator, d as DevtoolsServer, p as hostHeaderIsLoopback, t as attachDevtoolsRoutes } from "./http-uVPjDDld.js";
2
- import { t as listenLoopbackDualStack } from "./listen-NVMbBWHw.js";
1
+ import { f as DevtoolsServer, m as hostHeaderIsLoopback, r as resolveSourceRoot, t as attachDevtoolsRoutes, x as ErrorAggregator } from "./http-CNZMrnzv.js";
2
+ import { t as listenLoopbackDualStack } from "./listen-DBfsfcdd.js";
3
3
  import { DevtoolsSpanExporter } from "./server/exporter.js";
4
4
  import { DevtoolsLogExporter } from "./server/log-exporter.js";
5
5
  import { DevtoolsRemoteExporter } from "./server/remote-exporter.js";
@@ -20,12 +20,19 @@ function createDevtools(options = {}) {
20
20
  maxLogCount: options.maxLogCount,
21
21
  maxMetricCount: options.maxMetricCount
22
22
  });
23
- attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
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, { loopbackOnly })
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}`);
@@ -0,0 +1,47 @@
1
+ //#region src/widget/attrs.ts
2
+ /** The string an attribute carries, or undefined when it carried something else. */
3
+ function stringAttr(attributes, ...keys) {
4
+ for (const key of keys) {
5
+ const value = attributes?.[key];
6
+ if (typeof value === "string" && value.length > 0) return value;
7
+ }
8
+ }
9
+ /** The string a wire value carries, or undefined when it carried something else. */
10
+ function asString(value) {
11
+ return typeof value === "string" ? value : void 0;
12
+ }
13
+ /**
14
+ * The number a wire value carries. A numeric string counts: instrumentations
15
+ * routinely send counts and durations as strings.
16
+ */
17
+ function asNumber(value) {
18
+ if (typeof value === "number") return value;
19
+ if (typeof value === "string" && value !== "") {
20
+ const parsed = Number(value);
21
+ return Number.isFinite(parsed) ? parsed : void 0;
22
+ }
23
+ }
24
+ /** The boolean a wire value carries, including the strings 'true' and 'false'. */
25
+ function asBoolean(value) {
26
+ if (typeof value === "boolean") return value;
27
+ if (value === "true") return true;
28
+ if (value === "false") return false;
29
+ }
30
+ /** The strings a wire value carries - a list of them, or a lone one. */
31
+ function asStringArray(value) {
32
+ if (Array.isArray(value)) {
33
+ const strings = value.filter((item) => typeof item === "string");
34
+ return strings.length === value.length ? strings : void 0;
35
+ }
36
+ return typeof value === "string" ? [value] : void 0;
37
+ }
38
+
39
+ //#endregion
40
+ //#region src/widget/utils/json-fields.ts
41
+ /** The value as an object, or `undefined` when it is anything else. */
42
+ function asObject(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
44
+ }
45
+
46
+ //#endregion
47
+ export { asStringArray as a, asString as i, asBoolean as n, stringAttr as o, asNumber as r, asObject as t };