@neat.is/core 0.4.3 → 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,8 +6600,8 @@ export async function register() {
6542
6600
  }
6543
6601
  `;
6544
6602
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6545
- process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
6546
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
6603
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6604
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6547
6605
 
6548
6606
  import { NodeSDK } from '@opentelemetry/sdk-node'
6549
6607
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
@@ -6551,30 +6609,25 @@ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentation
6551
6609
  new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6552
6610
  `;
6553
6611
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6554
- process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
6555
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
6612
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6613
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6556
6614
 
6557
6615
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6558
6616
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6559
6617
 
6560
6618
  new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6561
6619
  `;
6562
- function renderNextInstrumentationNode(template, projectName) {
6563
- return template.replace(/__PROJECT_NAME__/g, projectName);
6620
+ function renderNextInstrumentationNode(template, serviceName, projectName) {
6621
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6564
6622
  }
6565
6623
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6566
6624
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6567
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6568
- // the OTel SDK starts so the exporter target and service name are in scope.
6569
- import { fileURLToPath } from 'node:url'
6570
- import path from 'node:path'
6571
- 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
+
6572
6628
  import { NodeSDK } from '@opentelemetry/sdk-node'
6573
6629
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6574
6630
 
6575
- const here = path.dirname(fileURLToPath(import.meta.url))
6576
- dotenv.config({ path: path.join(here, '.env.neat') })
6577
-
6578
6631
  const sdk = new NodeSDK({
6579
6632
  serviceName: process.env.OTEL_SERVICE_NAME,
6580
6633
  instrumentations: [getNodeAutoInstrumentations()],
@@ -6582,14 +6635,9 @@ const sdk = new NodeSDK({
6582
6635
  sdk.start()
6583
6636
  `;
6584
6637
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6585
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6586
- // the OTel SDK starts so the exporter target and service name are in scope.
6587
- const path = require('node:path')
6588
- try {
6589
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6590
- } catch (err) {
6591
- // dotenv unavailable \u2014 fall through to process.env.
6592
- }
6638
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6639
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6640
+
6593
6641
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6594
6642
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6595
6643
 
@@ -6599,6 +6647,9 @@ const sdk = new NodeSDK({
6599
6647
  })
6600
6648
  sdk.start()
6601
6649
  `;
6650
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
6651
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6652
+ }
6602
6653
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6603
6654
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6604
6655
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -6661,11 +6712,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
6661
6712
  var SDK_PACKAGES = [
6662
6713
  { name: "@opentelemetry/api", version: "^1.9.0" },
6663
6714
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
6664
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
6665
- // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
6666
- // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
6667
- // before the auto-instrumentation hook attaches.
6668
- { name: "dotenv", version: "^16.4.5" }
6715
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
6669
6716
  ];
6670
6717
  var OTEL_ENV = {
6671
6718
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
@@ -6673,13 +6720,36 @@ var OTEL_ENV = {
6673
6720
  // patch render: it documents the key/value the user can inspect in the
6674
6721
  // generated .env.neat.
6675
6722
  file: null,
6676
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6677
- value: "http://localhost:4318"
6723
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
6724
+ value: "http://localhost:4318/projects/<project>/v1/traces"
6678
6725
  };
6679
- 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) {
6680
6730
  if (project && project.length > 0) return project;
6681
6731
  return pkg.name ?? import_node_path40.default.basename(serviceDir);
6682
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
+ }
6683
6753
  async function readPackageJson(serviceDir) {
6684
6754
  try {
6685
6755
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6926,7 +6996,6 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6926
6996
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6927
6997
  const dependencyEdits = [];
6928
6998
  for (const sdk of SDK_PACKAGES) {
6929
- if (sdk.name === "dotenv") continue;
6930
6999
  if (sdk.name in existingDeps) continue;
6931
7000
  dependencyEdits.push({
6932
7001
  file: manifestPath,
@@ -6935,7 +7004,8 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6935
7004
  version: sdk.version
6936
7005
  });
6937
7006
  }
6938
- const projectName = envServiceName(pkg, serviceDir, project);
7007
+ const svcName = serviceNodeName(pkg, serviceDir);
7008
+ const projectName = projectToken(pkg, serviceDir, project);
6939
7009
  const generatedFiles = [];
6940
7010
  if (!await exists2(instrumentationFile)) {
6941
7011
  generatedFiles.push({
@@ -6949,6 +7019,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6949
7019
  file: instrumentationNodeFile,
6950
7020
  contents: renderNextInstrumentationNode(
6951
7021
  useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
7022
+ svcName,
6952
7023
  projectName
6953
7024
  ),
6954
7025
  skipIfExists: true
@@ -6957,7 +7028,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6957
7028
  if (!await exists2(envNeatFile)) {
6958
7029
  generatedFiles.push({
6959
7030
  file: envNeatFile,
6960
- contents: renderEnvNeat(projectName),
7031
+ contents: renderEnvNeat(svcName, projectName),
6961
7032
  skipIfExists: true
6962
7033
  });
6963
7034
  }
@@ -7018,11 +7089,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
7018
7089
  if (!await exists2(envNeatFile)) {
7019
7090
  generatedFiles.push({
7020
7091
  file: envNeatFile,
7021
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7092
+ contents: renderEnvNeat(
7093
+ serviceNodeName(pkg, serviceDir),
7094
+ projectToken(pkg, serviceDir, project)
7095
+ ),
7022
7096
  skipIfExists: true
7023
7097
  });
7024
7098
  }
7025
7099
  }
7100
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
7101
+ return renderFrameworkOtelInit(
7102
+ template,
7103
+ serviceNodeName(pkg, serviceDir),
7104
+ projectToken(pkg, serviceDir, project)
7105
+ );
7106
+ }
7026
7107
  function fileImportsOtelHook(raw, specifiers) {
7027
7108
  const lines = raw.split(/\r?\n/);
7028
7109
  for (const line of lines) {
@@ -7048,7 +7129,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
7048
7129
  if (!await exists2(otelServerFile)) {
7049
7130
  generatedFiles.push({
7050
7131
  file: otelServerFile,
7051
- 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
+ ),
7052
7138
  skipIfExists: true
7053
7139
  });
7054
7140
  }
@@ -7102,7 +7188,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
7102
7188
  if (!await exists2(otelInitFile)) {
7103
7189
  generatedFiles.push({
7104
7190
  file: otelInitFile,
7105
- 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
+ ),
7106
7197
  skipIfExists: true
7107
7198
  });
7108
7199
  }
@@ -7165,7 +7256,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7165
7256
  if (!await exists2(otelInitFile)) {
7166
7257
  generatedFiles.push({
7167
7258
  file: otelInitFile,
7168
- 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
+ ),
7169
7265
  skipIfExists: true
7170
7266
  });
7171
7267
  }
@@ -7221,7 +7317,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
7221
7317
  if (!await exists2(otelInitFile)) {
7222
7318
  generatedFiles.push({
7223
7319
  file: otelInitFile,
7224
- 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
+ ),
7225
7326
  skipIfExists: true
7226
7327
  });
7227
7328
  }
@@ -7313,6 +7414,10 @@ async function plan(serviceDir, opts) {
7313
7414
  return planAstro(serviceDir, pkg, manifestPath, project);
7314
7415
  }
7315
7416
  }
7417
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
7418
+ if (runtimeKind !== "node") {
7419
+ return { ...empty, runtimeKind };
7420
+ }
7316
7421
  const entryFile = await resolveEntry(serviceDir, pkg);
7317
7422
  if (!entryFile) {
7318
7423
  return { ...empty, libOnly: true };
@@ -7347,18 +7452,20 @@ async function plan(serviceDir, opts) {
7347
7452
  } catch {
7348
7453
  return { ...empty, libOnly: true };
7349
7454
  }
7455
+ const svcName = serviceNodeName(pkg, serviceDir);
7456
+ const projectName = projectToken(pkg, serviceDir, project);
7350
7457
  const generatedFiles = [];
7351
7458
  if (!await exists2(otelInitFile)) {
7352
7459
  generatedFiles.push({
7353
7460
  file: otelInitFile,
7354
- contents: otelInitContents(flavor),
7461
+ contents: renderNodeOtelInit(otelInitContents(flavor), svcName, projectName),
7355
7462
  skipIfExists: true
7356
7463
  });
7357
7464
  }
7358
7465
  if (!await exists2(envNeatFile)) {
7359
7466
  generatedFiles.push({
7360
7467
  file: envNeatFile,
7361
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7468
+ contents: renderEnvNeat(svcName, projectName),
7362
7469
  skipIfExists: true
7363
7470
  });
7364
7471
  }
@@ -7415,6 +7522,22 @@ async function apply(installPlan) {
7415
7522
  writtenFiles: []
7416
7523
  };
7417
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
+ }
7418
7541
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
7419
7542
  return {
7420
7543
  serviceDir,
@@ -7901,11 +8024,13 @@ async function applyInstallersOver(services, project) {
7901
8024
  let instrumented = 0;
7902
8025
  let already = 0;
7903
8026
  let libOnly = 0;
8027
+ let browserBundle = 0;
8028
+ let reactNative = 0;
7904
8029
  for (const svc of services) {
7905
8030
  const installer = await pickInstaller(svc.dir);
7906
8031
  if (!installer) continue;
7907
8032
  const plan3 = await installer.plan(svc.dir, { project });
7908
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
8033
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
7909
8034
  already++;
7910
8035
  continue;
7911
8036
  }
@@ -7913,8 +8038,15 @@ async function applyInstallersOver(services, project) {
7913
8038
  if (outcome.outcome === "instrumented") instrumented++;
7914
8039
  else if (outcome.outcome === "already-instrumented") already++;
7915
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
+ }
7916
8048
  }
7917
- return { instrumented, alreadyInstrumented: already, libOnly };
8049
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
7918
8050
  }
7919
8051
  async function promptYesNo(question) {
7920
8052
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
@@ -8118,13 +8250,15 @@ async function runOrchestrator(opts) {
8118
8250
  if (gi.action !== "unchanged") {
8119
8251
  console.log(`${gi.action} .gitignore (neat-out/)`);
8120
8252
  }
8253
+ let currentProjectName = opts.project;
8121
8254
  try {
8122
- await addProject({
8255
+ const entry2 = await addProject({
8123
8256
  name: opts.project,
8124
8257
  path: opts.scanPath,
8125
8258
  languages,
8126
8259
  status: "active"
8127
8260
  });
8261
+ currentProjectName = entry2.name;
8128
8262
  } catch (err) {
8129
8263
  if (!(err instanceof ProjectNameCollisionError)) throw err;
8130
8264
  console.error(`neat: ${err.message}`);
@@ -8132,6 +8266,20 @@ async function runOrchestrator(opts) {
8132
8266
  result.exitCode = 1;
8133
8267
  return result;
8134
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
+ }
8135
8283
  if (!runApply) {
8136
8284
  result.steps.apply.skipped = true;
8137
8285
  console.log("skipped instrumentation (--no-instrument)");
@@ -8804,7 +8952,7 @@ async function runSync(opts) {
8804
8952
  snapshotPath = target;
8805
8953
  }
8806
8954
  const skipApply = opts.dryRun || opts.noInstrument;
8807
- 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);
8808
8956
  const warnings = [];
8809
8957
  let daemonState = "skipped";
8810
8958
  let exitCode = 0;
@@ -9152,7 +9300,7 @@ async function buildPatchSections(services, project) {
9152
9300
  const installer = await pickInstaller(svc.dir);
9153
9301
  if (!installer) continue;
9154
9302
  const plan3 = await installer.plan(svc.dir, { project });
9155
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
9303
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
9156
9304
  sections.push({ installer: installer.name, plan: plan3 });
9157
9305
  }
9158
9306
  return sections;
@@ -9196,13 +9344,15 @@ async function runInit(opts) {
9196
9344
  written.push(gitignoreResult.file);
9197
9345
  }
9198
9346
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9347
+ let currentProjectName = opts.project;
9199
9348
  try {
9200
- await addProject({
9349
+ const entry2 = await addProject({
9201
9350
  name: opts.project,
9202
9351
  path: opts.scanPath,
9203
9352
  languages,
9204
9353
  status: "active"
9205
9354
  });
9355
+ currentProjectName = entry2.name;
9206
9356
  } catch (err) {
9207
9357
  if (err instanceof ProjectNameCollisionError) {
9208
9358
  console.error(`neat init: ${err.message}`);
@@ -9211,11 +9361,27 @@ async function runInit(opts) {
9211
9361
  }
9212
9362
  throw err;
9213
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
+ }
9214
9378
  if (!opts.noInstall) {
9215
9379
  if (opts.apply) {
9216
9380
  let instrumented = 0;
9217
9381
  let alreadyInstrumented = 0;
9218
9382
  let libOnly = 0;
9383
+ let browserBundle = 0;
9384
+ let reactNative = 0;
9219
9385
  for (const section of sections) {
9220
9386
  const installer = INSTALLERS.find((i) => i.name === section.installer);
9221
9387
  if (!installer) continue;
@@ -9227,13 +9393,24 @@ async function runInit(opts) {
9227
9393
  alreadyInstrumented++;
9228
9394
  } else if (outcome.outcome === "lib-only") {
9229
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.`);
9230
9402
  }
9231
9403
  }
9232
9404
  if (sections.length > 0) {
9233
9405
  console.log("");
9234
- console.log(
9235
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
9236
- );
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(", ")}`);
9237
9414
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
9238
9415
  }
9239
9416
  } else {