@neat.is/core 0.4.2 → 0.4.4

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
@@ -401,6 +401,64 @@ async function buildOtelReceiver(opts) {
401
401
  for (const s of spans) queue.push(s);
402
402
  drainPromise = drainPromise.then(() => drain());
403
403
  };
404
+ const projectQueue = [];
405
+ let projectDraining = false;
406
+ let projectDrainPromise = Promise.resolve();
407
+ const drainProject = async () => {
408
+ if (projectDraining) return;
409
+ projectDraining = true;
410
+ try {
411
+ while (projectQueue.length > 0) {
412
+ const { project, span } = projectQueue.shift();
413
+ try {
414
+ if (opts.onProjectSpan) {
415
+ await opts.onProjectSpan(project, span);
416
+ } else {
417
+ await opts.onSpan(span);
418
+ }
419
+ } catch (err) {
420
+ console.warn(`[neat] otel handler error: ${err.message}`);
421
+ }
422
+ }
423
+ } finally {
424
+ projectDraining = false;
425
+ }
426
+ };
427
+ const enqueueProject = (project, spans) => {
428
+ if (spans.length === 0) return;
429
+ for (const s of spans) projectQueue.push({ project, span: s });
430
+ projectDrainPromise = projectDrainPromise.then(() => drainProject());
431
+ };
432
+ const legacyEndpointWarned = /* @__PURE__ */ new Set();
433
+ function warnLegacyEndpoint(serviceName) {
434
+ if (legacyEndpointWarned.has(serviceName)) return;
435
+ legacyEndpointWarned.add(serviceName);
436
+ console.warn(
437
+ `[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name="${serviceName}").`
438
+ );
439
+ }
440
+ async function readOtlpBody(req) {
441
+ const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
442
+ if (ct === "application/x-protobuf") {
443
+ try {
444
+ const body = await decodeProtobufBody(req.body);
445
+ return { ok: true, body, flavor: "protobuf" };
446
+ } catch (err) {
447
+ return { ok: false, code: 400, error: `protobuf decode failed: ${err.message}` };
448
+ }
449
+ }
450
+ if (!ct || ct === "application/json") {
451
+ return { ok: true, body: req.body ?? {}, flavor: "json" };
452
+ }
453
+ return { ok: false, code: 415, error: `unsupported content-type: ${ct}` };
454
+ }
455
+ function sendOtlpSuccess(reply, flavor) {
456
+ if (flavor === "protobuf") {
457
+ const buf = encodeProtobufResponseBody();
458
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
459
+ }
460
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
461
+ }
404
462
  app.addContentTypeParser(
405
463
  "application/x-protobuf",
406
464
  { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
@@ -410,26 +468,44 @@ async function buildOtelReceiver(opts) {
410
468
  );
411
469
  app.get("/health", async () => ({ ok: true }));
412
470
  app.post("/v1/traces", async (req, reply) => {
413
- const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
414
- let body;
415
- let responseFlavor;
416
- if (ct === "application/x-protobuf") {
417
- responseFlavor = "protobuf";
471
+ const result = await readOtlpBody(req);
472
+ if (!result.ok) {
473
+ return reply.code(result.code).send({ error: result.error });
474
+ }
475
+ const spans = parseOtlpRequest(result.body);
476
+ for (const s of spans) warnLegacyEndpoint(s.service);
477
+ if (opts.onErrorSpanSync) {
418
478
  try {
419
- body = await decodeProtobufBody(req.body);
479
+ for (const span of spans) {
480
+ if (span.statusCode === 2) await opts.onErrorSpanSync(span);
481
+ }
420
482
  } catch (err) {
421
- return reply.code(400).send({
422
- error: `protobuf decode failed: ${err.message}`
483
+ return reply.code(500).send({
484
+ error: `error-event write failed: ${err.message}`
423
485
  });
424
486
  }
425
- } else if (!ct || ct === "application/json") {
426
- responseFlavor = "json";
427
- body = req.body ?? {};
428
- } else {
429
- return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
430
487
  }
431
- const spans = parseOtlpRequest(body);
432
- if (opts.onErrorSpanSync) {
488
+ enqueue(spans);
489
+ return sendOtlpSuccess(reply, result.flavor);
490
+ });
491
+ app.post("/projects/:project/v1/traces", async (req, reply) => {
492
+ const project = req.params.project;
493
+ const result = await readOtlpBody(req);
494
+ if (!result.ok) {
495
+ return reply.code(result.code).send({ error: result.error });
496
+ }
497
+ const spans = parseOtlpRequest(result.body);
498
+ if (opts.onProjectErrorSpanSync) {
499
+ try {
500
+ for (const span of spans) {
501
+ if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span);
502
+ }
503
+ } catch (err) {
504
+ return reply.code(500).send({
505
+ error: `error-event write failed: ${err.message}`
506
+ });
507
+ }
508
+ } else if (opts.onErrorSpanSync) {
433
509
  try {
434
510
  for (const span of spans) {
435
511
  if (span.statusCode === 2) await opts.onErrorSpanSync(span);
@@ -440,17 +516,13 @@ async function buildOtelReceiver(opts) {
440
516
  });
441
517
  }
442
518
  }
443
- enqueue(spans);
444
- if (responseFlavor === "protobuf") {
445
- const buf = encodeProtobufResponseBody();
446
- return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
447
- }
448
- return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
519
+ enqueueProject(project, spans);
520
+ return sendOtlpSuccess(reply, result.flavor);
449
521
  });
450
522
  const decorated = app;
451
523
  decorated.flushPending = async () => {
452
- while (queue.length > 0 || draining) {
453
- await drainPromise;
524
+ while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {
525
+ await Promise.all([drainPromise, projectDrainPromise]);
454
526
  }
455
527
  };
456
528
  return decorated;
@@ -6484,45 +6556,31 @@ var import_node_path40 = __toESM(require("path"), 1);
6484
6556
  init_cjs_shims();
6485
6557
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
6486
6558
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
6487
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6488
- // the auto-instrumentation hook attaches. Configure via the env file.
6489
- const path = require('node:path')
6490
- try {
6491
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6492
- } catch (err) {
6493
- // dotenv unavailable \u2014 fall through to process.env.
6494
- }
6559
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6560
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6561
+
6495
6562
  require('@opentelemetry/auto-instrumentations-node/register')
6496
6563
  `;
6497
6564
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
6498
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6499
- // the auto-instrumentation hook attaches. Configure via the env file.
6500
- import { fileURLToPath } from 'node:url'
6501
- import path from 'node:path'
6502
- import dotenv from 'dotenv'
6503
-
6504
- const here = path.dirname(fileURLToPath(import.meta.url))
6505
- dotenv.config({ path: path.join(here, '.env.neat') })
6565
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6566
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6506
6567
 
6507
6568
  await import('@opentelemetry/auto-instrumentations-node/register')
6508
6569
  `;
6509
6570
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
6510
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6511
- // the auto-instrumentation hook attaches. Configure via the env file.
6512
- import { fileURLToPath } from 'node:url'
6513
- import path from 'node:path'
6514
- import dotenv from 'dotenv'
6515
-
6516
- const here = path.dirname(fileURLToPath(import.meta.url))
6517
- dotenv.config({ path: path.join(here, '.env.neat') })
6571
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6572
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6518
6573
 
6519
6574
  await import('@opentelemetry/auto-instrumentations-node/register')
6520
6575
  `;
6521
- function renderEnvNeat(projectName) {
6576
+ function renderNodeOtelInit(template, serviceName, projectName) {
6577
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6578
+ }
6579
+ function renderEnvNeat(serviceName, projectName) {
6522
6580
  return [
6523
6581
  "# Generated by `neat init --apply` (ADR-069).",
6524
- `OTEL_SERVICE_NAME=${projectName}`,
6525
- "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
6582
+ `OTEL_SERVICE_NAME=${serviceName}`,
6583
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
6526
6584
  ""
6527
6585
  ].join("\n");
6528
6586
  }
@@ -6542,54 +6600,34 @@ export async function register() {
6542
6600
  }
6543
6601
  `;
6544
6602
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6545
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6546
- // the OTel SDK starts so the exporter target and service name are in scope.
6547
- import { fileURLToPath } from 'node:url'
6548
- import path from 'node:path'
6549
- import dotenv from 'dotenv'
6603
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6604
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6605
+
6550
6606
  import { NodeSDK } from '@opentelemetry/sdk-node'
6551
6607
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6552
6608
 
6553
- const here = path.dirname(fileURLToPath(import.meta.url))
6554
- dotenv.config({ path: path.join(here, '.env.neat') })
6555
-
6556
- const sdk = new NodeSDK({
6557
- serviceName: process.env.OTEL_SERVICE_NAME,
6558
- instrumentations: [getNodeAutoInstrumentations()],
6559
- })
6560
- sdk.start()
6609
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6561
6610
  `;
6562
6611
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6563
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6564
- // the OTel SDK starts so the exporter target and service name are in scope.
6565
- import { fileURLToPath } from 'node:url'
6566
- import path from 'node:path'
6567
- import dotenv from 'dotenv'
6568
- import { NodeSDK } from '@opentelemetry/sdk-node'
6569
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6612
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6613
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6570
6614
 
6571
- const here = path.dirname(fileURLToPath(import.meta.url))
6572
- dotenv.config({ path: path.join(here, '.env.neat') })
6615
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6616
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6573
6617
 
6574
- const sdk = new NodeSDK({
6575
- serviceName: process.env.OTEL_SERVICE_NAME,
6576
- instrumentations: [getNodeAutoInstrumentations()],
6577
- })
6578
- sdk.start()
6618
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6579
6619
  `;
6620
+ function renderNextInstrumentationNode(template, serviceName, projectName) {
6621
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6622
+ }
6580
6623
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6581
6624
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6582
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6583
- // the OTel SDK starts so the exporter target and service name are in scope.
6584
- import { fileURLToPath } from 'node:url'
6585
- import path from 'node:path'
6586
- import dotenv from 'dotenv'
6625
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6626
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6627
+
6587
6628
  import { NodeSDK } from '@opentelemetry/sdk-node'
6588
6629
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6589
6630
 
6590
- const here = path.dirname(fileURLToPath(import.meta.url))
6591
- dotenv.config({ path: path.join(here, '.env.neat') })
6592
-
6593
6631
  const sdk = new NodeSDK({
6594
6632
  serviceName: process.env.OTEL_SERVICE_NAME,
6595
6633
  instrumentations: [getNodeAutoInstrumentations()],
@@ -6597,14 +6635,9 @@ const sdk = new NodeSDK({
6597
6635
  sdk.start()
6598
6636
  `;
6599
6637
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6600
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6601
- // the OTel SDK starts so the exporter target and service name are in scope.
6602
- const path = require('node:path')
6603
- try {
6604
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6605
- } catch (err) {
6606
- // dotenv unavailable \u2014 fall through to process.env.
6607
- }
6638
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6639
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6640
+
6608
6641
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6609
6642
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6610
6643
 
@@ -6614,6 +6647,9 @@ const sdk = new NodeSDK({
6614
6647
  })
6615
6648
  sdk.start()
6616
6649
  `;
6650
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
6651
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6652
+ }
6617
6653
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6618
6654
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6619
6655
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -6676,11 +6712,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
6676
6712
  var SDK_PACKAGES = [
6677
6713
  { name: "@opentelemetry/api", version: "^1.9.0" },
6678
6714
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
6679
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
6680
- // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
6681
- // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
6682
- // before the auto-instrumentation hook attaches.
6683
- { name: "dotenv", version: "^16.4.5" }
6715
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
6684
6716
  ];
6685
6717
  var OTEL_ENV = {
6686
6718
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
@@ -6688,13 +6720,36 @@ var OTEL_ENV = {
6688
6720
  // patch render: it documents the key/value the user can inspect in the
6689
6721
  // generated .env.neat.
6690
6722
  file: null,
6691
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6692
- value: "http://localhost:4318"
6723
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
6724
+ value: "http://localhost:4318/projects/<project>/v1/traces"
6693
6725
  };
6694
- function envServiceName(pkg, serviceDir, project) {
6726
+ function serviceNodeName(pkg, serviceDir) {
6727
+ return pkg.name ?? import_node_path40.default.basename(serviceDir);
6728
+ }
6729
+ function projectToken(pkg, serviceDir, project) {
6695
6730
  if (project && project.length > 0) return project;
6696
6731
  return pkg.name ?? import_node_path40.default.basename(serviceDir);
6697
6732
  }
6733
+ async function readJsonFile(p) {
6734
+ try {
6735
+ const raw = await import_node_fs25.promises.readFile(p, "utf8");
6736
+ return JSON.parse(raw);
6737
+ } catch {
6738
+ return null;
6739
+ }
6740
+ }
6741
+ async function detectRuntimeKind(pkgRoot, pkg) {
6742
+ const deps = allDeps(pkg);
6743
+ if ("react-native" in deps || "expo" in deps) return "react-native";
6744
+ const appJson = await readJsonFile(import_node_path40.default.join(pkgRoot, "app.json"));
6745
+ if (appJson && typeof appJson === "object" && "expo" in appJson) {
6746
+ return "react-native";
6747
+ }
6748
+ if (await exists2(import_node_path40.default.join(pkgRoot, "vite.config.js")) || await exists2(import_node_path40.default.join(pkgRoot, "vite.config.ts")) || await exists2(import_node_path40.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
6749
+ return "browser-bundle";
6750
+ }
6751
+ return "node";
6752
+ }
6698
6753
  async function readPackageJson(serviceDir) {
6699
6754
  try {
6700
6755
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6919,14 +6974,25 @@ function lineIsOtelInjection(line) {
6919
6974
  if (trimmed.length === 0) return false;
6920
6975
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6921
6976
  }
6977
+ async function detectsSrcLayout(serviceDir) {
6978
+ const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
6979
+ exists2(import_node_path40.default.join(serviceDir, "src", "app")),
6980
+ exists2(import_node_path40.default.join(serviceDir, "src", "pages")),
6981
+ exists2(import_node_path40.default.join(serviceDir, "app")),
6982
+ exists2(import_node_path40.default.join(serviceDir, "pages"))
6983
+ ]);
6984
+ return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
6985
+ }
6922
6986
  async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
6923
6987
  const useTs = await isTypeScriptProject(serviceDir);
6924
- const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6988
+ const srcLayout = await detectsSrcLayout(serviceDir);
6989
+ const baseDir = srcLayout ? import_node_path40.default.join(serviceDir, "src") : serviceDir;
6990
+ const instrumentationFile = import_node_path40.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6925
6991
  const instrumentationNodeFile = import_node_path40.default.join(
6926
- serviceDir,
6992
+ baseDir,
6927
6993
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
6928
6994
  );
6929
- const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6995
+ const envNeatFile = import_node_path40.default.join(baseDir, ".env.neat");
6930
6996
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6931
6997
  const dependencyEdits = [];
6932
6998
  for (const sdk of SDK_PACKAGES) {
@@ -6938,6 +7004,8 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6938
7004
  version: sdk.version
6939
7005
  });
6940
7006
  }
7007
+ const svcName = serviceNodeName(pkg, serviceDir);
7008
+ const projectName = projectToken(pkg, serviceDir, project);
6941
7009
  const generatedFiles = [];
6942
7010
  if (!await exists2(instrumentationFile)) {
6943
7011
  generatedFiles.push({
@@ -6949,14 +7017,18 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6949
7017
  if (!await exists2(instrumentationNodeFile)) {
6950
7018
  generatedFiles.push({
6951
7019
  file: instrumentationNodeFile,
6952
- contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
7020
+ contents: renderNextInstrumentationNode(
7021
+ useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
7022
+ svcName,
7023
+ projectName
7024
+ ),
6953
7025
  skipIfExists: true
6954
7026
  });
6955
7027
  }
6956
7028
  if (!await exists2(envNeatFile)) {
6957
7029
  generatedFiles.push({
6958
7030
  file: envNeatFile,
6959
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7031
+ contents: renderEnvNeat(svcName, projectName),
6960
7032
  skipIfExists: true
6961
7033
  });
6962
7034
  }
@@ -7017,11 +7089,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
7017
7089
  if (!await exists2(envNeatFile)) {
7018
7090
  generatedFiles.push({
7019
7091
  file: envNeatFile,
7020
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7092
+ contents: renderEnvNeat(
7093
+ serviceNodeName(pkg, serviceDir),
7094
+ projectToken(pkg, serviceDir, project)
7095
+ ),
7021
7096
  skipIfExists: true
7022
7097
  });
7023
7098
  }
7024
7099
  }
7100
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
7101
+ return renderFrameworkOtelInit(
7102
+ template,
7103
+ serviceNodeName(pkg, serviceDir),
7104
+ projectToken(pkg, serviceDir, project)
7105
+ );
7106
+ }
7025
7107
  function fileImportsOtelHook(raw, specifiers) {
7026
7108
  const lines = raw.split(/\r?\n/);
7027
7109
  for (const line of lines) {
@@ -7047,7 +7129,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
7047
7129
  if (!await exists2(otelServerFile)) {
7048
7130
  generatedFiles.push({
7049
7131
  file: otelServerFile,
7050
- contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
7132
+ contents: renderFrameworkOtelInitForPkg(
7133
+ useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
7134
+ pkg,
7135
+ serviceDir,
7136
+ project
7137
+ ),
7051
7138
  skipIfExists: true
7052
7139
  });
7053
7140
  }
@@ -7101,7 +7188,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
7101
7188
  if (!await exists2(otelInitFile)) {
7102
7189
  generatedFiles.push({
7103
7190
  file: otelInitFile,
7104
- contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7191
+ contents: renderFrameworkOtelInitForPkg(
7192
+ useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7193
+ pkg,
7194
+ serviceDir,
7195
+ project
7196
+ ),
7105
7197
  skipIfExists: true
7106
7198
  });
7107
7199
  }
@@ -7164,7 +7256,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7164
7256
  if (!await exists2(otelInitFile)) {
7165
7257
  generatedFiles.push({
7166
7258
  file: otelInitFile,
7167
- contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7259
+ contents: renderFrameworkOtelInitForPkg(
7260
+ useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7261
+ pkg,
7262
+ serviceDir,
7263
+ project
7264
+ ),
7168
7265
  skipIfExists: true
7169
7266
  });
7170
7267
  }
@@ -7220,7 +7317,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
7220
7317
  if (!await exists2(otelInitFile)) {
7221
7318
  generatedFiles.push({
7222
7319
  file: otelInitFile,
7223
- contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7320
+ contents: renderFrameworkOtelInitForPkg(
7321
+ useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7322
+ pkg,
7323
+ serviceDir,
7324
+ project
7325
+ ),
7224
7326
  skipIfExists: true
7225
7327
  });
7226
7328
  }
@@ -7312,6 +7414,10 @@ async function plan(serviceDir, opts) {
7312
7414
  return planAstro(serviceDir, pkg, manifestPath, project);
7313
7415
  }
7314
7416
  }
7417
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
7418
+ if (runtimeKind !== "node") {
7419
+ return { ...empty, runtimeKind };
7420
+ }
7315
7421
  const entryFile = await resolveEntry(serviceDir, pkg);
7316
7422
  if (!entryFile) {
7317
7423
  return { ...empty, libOnly: true };
@@ -7346,18 +7452,20 @@ async function plan(serviceDir, opts) {
7346
7452
  } catch {
7347
7453
  return { ...empty, libOnly: true };
7348
7454
  }
7455
+ const svcName = serviceNodeName(pkg, serviceDir);
7456
+ const projectName = projectToken(pkg, serviceDir, project);
7349
7457
  const generatedFiles = [];
7350
7458
  if (!await exists2(otelInitFile)) {
7351
7459
  generatedFiles.push({
7352
7460
  file: otelInitFile,
7353
- contents: otelInitContents(flavor),
7461
+ contents: renderNodeOtelInit(otelInitContents(flavor), svcName, projectName),
7354
7462
  skipIfExists: true
7355
7463
  });
7356
7464
  }
7357
7465
  if (!await exists2(envNeatFile)) {
7358
7466
  generatedFiles.push({
7359
7467
  file: envNeatFile,
7360
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7468
+ contents: renderEnvNeat(svcName, projectName),
7361
7469
  skipIfExists: true
7362
7470
  });
7363
7471
  }
@@ -7382,9 +7490,13 @@ function isAllowedWritePath(serviceDir, target) {
7382
7490
  if (base === "package.json") return true;
7383
7491
  if (base === ".env.neat") return true;
7384
7492
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7385
- if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7386
- if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7387
7493
  const relPosix = rel.split(import_node_path40.default.sep).join("/");
7494
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
7495
+ if (relPosix === base) return true;
7496
+ if (relPosix === `src/${base}`) return true;
7497
+ return false;
7498
+ }
7499
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7388
7500
  if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
7389
7501
  if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
7390
7502
  if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
@@ -7410,6 +7522,22 @@ async function apply(installPlan) {
7410
7522
  writtenFiles: []
7411
7523
  };
7412
7524
  }
7525
+ if (installPlan.runtimeKind === "browser-bundle") {
7526
+ return {
7527
+ serviceDir,
7528
+ outcome: "browser-bundle",
7529
+ reason: "browser bundle; Node OTel SDK cannot run here",
7530
+ writtenFiles: []
7531
+ };
7532
+ }
7533
+ if (installPlan.runtimeKind === "react-native") {
7534
+ return {
7535
+ serviceDir,
7536
+ outcome: "react-native",
7537
+ reason: "React Native / Expo target; Node OTel SDK cannot run here",
7538
+ writtenFiles: []
7539
+ };
7540
+ }
7413
7541
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
7414
7542
  return {
7415
7543
  serviceDir,
@@ -7896,11 +8024,13 @@ async function applyInstallersOver(services, project) {
7896
8024
  let instrumented = 0;
7897
8025
  let already = 0;
7898
8026
  let libOnly = 0;
8027
+ let browserBundle = 0;
8028
+ let reactNative = 0;
7899
8029
  for (const svc of services) {
7900
8030
  const installer = await pickInstaller(svc.dir);
7901
8031
  if (!installer) continue;
7902
8032
  const plan3 = await installer.plan(svc.dir, { project });
7903
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
8033
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
7904
8034
  already++;
7905
8035
  continue;
7906
8036
  }
@@ -7908,8 +8038,15 @@ async function applyInstallersOver(services, project) {
7908
8038
  if (outcome.outcome === "instrumented") instrumented++;
7909
8039
  else if (outcome.outcome === "already-instrumented") already++;
7910
8040
  else if (outcome.outcome === "lib-only") libOnly++;
8041
+ else if (outcome.outcome === "browser-bundle") {
8042
+ browserBundle++;
8043
+ console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
8044
+ } else if (outcome.outcome === "react-native") {
8045
+ reactNative++;
8046
+ console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
8047
+ }
7911
8048
  }
7912
- return { instrumented, alreadyInstrumented: already, libOnly };
8049
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
7913
8050
  }
7914
8051
  async function promptYesNo(question) {
7915
8052
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
@@ -8113,13 +8250,15 @@ async function runOrchestrator(opts) {
8113
8250
  if (gi.action !== "unchanged") {
8114
8251
  console.log(`${gi.action} .gitignore (neat-out/)`);
8115
8252
  }
8253
+ let currentProjectName = opts.project;
8116
8254
  try {
8117
- await addProject({
8255
+ const entry2 = await addProject({
8118
8256
  name: opts.project,
8119
8257
  path: opts.scanPath,
8120
8258
  languages,
8121
8259
  status: "active"
8122
8260
  });
8261
+ currentProjectName = entry2.name;
8123
8262
  } catch (err) {
8124
8263
  if (!(err instanceof ProjectNameCollisionError)) throw err;
8125
8264
  console.error(`neat: ${err.message}`);
@@ -8127,6 +8266,20 @@ async function runOrchestrator(opts) {
8127
8266
  result.exitCode = 1;
8128
8267
  return result;
8129
8268
  }
8269
+ const siblings = await listProjects();
8270
+ const paused = [];
8271
+ for (const p of siblings) {
8272
+ if (p.name !== currentProjectName && p.status === "active") {
8273
+ await setStatus(p.name, "paused");
8274
+ paused.push(p.name);
8275
+ }
8276
+ }
8277
+ if (paused.length > 0) {
8278
+ const plural = paused.length === 1 ? "" : "s";
8279
+ console.log(
8280
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
8281
+ );
8282
+ }
8130
8283
  if (!runApply) {
8131
8284
  result.steps.apply.skipped = true;
8132
8285
  console.log("skipped instrumentation (--no-instrument)");
@@ -8799,7 +8952,7 @@ async function runSync(opts) {
8799
8952
  snapshotPath = target;
8800
8953
  }
8801
8954
  const skipApply = opts.dryRun || opts.noInstrument;
8802
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8955
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8803
8956
  const warnings = [];
8804
8957
  let daemonState = "skipped";
8805
8958
  let exitCode = 0;
@@ -9147,7 +9300,7 @@ async function buildPatchSections(services, project) {
9147
9300
  const installer = await pickInstaller(svc.dir);
9148
9301
  if (!installer) continue;
9149
9302
  const plan3 = await installer.plan(svc.dir, { project });
9150
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
9303
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
9151
9304
  sections.push({ installer: installer.name, plan: plan3 });
9152
9305
  }
9153
9306
  return sections;
@@ -9191,13 +9344,15 @@ async function runInit(opts) {
9191
9344
  written.push(gitignoreResult.file);
9192
9345
  }
9193
9346
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9347
+ let currentProjectName = opts.project;
9194
9348
  try {
9195
- await addProject({
9349
+ const entry2 = await addProject({
9196
9350
  name: opts.project,
9197
9351
  path: opts.scanPath,
9198
9352
  languages,
9199
9353
  status: "active"
9200
9354
  });
9355
+ currentProjectName = entry2.name;
9201
9356
  } catch (err) {
9202
9357
  if (err instanceof ProjectNameCollisionError) {
9203
9358
  console.error(`neat init: ${err.message}`);
@@ -9206,11 +9361,27 @@ async function runInit(opts) {
9206
9361
  }
9207
9362
  throw err;
9208
9363
  }
9364
+ const siblings = await listProjects();
9365
+ const paused = [];
9366
+ for (const p of siblings) {
9367
+ if (p.name !== currentProjectName && p.status === "active") {
9368
+ await setStatus(p.name, "paused");
9369
+ paused.push(p.name);
9370
+ }
9371
+ }
9372
+ if (paused.length > 0) {
9373
+ const plural = paused.length === 1 ? "" : "s";
9374
+ console.log(
9375
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
9376
+ );
9377
+ }
9209
9378
  if (!opts.noInstall) {
9210
9379
  if (opts.apply) {
9211
9380
  let instrumented = 0;
9212
9381
  let alreadyInstrumented = 0;
9213
9382
  let libOnly = 0;
9383
+ let browserBundle = 0;
9384
+ let reactNative = 0;
9214
9385
  for (const section of sections) {
9215
9386
  const installer = INSTALLERS.find((i) => i.name === section.installer);
9216
9387
  if (!installer) continue;
@@ -9222,13 +9393,24 @@ async function runInit(opts) {
9222
9393
  alreadyInstrumented++;
9223
9394
  } else if (outcome.outcome === "lib-only") {
9224
9395
  libOnly++;
9396
+ } else if (outcome.outcome === "browser-bundle") {
9397
+ browserBundle++;
9398
+ console.log(`skipping ${section.plan.serviceDir}: browser bundle; browser-OTel support lands in a future release.`);
9399
+ } else if (outcome.outcome === "react-native") {
9400
+ reactNative++;
9401
+ console.log(`skipping ${section.plan.serviceDir}: React Native target; browser-OTel support lands in a future release.`);
9225
9402
  }
9226
9403
  }
9227
9404
  if (sections.length > 0) {
9228
9405
  console.log("");
9229
- console.log(
9230
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
9231
- );
9406
+ const parts = [
9407
+ `instrumented ${instrumented}`,
9408
+ `already-instrumented ${alreadyInstrumented}`,
9409
+ `lib-only ${libOnly}`
9410
+ ];
9411
+ if (browserBundle > 0) parts.push(`browser-bundle ${browserBundle}`);
9412
+ if (reactNative > 0) parts.push(`react-native ${reactNative}`);
9413
+ console.log(`apply: ${parts.join(", ")}`);
9232
9414
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
9233
9415
  }
9234
9416
  } else {
@@ -9718,7 +9900,7 @@ async function runQueryVerb(cmd, parsed) {
9718
9900
  }
9719
9901
  }
9720
9902
  var entry = process.argv[1] ?? "";
9721
- if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
9903
+ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat") || entry.endsWith("/neat.is")) {
9722
9904
  main().catch((err) => {
9723
9905
  console.error(err);
9724
9906
  process.exit(1);