@neat.is/core 0.4.20-dev.20260620 → 0.4.20-dev.20260622

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/cli.cjs CHANGED
@@ -1958,13 +1958,14 @@ var parentSpanCache = /* @__PURE__ */ new Map();
1958
1958
  function parentSpanKey(traceId, spanId) {
1959
1959
  return `${traceId}:${spanId}`;
1960
1960
  }
1961
- function cacheSpanService(span, now) {
1961
+ function cacheSpanService(span, now, callSite) {
1962
1962
  if (!span.traceId || !span.spanId) return;
1963
1963
  const key = parentSpanKey(span.traceId, span.spanId);
1964
1964
  parentSpanCache.delete(key);
1965
1965
  parentSpanCache.set(key, {
1966
1966
  service: span.service,
1967
1967
  env: span.env ?? "unknown",
1968
+ ...callSite ? { callSite } : {},
1968
1969
  expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1969
1970
  });
1970
1971
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
@@ -1980,7 +1981,11 @@ function lookupParentSpan(traceId, parentSpanId, now) {
1980
1981
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1981
1982
  return null;
1982
1983
  }
1983
- return { service: entry2.service, env: entry2.env };
1984
+ return {
1985
+ service: entry2.service,
1986
+ env: entry2.env,
1987
+ ...entry2.callSite ? { callSite: entry2.callSite } : {}
1988
+ };
1984
1989
  }
1985
1990
  function resolveServiceId(graph, host, env) {
1986
1991
  const envTagged = (0, import_types3.serviceId)(host, env);
@@ -2253,9 +2258,9 @@ async function handleSpan(ctx, span) {
2253
2258
  }
2254
2259
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
2255
2260
  const isError = span.statusCode === 2;
2256
- cacheSpanService(span, nowMs);
2257
2261
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2258
2262
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2263
+ cacheSpanService(span, nowMs, callSite);
2259
2264
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2260
2265
  const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2261
2266
  let affectedNode = sourceId;
@@ -2312,13 +2317,19 @@ async function handleSpan(ctx, span) {
2312
2317
  const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
2313
2318
  if (parent && parent.service !== span.service) {
2314
2319
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2320
+ const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2321
+ const fallbackEvidence = parent.callSite ? {
2322
+ file: parent.callSite.relPath,
2323
+ ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2324
+ } : void 0;
2315
2325
  upsertObservedEdge(
2316
2326
  ctx.graph,
2317
2327
  import_types3.EdgeType.CALLS,
2318
- parentId,
2328
+ fallbackSource,
2319
2329
  sourceId,
2320
2330
  ts,
2321
- isError
2331
+ isError,
2332
+ fallbackEvidence
2322
2333
  );
2323
2334
  }
2324
2335
  }
@@ -2920,6 +2931,9 @@ function workspaceGlobs(pkg) {
2920
2931
  if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
2921
2932
  return null;
2922
2933
  }
2934
+ async function hasPythonManifest(dir) {
2935
+ return await exists(import_node_path9.default.join(dir, "pyproject.toml")) || await exists(import_node_path9.default.join(dir, "requirements.txt")) || await exists(import_node_path9.default.join(dir, "setup.py"));
2936
+ }
2923
2937
  async function loadGitignore(scanPath) {
2924
2938
  const gitignorePath = import_node_path9.default.join(scanPath, ".gitignore");
2925
2939
  if (!await exists(gitignorePath)) return null;
@@ -2986,6 +3000,13 @@ function detectJsFramework(pkg) {
2986
3000
  if (deps["astro"] !== void 0) return "astro";
2987
3001
  return void 0;
2988
3002
  }
3003
+ async function detectJsServiceLanguage(dir, pkg) {
3004
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3005
+ if (deps["typescript"] !== void 0) return "typescript";
3006
+ const entries = await import_node_fs9.promises.readdir(dir).catch(() => []);
3007
+ if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
3008
+ return "javascript";
3009
+ }
2989
3010
  async function discoverNodeService(scanPath, dir) {
2990
3011
  const pkgPath = import_node_path9.default.join(dir, "package.json");
2991
3012
  if (!await exists(pkgPath)) return null;
@@ -2998,11 +3019,12 @@ async function discoverNodeService(scanPath, dir) {
2998
3019
  }
2999
3020
  if (!pkg.name) return null;
3000
3021
  const framework = detectJsFramework(pkg);
3022
+ const language = await detectJsServiceLanguage(dir, pkg);
3001
3023
  const node = {
3002
3024
  id: (0, import_types5.serviceId)(pkg.name),
3003
3025
  type: import_types5.NodeType.ServiceNode,
3004
3026
  name: pkg.name,
3005
- language: "javascript",
3027
+ language,
3006
3028
  version: pkg.version,
3007
3029
  dependencies: pkg.dependencies ?? {},
3008
3030
  repoPath: import_node_path9.default.relative(scanPath, dir),
@@ -3045,7 +3067,11 @@ async function discoverServices(scanPath) {
3045
3067
  if (wsGlobs) {
3046
3068
  candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
3047
3069
  } else {
3048
- if (rootPkg && rootPkg.name) candidateDirs.push(scanPath);
3070
+ if (rootPkg && rootPkg.name) {
3071
+ candidateDirs.push(scanPath);
3072
+ } else if (await hasPythonManifest(scanPath)) {
3073
+ candidateDirs.push(scanPath);
3074
+ }
3049
3075
  const ig = await loadGitignore(scanPath);
3050
3076
  await walkDirs(
3051
3077
  scanPath,
@@ -3054,7 +3080,7 @@ async function discoverServices(scanPath) {
3054
3080
  async (dir) => {
3055
3081
  if (await exists(import_node_path9.default.join(dir, "package.json"))) {
3056
3082
  candidateDirs.push(dir);
3057
- } else if (await exists(import_node_path9.default.join(dir, "pyproject.toml")) || await exists(import_node_path9.default.join(dir, "requirements.txt")) || await exists(import_node_path9.default.join(dir, "setup.py"))) {
3083
+ } else if (await hasPythonManifest(dir)) {
3058
3084
  candidateDirs.push(dir);
3059
3085
  }
3060
3086
  }
@@ -8686,6 +8712,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
8686
8712
  `;
8687
8713
 
8688
8714
  // src/installers/javascript.ts
8715
+ var import_instrumentation_registry2 = require("@neat.is/instrumentation-registry");
8689
8716
  var SDK_PACKAGES = [
8690
8717
  { name: "@opentelemetry/api", version: "^1.9.0" },
8691
8718
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
@@ -8810,6 +8837,34 @@ function hasNextDependency(pkg) {
8810
8837
  function allDeps(pkg) {
8811
8838
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
8812
8839
  }
8840
+ var WEB_FRAMEWORK_DEPS = [
8841
+ "express",
8842
+ "fastify",
8843
+ "koa",
8844
+ "@hapi/hapi",
8845
+ "hapi",
8846
+ "restify",
8847
+ "connect",
8848
+ "@nestjs/core",
8849
+ "next",
8850
+ "hono",
8851
+ "elysia",
8852
+ "polka",
8853
+ "micro"
8854
+ ];
8855
+ function looksLikeWebApp(pkg) {
8856
+ const deps = allDeps(pkg);
8857
+ return WEB_FRAMEWORK_DEPS.some((name) => name in deps);
8858
+ }
8859
+ function uninstrumentedLibraries(pkg) {
8860
+ const deps = allDeps(pkg);
8861
+ const out = [];
8862
+ for (const [name, version] of Object.entries(deps)) {
8863
+ const entry2 = (0, import_instrumentation_registry2.resolve)(name, version);
8864
+ if (entry2 && entry2.coverage === "gap") out.push(name);
8865
+ }
8866
+ return out;
8867
+ }
8813
8868
  var REMIX_ENTRY_CANDIDATES = [
8814
8869
  "app/entry.server.ts",
8815
8870
  "app/entry.server.tsx",
@@ -8889,6 +8944,9 @@ var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
8889
8944
  var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
8890
8945
  (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
8891
8946
  );
8947
+ var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
8948
+ (name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
8949
+ );
8892
8950
  var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
8893
8951
  "node",
8894
8952
  "ts-node",
@@ -8963,6 +9021,10 @@ async function resolveEntry(serviceDir, pkg) {
8963
9021
  const candidate = import_node_path46.default.join(serviceDir, rel);
8964
9022
  if (await exists3(candidate)) return candidate;
8965
9023
  }
9024
+ for (const rel of ROOT_NAMED_CANDIDATES) {
9025
+ const candidate = import_node_path46.default.join(serviceDir, rel);
9026
+ if (await exists3(candidate)) return candidate;
9027
+ }
8966
9028
  for (const name of INDEX_CANDIDATES) {
8967
9029
  const candidate = import_node_path46.default.join(serviceDir, name);
8968
9030
  if (await exists3(candidate)) return candidate;
@@ -10172,8 +10234,17 @@ async function applyInstallersOver(services, project, options = {}) {
10172
10234
  if (!installPlans.has(key)) installPlans.set(key, cmd);
10173
10235
  }
10174
10236
  } else if (outcome.outcome === "already-instrumented") already++;
10175
- else if (outcome.outcome === "lib-only") libOnly++;
10176
- else if (outcome.outcome === "browser-bundle") {
10237
+ else if (outcome.outcome === "lib-only") {
10238
+ libOnly++;
10239
+ if (svc.pkg && looksLikeWebApp(svc.pkg)) {
10240
+ const svcName = import_node_path50.default.basename(svc.dir);
10241
+ console.warn(
10242
+ `neat: runtime layer won't engage for ${svcName}: no entry point found.
10243
+ ${svc.dir} depends on a web framework but neat couldn't resolve an entry to instrument.
10244
+ Add a "start" script to package.json, or point neat at the entry file directly.`
10245
+ );
10246
+ }
10247
+ } else if (outcome.outcome === "browser-bundle") {
10177
10248
  browserBundle++;
10178
10249
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
10179
10250
  } else if (outcome.outcome === "react-native") {
@@ -10232,6 +10303,19 @@ async function applyInstallersOver(services, project, options = {}) {
10232
10303
  See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
10233
10304
  );
10234
10305
  }
10306
+ if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
10307
+ const gaps = uninstrumentedLibraries(svc.pkg);
10308
+ if (gaps.length > 0) {
10309
+ const svcName = import_node_path50.default.basename(svc.dir);
10310
+ const list = gaps.join(", ");
10311
+ const verb = gaps.length === 1 ? "this library" : "these libraries";
10312
+ console.warn(
10313
+ `neat: calls to ${list} won't be observed by default in ${svcName}.
10314
+ ${verb} aren't in the default instrumentation set, so they produce no OBSERVED edges.
10315
+ Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
10316
+ );
10317
+ }
10318
+ }
10235
10319
  }
10236
10320
  const packageManagerInstalls = [];
10237
10321
  for (const cmd of installPlans.values()) {
@@ -11256,7 +11340,10 @@ async function checkDaemonHealth(baseUrl) {
11256
11340
  }
11257
11341
  function snapshotForGraph(persisted) {
11258
11342
  return {
11259
- schemaVersion: 3,
11343
+ // Stamp the live schema version the daemon validates against on the
11344
+ // receiving `/snapshot` merge. Tracking the constant keeps the push
11345
+ // aligned with the current snapshot shape across schema migrations.
11346
+ schemaVersion: SCHEMA_VERSION,
11260
11347
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
11261
11348
  graph: persisted.graph.export()
11262
11349
  };