@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/index.cjs CHANGED
@@ -341,6 +341,11 @@ function pickEnv(spanAttrs, resourceAttrs) {
341
341
  }
342
342
  return ENV_FALLBACK;
343
343
  }
344
+ function normalizeDbSystem(attrs) {
345
+ const raw = attrs["db.system"];
346
+ if (typeof raw !== "string") return void 0;
347
+ return raw === "mongoose" ? "mongodb" : raw;
348
+ }
344
349
  function messagingDestinationOf(attrs) {
345
350
  for (const key of ["messaging.destination.name", "messaging.destination"]) {
346
351
  const v = attrs[key];
@@ -395,7 +400,7 @@ function parseOtlpRequest(body) {
395
400
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
396
401
  env: pickEnv(attrs, resourceAttrs),
397
402
  attributes: attrs,
398
- dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
403
+ dbSystem: normalizeDbSystem(attrs),
399
404
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
400
405
  dbCollection: typeof attrs["db.collection.name"] === "string" ? attrs["db.collection.name"] : typeof attrs["db.mongodb.collection"] === "string" ? attrs["db.mongodb.collection"] : void 0,
401
406
  messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
@@ -5340,6 +5345,7 @@ init_cjs_shims();
5340
5345
  var import_node_path22 = __toESM(require("path"), 1);
5341
5346
  var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5342
5347
  var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5348
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5343
5349
  var import_types11 = require("@neat.is/types");
5344
5350
  var PARSE_CHUNK2 = 16384;
5345
5351
  function parseSource2(parser, source) {
@@ -5352,6 +5358,11 @@ function makeJsParser2() {
5352
5358
  p.setLanguage(import_tree_sitter_javascript2.default);
5353
5359
  return p;
5354
5360
  }
5361
+ function makePyParser2() {
5362
+ const p = new import_tree_sitter2.default();
5363
+ p.setLanguage(import_tree_sitter_python2.default);
5364
+ return p;
5365
+ }
5355
5366
  var ROUTER_METHODS = /* @__PURE__ */ new Set([
5356
5367
  "get",
5357
5368
  "post",
@@ -5364,6 +5375,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5364
5375
  ]);
5365
5376
  var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5366
5377
  var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5378
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
5367
5379
  function canonicalizeTemplate(raw) {
5368
5380
  let p = raw.split("?")[0].split("#")[0];
5369
5381
  if (!p.startsWith("/")) p = "/" + p;
@@ -5591,8 +5603,102 @@ function nextRoutesFromFile(source, relFile, parser) {
5591
5603
  }
5592
5604
  return [];
5593
5605
  }
5606
+ function pyStaticStringText(node) {
5607
+ if (node.type !== "string") return null;
5608
+ for (let i = 0; i < node.namedChildCount; i++) {
5609
+ const child = node.namedChild(i);
5610
+ if (child?.type === "interpolation") return null;
5611
+ if (child?.type === "string_content") return child.text;
5612
+ }
5613
+ return "";
5614
+ }
5615
+ function keywordArrayStrings(argsNode, key) {
5616
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
5617
+ const arg = argsNode.namedChild(i);
5618
+ if (arg?.type !== "keyword_argument") continue;
5619
+ if (arg.childForFieldName("name")?.text !== key) continue;
5620
+ const val = arg.childForFieldName("value");
5621
+ if (!val || val.type !== "list") return [];
5622
+ const out = [];
5623
+ for (let j = 0; j < val.namedChildCount; j++) {
5624
+ const el = val.namedChild(j);
5625
+ if (el?.type === "string") {
5626
+ const s = pyStaticStringText(el);
5627
+ if (s) out.push(s);
5628
+ }
5629
+ }
5630
+ return out;
5631
+ }
5632
+ return [];
5633
+ }
5634
+ function collectApiRouterPrefixes(root) {
5635
+ const prefixes = /* @__PURE__ */ new Map();
5636
+ walk(root, (node) => {
5637
+ if (node.type !== "assignment") return;
5638
+ const right = node.childForFieldName("right");
5639
+ if (!right || right.type !== "call") return;
5640
+ const fn = right.childForFieldName("function");
5641
+ if (!fn) return;
5642
+ const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
5643
+ if (ctor !== "APIRouter") return;
5644
+ const left = node.childForFieldName("left");
5645
+ if (!left || left.type !== "identifier") return;
5646
+ const args = right.childForFieldName("arguments");
5647
+ if (!args) return;
5648
+ for (let i = 0; i < args.namedChildCount; i++) {
5649
+ const arg = args.namedChild(i);
5650
+ if (arg?.type !== "keyword_argument") continue;
5651
+ if (arg.childForFieldName("name")?.text !== "prefix") continue;
5652
+ const val = arg.childForFieldName("value");
5653
+ const p = val ? pyStaticStringText(val) : null;
5654
+ if (p !== null) prefixes.set(left.text, p);
5655
+ }
5656
+ });
5657
+ return prefixes;
5658
+ }
5659
+ function fastapiRoutesFromSource(source, parser) {
5660
+ const tree = parseSource2(parser, source);
5661
+ const prefixes = collectApiRouterPrefixes(tree.rootNode);
5662
+ const out = [];
5663
+ walk(tree.rootNode, (node) => {
5664
+ if (node.type !== "decorator") return;
5665
+ const call = node.namedChild(0);
5666
+ if (!call || call.type !== "call") return;
5667
+ const fn = call.childForFieldName("function");
5668
+ if (!fn || fn.type !== "attribute") return;
5669
+ const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5670
+ if (!method) return;
5671
+ const isVerb = FASTAPI_METHODS.has(method);
5672
+ if (!isVerb && method !== "api_route") return;
5673
+ const args = call.childForFieldName("arguments");
5674
+ const first = args?.namedChild(0);
5675
+ if (!first || first.type !== "string") return;
5676
+ const rawPath = pyStaticStringText(first);
5677
+ if (rawPath === null || !rawPath.startsWith("/")) return;
5678
+ const obj = fn.childForFieldName("object")?.text;
5679
+ const prefix = obj ? prefixes.get(obj) ?? "" : "";
5680
+ const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5681
+ const line = node.startPosition.row + 1;
5682
+ if (isVerb) {
5683
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5684
+ return;
5685
+ }
5686
+ const methods = keywordArrayStrings(args, "methods");
5687
+ const list = methods.length > 0 ? methods : ["ALL"];
5688
+ for (const m of list) {
5689
+ out.push({
5690
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5691
+ pathTemplate,
5692
+ line,
5693
+ framework: "fastapi"
5694
+ });
5695
+ }
5696
+ });
5697
+ return out;
5698
+ }
5594
5699
  async function addRoutes(graph, services) {
5595
5700
  const jsParser = makeJsParser2();
5701
+ const pyParser = makePyParser2();
5596
5702
  let nodesAdded = 0;
5597
5703
  let edgesAdded = 0;
5598
5704
  for (const service of services) {
@@ -5604,15 +5710,20 @@ async function addRoutes(graph, services) {
5604
5710
  const hasFastify = deps["fastify"] !== void 0;
5605
5711
  const hasHono = deps["hono"] !== void 0;
5606
5712
  const hasNext = deps["next"] !== void 0;
5607
- if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
5713
+ const hasFastapi = deps["fastapi"] !== void 0;
5714
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
5608
5715
  const files = await loadSourceFiles(service.dir);
5609
5716
  for (const file of files) {
5610
5717
  if (isTestPath(file.path)) continue;
5611
- if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
5718
+ const ext = import_node_path22.default.extname(file.path);
5719
+ const isPy = ext === ".py";
5720
+ if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy) continue;
5612
5721
  const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5613
5722
  let routes;
5614
5723
  try {
5615
- if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5724
+ if (isPy) {
5725
+ routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5726
+ } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5616
5727
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5617
5728
  } else if (hasExpress || hasFastify || hasHono) {
5618
5729
  routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
@@ -5796,7 +5907,7 @@ init_cjs_shims();
5796
5907
  var import_node_path24 = __toESM(require("path"), 1);
5797
5908
  var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5798
5909
  var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5799
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5910
+ var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
5800
5911
  var import_types13 = require("@neat.is/types");
5801
5912
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
5802
5913
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
@@ -5851,14 +5962,14 @@ function makeJsParser3() {
5851
5962
  p.setLanguage(import_tree_sitter_javascript3.default);
5852
5963
  return p;
5853
5964
  }
5854
- function makePyParser2() {
5965
+ function makePyParser3() {
5855
5966
  const p = new import_tree_sitter3.default();
5856
- p.setLanguage(import_tree_sitter_python2.default);
5967
+ p.setLanguage(import_tree_sitter_python3.default);
5857
5968
  return p;
5858
5969
  }
5859
5970
  async function addHttpCallEdges(graph, services) {
5860
5971
  const jsParser = makeJsParser3();
5861
- const pyParser = makePyParser2();
5972
+ const pyParser = makePyParser3();
5862
5973
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5863
5974
  let nodesAdded = 0;
5864
5975
  let edgesAdded = 0;
@@ -9872,6 +9983,42 @@ function registerRoutes(scope, ctx) {
9872
9983
  return reply.code(500).send({ error: err.message });
9873
9984
  }
9874
9985
  });
9986
+ scope.get("/instrumentation", async (req, reply) => {
9987
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9988
+ if (!proj) return;
9989
+ if (!proj.scanPath) {
9990
+ return { engaged: null };
9991
+ }
9992
+ try {
9993
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
9994
+ const uninstrumented = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
9995
+ if (state.hookFiles.length === 0) {
9996
+ return {
9997
+ engaged: false,
9998
+ diagnosis: {
9999
+ reason: "No instrumented entry point found \u2014 NEAT hasn't written an OTel init hook for this project.",
10000
+ fixCommand: "neat init",
10001
+ detail: "Run `neat init` so NEAT writes the instrumentation hook next to your entry point, then run your app."
10002
+ }
10003
+ };
10004
+ }
10005
+ if (uninstrumented.length > 0) {
10006
+ const names = uninstrumented.map((u) => u.library);
10007
+ const more = names.length > 1 ? ` (and ${names.length - 1} more)` : "";
10008
+ return {
10009
+ engaged: false,
10010
+ diagnosis: {
10011
+ reason: `\`${names[0]}\`${more} isn't in the auto-instrumentation set, so spans from it won't reach the graph.`,
10012
+ fixCommand: "neat extend",
10013
+ detail: `Uninstrumented on your hot path: ${names.join(", ")}. Run \`neat extend\` to wire the missing instrumentation.`
10014
+ }
10015
+ };
10016
+ }
10017
+ return { engaged: true };
10018
+ } catch {
10019
+ return { engaged: null };
10020
+ }
10021
+ });
9875
10022
  scope.post("/extend/apply", async (req, reply) => {
9876
10023
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9877
10024
  if (!proj) return;
@@ -11742,15 +11889,21 @@ var PROVIDER_DISPATCH = {
11742
11889
  resolveTarget: createCloudflareResolveTarget(config, graph)
11743
11890
  };
11744
11891
  },
11745
- // GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
11746
- // token live" endpoint. 200 on a valid token, 401 on an invalid one.
11892
+ // GET /accounts/{accountId}/tokens/verify — the *account-scoped* token-verify
11893
+ // endpoint. A Workers connector token is scoped to the account, and the
11894
+ // user-level `GET /user/tokens/verify` returns 401 "Invalid API Token" for
11895
+ // such a token even though it authenticates fine against the account's own
11896
+ // resources (confirmed live). Probing the account-scoped verify endpoint —
11897
+ // `accountId` is already required for this provider — returns 200
11898
+ // `{status:"active"}` for a working token and 401 for a bad one, so a valid
11899
+ // Workers token is no longer falsely rejected at `neat connector add`.
11747
11900
  validate({ credentials, options, fetchImpl }) {
11748
11901
  const cfg = options;
11749
11902
  const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
11750
11903
  return authProbe({
11751
11904
  provider: "cloudflare",
11752
11905
  accountKey: cfg.accountId ?? "validate",
11753
- url: `${baseUrl}/user/tokens/verify`,
11906
+ url: `${baseUrl}/accounts/${cfg.accountId ?? ""}/tokens/verify`,
11754
11907
  token: String(credentials.apiToken ?? ""),
11755
11908
  ...fetchImpl ? { fetchImpl } : {}
11756
11909
  });