@neat.is/core 0.5.4-dev.20260721 → 0.6.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/dist/neatd.js CHANGED
@@ -2,15 +2,15 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-VR73QNJD.js";
5
+ } from "./chunk-3YUNROLT.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-PEFX3DBR.js";
9
+ } from "./chunk-7C6ZULNU.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
13
- } from "./chunk-I4NZ7PSN.js";
13
+ } from "./chunk-BZ3AJVAC.js";
14
14
 
15
15
  // src/neatd.ts
16
16
  import { promises as fs } from "fs";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  reshapeGrpcRequest,
3
3
  startOtelGrpcReceiver
4
- } from "./chunk-4RU3AAOI.js";
5
- import "./chunk-I4NZ7PSN.js";
4
+ } from "./chunk-MVINCLQM.js";
5
+ import "./chunk-BZ3AJVAC.js";
6
6
  export {
7
7
  reshapeGrpcRequest,
8
8
  startOtelGrpcReceiver
9
9
  };
10
- //# sourceMappingURL=otel-grpc-TEGOXE6T.js.map
10
+ //# sourceMappingURL=otel-grpc-XS45HBET.js.map
package/dist/server.cjs CHANGED
@@ -340,6 +340,11 @@ function pickEnv(spanAttrs, resourceAttrs) {
340
340
  }
341
341
  return ENV_FALLBACK;
342
342
  }
343
+ function normalizeDbSystem(attrs) {
344
+ const raw = attrs["db.system"];
345
+ if (typeof raw !== "string") return void 0;
346
+ return raw === "mongoose" ? "mongodb" : raw;
347
+ }
343
348
  function messagingDestinationOf(attrs) {
344
349
  for (const key of ["messaging.destination.name", "messaging.destination"]) {
345
350
  const v = attrs[key];
@@ -394,7 +399,7 @@ function parseOtlpRequest(body) {
394
399
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
395
400
  env: pickEnv(attrs, resourceAttrs),
396
401
  attributes: attrs,
397
- dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
402
+ dbSystem: normalizeDbSystem(attrs),
398
403
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
399
404
  dbCollection: typeof attrs["db.collection.name"] === "string" ? attrs["db.collection.name"] : typeof attrs["db.mongodb.collection"] === "string" ? attrs["db.mongodb.collection"] : void 0,
400
405
  messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
@@ -5858,6 +5863,7 @@ init_cjs_shims();
5858
5863
  var import_node_path24 = __toESM(require("path"), 1);
5859
5864
  var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5860
5865
  var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5866
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5861
5867
  var import_types12 = require("@neat.is/types");
5862
5868
  var PARSE_CHUNK2 = 16384;
5863
5869
  function parseSource2(parser, source) {
@@ -5870,6 +5876,11 @@ function makeJsParser2() {
5870
5876
  p.setLanguage(import_tree_sitter_javascript2.default);
5871
5877
  return p;
5872
5878
  }
5879
+ function makePyParser2() {
5880
+ const p = new import_tree_sitter2.default();
5881
+ p.setLanguage(import_tree_sitter_python2.default);
5882
+ return p;
5883
+ }
5873
5884
  var ROUTER_METHODS = /* @__PURE__ */ new Set([
5874
5885
  "get",
5875
5886
  "post",
@@ -5882,6 +5893,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5882
5893
  ]);
5883
5894
  var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5884
5895
  var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5896
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
5885
5897
  function canonicalizeTemplate(raw) {
5886
5898
  let p = raw.split("?")[0].split("#")[0];
5887
5899
  if (!p.startsWith("/")) p = "/" + p;
@@ -6109,8 +6121,102 @@ function nextRoutesFromFile(source, relFile, parser) {
6109
6121
  }
6110
6122
  return [];
6111
6123
  }
6124
+ function pyStaticStringText(node) {
6125
+ if (node.type !== "string") return null;
6126
+ for (let i = 0; i < node.namedChildCount; i++) {
6127
+ const child = node.namedChild(i);
6128
+ if (child?.type === "interpolation") return null;
6129
+ if (child?.type === "string_content") return child.text;
6130
+ }
6131
+ return "";
6132
+ }
6133
+ function keywordArrayStrings(argsNode, key) {
6134
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
6135
+ const arg = argsNode.namedChild(i);
6136
+ if (arg?.type !== "keyword_argument") continue;
6137
+ if (arg.childForFieldName("name")?.text !== key) continue;
6138
+ const val = arg.childForFieldName("value");
6139
+ if (!val || val.type !== "list") return [];
6140
+ const out = [];
6141
+ for (let j = 0; j < val.namedChildCount; j++) {
6142
+ const el = val.namedChild(j);
6143
+ if (el?.type === "string") {
6144
+ const s = pyStaticStringText(el);
6145
+ if (s) out.push(s);
6146
+ }
6147
+ }
6148
+ return out;
6149
+ }
6150
+ return [];
6151
+ }
6152
+ function collectApiRouterPrefixes(root) {
6153
+ const prefixes = /* @__PURE__ */ new Map();
6154
+ walk(root, (node) => {
6155
+ if (node.type !== "assignment") return;
6156
+ const right = node.childForFieldName("right");
6157
+ if (!right || right.type !== "call") return;
6158
+ const fn = right.childForFieldName("function");
6159
+ if (!fn) return;
6160
+ const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
6161
+ if (ctor !== "APIRouter") return;
6162
+ const left = node.childForFieldName("left");
6163
+ if (!left || left.type !== "identifier") return;
6164
+ const args = right.childForFieldName("arguments");
6165
+ if (!args) return;
6166
+ for (let i = 0; i < args.namedChildCount; i++) {
6167
+ const arg = args.namedChild(i);
6168
+ if (arg?.type !== "keyword_argument") continue;
6169
+ if (arg.childForFieldName("name")?.text !== "prefix") continue;
6170
+ const val = arg.childForFieldName("value");
6171
+ const p = val ? pyStaticStringText(val) : null;
6172
+ if (p !== null) prefixes.set(left.text, p);
6173
+ }
6174
+ });
6175
+ return prefixes;
6176
+ }
6177
+ function fastapiRoutesFromSource(source, parser) {
6178
+ const tree = parseSource2(parser, source);
6179
+ const prefixes = collectApiRouterPrefixes(tree.rootNode);
6180
+ const out = [];
6181
+ walk(tree.rootNode, (node) => {
6182
+ if (node.type !== "decorator") return;
6183
+ const call = node.namedChild(0);
6184
+ if (!call || call.type !== "call") return;
6185
+ const fn = call.childForFieldName("function");
6186
+ if (!fn || fn.type !== "attribute") return;
6187
+ const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
6188
+ if (!method) return;
6189
+ const isVerb = FASTAPI_METHODS.has(method);
6190
+ if (!isVerb && method !== "api_route") return;
6191
+ const args = call.childForFieldName("arguments");
6192
+ const first = args?.namedChild(0);
6193
+ if (!first || first.type !== "string") return;
6194
+ const rawPath = pyStaticStringText(first);
6195
+ if (rawPath === null || !rawPath.startsWith("/")) return;
6196
+ const obj = fn.childForFieldName("object")?.text;
6197
+ const prefix = obj ? prefixes.get(obj) ?? "" : "";
6198
+ const pathTemplate = canonicalizeTemplate(prefix + rawPath);
6199
+ const line = node.startPosition.row + 1;
6200
+ if (isVerb) {
6201
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
6202
+ return;
6203
+ }
6204
+ const methods = keywordArrayStrings(args, "methods");
6205
+ const list = methods.length > 0 ? methods : ["ALL"];
6206
+ for (const m of list) {
6207
+ out.push({
6208
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
6209
+ pathTemplate,
6210
+ line,
6211
+ framework: "fastapi"
6212
+ });
6213
+ }
6214
+ });
6215
+ return out;
6216
+ }
6112
6217
  async function addRoutes(graph, services) {
6113
6218
  const jsParser = makeJsParser2();
6219
+ const pyParser = makePyParser2();
6114
6220
  let nodesAdded = 0;
6115
6221
  let edgesAdded = 0;
6116
6222
  for (const service of services) {
@@ -6122,15 +6228,20 @@ async function addRoutes(graph, services) {
6122
6228
  const hasFastify = deps["fastify"] !== void 0;
6123
6229
  const hasHono = deps["hono"] !== void 0;
6124
6230
  const hasNext = deps["next"] !== void 0;
6125
- if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
6231
+ const hasFastapi = deps["fastapi"] !== void 0;
6232
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
6126
6233
  const files = await loadSourceFiles(service.dir);
6127
6234
  for (const file of files) {
6128
6235
  if (isTestPath(file.path)) continue;
6129
- if (!JS_ROUTE_EXTENSIONS.has(import_node_path24.default.extname(file.path))) continue;
6236
+ const ext = import_node_path24.default.extname(file.path);
6237
+ const isPy = ext === ".py";
6238
+ if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy) continue;
6130
6239
  const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
6131
6240
  let routes;
6132
6241
  try {
6133
- if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
6242
+ if (isPy) {
6243
+ routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
6244
+ } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
6134
6245
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
6135
6246
  } else if (hasExpress || hasFastify || hasHono) {
6136
6247
  routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
@@ -6314,7 +6425,7 @@ init_cjs_shims();
6314
6425
  var import_node_path26 = __toESM(require("path"), 1);
6315
6426
  var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
6316
6427
  var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
6317
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
6428
+ var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
6318
6429
  var import_types14 = require("@neat.is/types");
6319
6430
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
6320
6431
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
@@ -6369,14 +6480,14 @@ function makeJsParser3() {
6369
6480
  p.setLanguage(import_tree_sitter_javascript3.default);
6370
6481
  return p;
6371
6482
  }
6372
- function makePyParser2() {
6483
+ function makePyParser3() {
6373
6484
  const p = new import_tree_sitter3.default();
6374
- p.setLanguage(import_tree_sitter_python2.default);
6485
+ p.setLanguage(import_tree_sitter_python3.default);
6375
6486
  return p;
6376
6487
  }
6377
6488
  async function addHttpCallEdges(graph, services) {
6378
6489
  const jsParser = makeJsParser3();
6379
- const pyParser = makePyParser2();
6490
+ const pyParser = makePyParser3();
6380
6491
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
6381
6492
  let nodesAdded = 0;
6382
6493
  let edgesAdded = 0;
@@ -9426,6 +9537,42 @@ function registerRoutes(scope, ctx) {
9426
9537
  return reply.code(500).send({ error: err.message });
9427
9538
  }
9428
9539
  });
9540
+ scope.get("/instrumentation", async (req, reply) => {
9541
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9542
+ if (!proj) return;
9543
+ if (!proj.scanPath) {
9544
+ return { engaged: null };
9545
+ }
9546
+ try {
9547
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
9548
+ const uninstrumented = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
9549
+ if (state.hookFiles.length === 0) {
9550
+ return {
9551
+ engaged: false,
9552
+ diagnosis: {
9553
+ reason: "No instrumented entry point found \u2014 NEAT hasn't written an OTel init hook for this project.",
9554
+ fixCommand: "neat init",
9555
+ detail: "Run `neat init` so NEAT writes the instrumentation hook next to your entry point, then run your app."
9556
+ }
9557
+ };
9558
+ }
9559
+ if (uninstrumented.length > 0) {
9560
+ const names = uninstrumented.map((u) => u.library);
9561
+ const more = names.length > 1 ? ` (and ${names.length - 1} more)` : "";
9562
+ return {
9563
+ engaged: false,
9564
+ diagnosis: {
9565
+ reason: `\`${names[0]}\`${more} isn't in the auto-instrumentation set, so spans from it won't reach the graph.`,
9566
+ fixCommand: "neat extend",
9567
+ detail: `Uninstrumented on your hot path: ${names.join(", ")}. Run \`neat extend\` to wire the missing instrumentation.`
9568
+ }
9569
+ };
9570
+ }
9571
+ return { engaged: true };
9572
+ } catch {
9573
+ return { engaged: null };
9574
+ }
9575
+ });
9429
9576
  scope.post("/extend/apply", async (req, reply) => {
9430
9577
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9431
9578
  if (!proj) return;