@neat.is/core 0.4.3 → 0.4.5

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
@@ -301,12 +301,15 @@ function parseOtlpRequest(body) {
301
301
  const out = [];
302
302
  for (const rs of body.resourceSpans ?? []) {
303
303
  const resourceAttrs = attrsToRecord(rs.resource?.attributes);
304
- const service = typeof resourceAttrs["service.name"] === "string" ? resourceAttrs["service.name"] : "unknown";
304
+ const rawServiceName = resourceAttrs["service.name"];
305
+ const resourceServiceNamePresent = typeof rawServiceName === "string" && rawServiceName.length > 0;
306
+ const service = resourceServiceNamePresent ? rawServiceName : "unidentified";
305
307
  for (const ss of rs.scopeSpans ?? []) {
306
308
  for (const span of ss.spans ?? []) {
307
309
  const attrs = attrsToRecord(span.attributes);
308
310
  const parsed = {
309
311
  service,
312
+ resourceServiceNamePresent,
310
313
  traceId: span.traceId ?? "",
311
314
  spanId: span.spanId ?? "",
312
315
  parentSpanId: span.parentSpanId || void 0,
@@ -401,6 +404,64 @@ async function buildOtelReceiver(opts) {
401
404
  for (const s of spans) queue.push(s);
402
405
  drainPromise = drainPromise.then(() => drain());
403
406
  };
407
+ const projectQueue = [];
408
+ let projectDraining = false;
409
+ let projectDrainPromise = Promise.resolve();
410
+ const drainProject = async () => {
411
+ if (projectDraining) return;
412
+ projectDraining = true;
413
+ try {
414
+ while (projectQueue.length > 0) {
415
+ const { project, span } = projectQueue.shift();
416
+ try {
417
+ if (opts.onProjectSpan) {
418
+ await opts.onProjectSpan(project, span);
419
+ } else {
420
+ await opts.onSpan(span);
421
+ }
422
+ } catch (err) {
423
+ console.warn(`[neat] otel handler error: ${err.message}`);
424
+ }
425
+ }
426
+ } finally {
427
+ projectDraining = false;
428
+ }
429
+ };
430
+ const enqueueProject = (project, spans) => {
431
+ if (spans.length === 0) return;
432
+ for (const s of spans) projectQueue.push({ project, span: s });
433
+ projectDrainPromise = projectDrainPromise.then(() => drainProject());
434
+ };
435
+ const legacyEndpointWarned = /* @__PURE__ */ new Set();
436
+ function warnLegacyEndpoint(serviceName) {
437
+ if (legacyEndpointWarned.has(serviceName)) return;
438
+ legacyEndpointWarned.add(serviceName);
439
+ console.warn(
440
+ `[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name="${serviceName}").`
441
+ );
442
+ }
443
+ async function readOtlpBody(req) {
444
+ const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
445
+ if (ct === "application/x-protobuf") {
446
+ try {
447
+ const body = await decodeProtobufBody(req.body);
448
+ return { ok: true, body, flavor: "protobuf" };
449
+ } catch (err) {
450
+ return { ok: false, code: 400, error: `protobuf decode failed: ${err.message}` };
451
+ }
452
+ }
453
+ if (!ct || ct === "application/json") {
454
+ return { ok: true, body: req.body ?? {}, flavor: "json" };
455
+ }
456
+ return { ok: false, code: 415, error: `unsupported content-type: ${ct}` };
457
+ }
458
+ function sendOtlpSuccess(reply, flavor) {
459
+ if (flavor === "protobuf") {
460
+ const buf = encodeProtobufResponseBody();
461
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
462
+ }
463
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
464
+ }
404
465
  app.addContentTypeParser(
405
466
  "application/x-protobuf",
406
467
  { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
@@ -410,26 +471,44 @@ async function buildOtelReceiver(opts) {
410
471
  );
411
472
  app.get("/health", async () => ({ ok: true }));
412
473
  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";
474
+ const result = await readOtlpBody(req);
475
+ if (!result.ok) {
476
+ return reply.code(result.code).send({ error: result.error });
477
+ }
478
+ const spans = parseOtlpRequest(result.body);
479
+ for (const s of spans) warnLegacyEndpoint(s.service);
480
+ if (opts.onErrorSpanSync) {
418
481
  try {
419
- body = await decodeProtobufBody(req.body);
482
+ for (const span of spans) {
483
+ if (span.statusCode === 2) await opts.onErrorSpanSync(span);
484
+ }
420
485
  } catch (err) {
421
- return reply.code(400).send({
422
- error: `protobuf decode failed: ${err.message}`
486
+ return reply.code(500).send({
487
+ error: `error-event write failed: ${err.message}`
423
488
  });
424
489
  }
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
490
  }
431
- const spans = parseOtlpRequest(body);
432
- if (opts.onErrorSpanSync) {
491
+ enqueue(spans);
492
+ return sendOtlpSuccess(reply, result.flavor);
493
+ });
494
+ app.post("/projects/:project/v1/traces", async (req, reply) => {
495
+ const project = req.params.project;
496
+ const result = await readOtlpBody(req);
497
+ if (!result.ok) {
498
+ return reply.code(result.code).send({ error: result.error });
499
+ }
500
+ const spans = parseOtlpRequest(result.body);
501
+ if (opts.onProjectErrorSpanSync) {
502
+ try {
503
+ for (const span of spans) {
504
+ if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span);
505
+ }
506
+ } catch (err) {
507
+ return reply.code(500).send({
508
+ error: `error-event write failed: ${err.message}`
509
+ });
510
+ }
511
+ } else if (opts.onErrorSpanSync) {
433
512
  try {
434
513
  for (const span of spans) {
435
514
  if (span.statusCode === 2) await opts.onErrorSpanSync(span);
@@ -440,17 +519,13 @@ async function buildOtelReceiver(opts) {
440
519
  });
441
520
  }
442
521
  }
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: {} });
522
+ enqueueProject(project, spans);
523
+ return sendOtlpSuccess(reply, result.flavor);
449
524
  });
450
525
  const decorated = app;
451
526
  decorated.flushPending = async () => {
452
- while (queue.length > 0 || draining) {
453
- await drainPromise;
527
+ while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {
528
+ await Promise.all([drainPromise, projectDrainPromise]);
454
529
  }
455
530
  };
456
531
  return decorated;
@@ -1548,6 +1623,14 @@ function thresholdForEdgeType(edgeType, overrides) {
1548
1623
  function nowIso(ctx) {
1549
1624
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1550
1625
  }
1626
+ var unidentifiedWarnedProjects = /* @__PURE__ */ new Set();
1627
+ function warnUnidentifiedSpan(project) {
1628
+ if (unidentifiedWarnedProjects.has(project)) return;
1629
+ unidentifiedWarnedProjects.add(project);
1630
+ console.warn(
1631
+ `[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
1632
+ );
1633
+ }
1551
1634
  function pickAttr(span, ...keys) {
1552
1635
  for (const k of keys) {
1553
1636
  const v = span.attributes[k];
@@ -1802,6 +1885,9 @@ async function handleSpan(ctx, span) {
1802
1885
  const ts = span.startTimeIso ?? nowIso(ctx);
1803
1886
  const nowMs = ctx.now ? ctx.now() : Date.now();
1804
1887
  const env = span.env ?? "unknown";
1888
+ if (span.resourceServiceNamePresent === false) {
1889
+ warnUnidentifiedSpan(ctx.project ?? DEFAULT_PROJECT);
1890
+ }
1805
1891
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1806
1892
  const isError = span.statusCode === 2;
1807
1893
  cacheSpanService(span, nowMs);
@@ -6484,45 +6570,49 @@ var import_node_path40 = __toESM(require("path"), 1);
6484
6570
  init_cjs_shims();
6485
6571
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
6486
6572
  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
- }
6495
- require('@opentelemetry/auto-instrumentations-node/register')
6573
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6574
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6575
+
6576
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6577
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6578
+
6579
+ const instrumentations = [getNodeAutoInstrumentations()]
6580
+ __INSTRUMENTATION_BLOCK__
6581
+ new NodeSDK({ instrumentations }).start()
6496
6582
  `;
6497
6583
  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'
6584
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6585
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6503
6586
 
6504
- const here = path.dirname(fileURLToPath(import.meta.url))
6505
- dotenv.config({ path: path.join(here, '.env.neat') })
6587
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6588
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6506
6589
 
6507
- await import('@opentelemetry/auto-instrumentations-node/register')
6590
+ const instrumentations = [getNodeAutoInstrumentations()]
6591
+ __INSTRUMENTATION_BLOCK__
6592
+ new NodeSDK({ instrumentations }).start()
6508
6593
  `;
6509
6594
  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'
6595
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6596
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6515
6597
 
6516
- const here = path.dirname(fileURLToPath(import.meta.url))
6517
- dotenv.config({ path: path.join(here, '.env.neat') })
6598
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6599
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6518
6600
 
6519
- await import('@opentelemetry/auto-instrumentations-node/register')
6601
+ const instrumentations = [getNodeAutoInstrumentations()]
6602
+ __INSTRUMENTATION_BLOCK__
6603
+ new NodeSDK({ instrumentations }).start()
6520
6604
  `;
6521
- function renderEnvNeat(projectName) {
6605
+ function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
6606
+ const block = registrations.length === 0 ? "" : `
6607
+ ${registrations.join("\n")}
6608
+ `;
6609
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
6610
+ }
6611
+ function renderEnvNeat(serviceName, projectName) {
6522
6612
  return [
6523
6613
  "# Generated by `neat init --apply` (ADR-069).",
6524
- `OTEL_SERVICE_NAME=${projectName}`,
6525
- "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
6614
+ `OTEL_SERVICE_NAME=${serviceName}`,
6615
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
6526
6616
  ""
6527
6617
  ].join("\n");
6528
6618
  }
@@ -6542,39 +6632,41 @@ export async function register() {
6542
6632
  }
6543
6633
  `;
6544
6634
  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'
6635
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6636
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6547
6637
 
6548
6638
  import { NodeSDK } from '@opentelemetry/sdk-node'
6549
6639
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6550
6640
 
6551
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6641
+ const instrumentations = [getNodeAutoInstrumentations()]
6642
+ __INSTRUMENTATION_BLOCK__
6643
+ new NodeSDK({ instrumentations }).start()
6552
6644
  `;
6553
6645
  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'
6646
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6647
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6556
6648
 
6557
6649
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6558
6650
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6559
6651
 
6560
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6652
+ const instrumentations = [getNodeAutoInstrumentations()]
6653
+ __INSTRUMENTATION_BLOCK__
6654
+ new NodeSDK({ instrumentations }).start()
6561
6655
  `;
6562
- function renderNextInstrumentationNode(template, projectName) {
6563
- return template.replace(/__PROJECT_NAME__/g, projectName);
6656
+ function renderNextInstrumentationNode(template, serviceName, projectName, registrations = []) {
6657
+ const block = registrations.length === 0 ? "" : `
6658
+ ${registrations.join("\n")}
6659
+ `;
6660
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
6564
6661
  }
6565
6662
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6566
6663
  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'
6664
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6665
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6666
+
6572
6667
  import { NodeSDK } from '@opentelemetry/sdk-node'
6573
6668
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6574
6669
 
6575
- const here = path.dirname(fileURLToPath(import.meta.url))
6576
- dotenv.config({ path: path.join(here, '.env.neat') })
6577
-
6578
6670
  const sdk = new NodeSDK({
6579
6671
  serviceName: process.env.OTEL_SERVICE_NAME,
6580
6672
  instrumentations: [getNodeAutoInstrumentations()],
@@ -6582,14 +6674,9 @@ const sdk = new NodeSDK({
6582
6674
  sdk.start()
6583
6675
  `;
6584
6676
  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
- }
6677
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6678
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6679
+
6593
6680
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6594
6681
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6595
6682
 
@@ -6599,6 +6686,9 @@ const sdk = new NodeSDK({
6599
6686
  })
6600
6687
  sdk.start()
6601
6688
  `;
6689
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
6690
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
6691
+ }
6602
6692
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6603
6693
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6604
6694
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -6661,25 +6751,56 @@ export const onRequest = defineMiddleware(async (context, next) => {
6661
6751
  var SDK_PACKAGES = [
6662
6752
  { name: "@opentelemetry/api", version: "^1.9.0" },
6663
6753
  { 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" }
6754
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
6669
6755
  ];
6756
+ function detectNonBundledInstrumentations(pkg) {
6757
+ const deps = allDeps(pkg);
6758
+ const out = [];
6759
+ if ("@prisma/client" in deps) {
6760
+ out.push({
6761
+ pkg: "@prisma/instrumentation",
6762
+ version: "^5.0.0",
6763
+ registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
6764
+ });
6765
+ }
6766
+ return out;
6767
+ }
6670
6768
  var OTEL_ENV = {
6671
6769
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
6672
6770
  // by the apply phase). The envEdits surface stays for the dry-run
6673
6771
  // patch render: it documents the key/value the user can inspect in the
6674
6772
  // generated .env.neat.
6675
6773
  file: null,
6676
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6677
- value: "http://localhost:4318"
6774
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
6775
+ value: "http://localhost:4318/projects/<project>/v1/traces"
6678
6776
  };
6679
- function envServiceName(pkg, serviceDir, project) {
6777
+ function serviceNodeName(pkg, serviceDir) {
6778
+ return pkg.name ?? import_node_path40.default.basename(serviceDir);
6779
+ }
6780
+ function projectToken(pkg, serviceDir, project) {
6680
6781
  if (project && project.length > 0) return project;
6681
6782
  return pkg.name ?? import_node_path40.default.basename(serviceDir);
6682
6783
  }
6784
+ async function readJsonFile(p) {
6785
+ try {
6786
+ const raw = await import_node_fs25.promises.readFile(p, "utf8");
6787
+ return JSON.parse(raw);
6788
+ } catch {
6789
+ return null;
6790
+ }
6791
+ }
6792
+ async function detectRuntimeKind(pkgRoot, pkg) {
6793
+ const deps = allDeps(pkg);
6794
+ if ("react-native" in deps || "expo" in deps) return "react-native";
6795
+ const appJson = await readJsonFile(import_node_path40.default.join(pkgRoot, "app.json"));
6796
+ if (appJson && typeof appJson === "object" && "expo" in appJson) {
6797
+ return "react-native";
6798
+ }
6799
+ 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) {
6800
+ return "browser-bundle";
6801
+ }
6802
+ return "node";
6803
+ }
6683
6804
  async function readPackageJson(serviceDir) {
6684
6805
  try {
6685
6806
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6926,7 +7047,6 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6926
7047
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6927
7048
  const dependencyEdits = [];
6928
7049
  for (const sdk of SDK_PACKAGES) {
6929
- if (sdk.name === "dotenv") continue;
6930
7050
  if (sdk.name in existingDeps) continue;
6931
7051
  dependencyEdits.push({
6932
7052
  file: manifestPath,
@@ -6935,7 +7055,19 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6935
7055
  version: sdk.version
6936
7056
  });
6937
7057
  }
6938
- const projectName = envServiceName(pkg, serviceDir, project);
7058
+ const nonBundled = detectNonBundledInstrumentations(pkg);
7059
+ for (const inst of nonBundled) {
7060
+ if (inst.pkg in existingDeps) continue;
7061
+ dependencyEdits.push({
7062
+ file: manifestPath,
7063
+ kind: "add",
7064
+ name: inst.pkg,
7065
+ version: inst.version
7066
+ });
7067
+ }
7068
+ const svcName = serviceNodeName(pkg, serviceDir);
7069
+ const projectName = projectToken(pkg, serviceDir, project);
7070
+ const registrations = nonBundled.map((i) => i.registration);
6939
7071
  const generatedFiles = [];
6940
7072
  if (!await exists2(instrumentationFile)) {
6941
7073
  generatedFiles.push({
@@ -6949,7 +7081,9 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6949
7081
  file: instrumentationNodeFile,
6950
7082
  contents: renderNextInstrumentationNode(
6951
7083
  useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
6952
- projectName
7084
+ svcName,
7085
+ projectName,
7086
+ registrations
6953
7087
  ),
6954
7088
  skipIfExists: true
6955
7089
  });
@@ -6957,7 +7091,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
6957
7091
  if (!await exists2(envNeatFile)) {
6958
7092
  generatedFiles.push({
6959
7093
  file: envNeatFile,
6960
- contents: renderEnvNeat(projectName),
7094
+ contents: renderEnvNeat(svcName, projectName),
6961
7095
  skipIfExists: true
6962
7096
  });
6963
7097
  }
@@ -7018,11 +7152,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
7018
7152
  if (!await exists2(envNeatFile)) {
7019
7153
  generatedFiles.push({
7020
7154
  file: envNeatFile,
7021
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7155
+ contents: renderEnvNeat(
7156
+ serviceNodeName(pkg, serviceDir),
7157
+ projectToken(pkg, serviceDir, project)
7158
+ ),
7022
7159
  skipIfExists: true
7023
7160
  });
7024
7161
  }
7025
7162
  }
7163
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
7164
+ return renderFrameworkOtelInit(
7165
+ template,
7166
+ serviceNodeName(pkg, serviceDir),
7167
+ projectToken(pkg, serviceDir, project)
7168
+ );
7169
+ }
7026
7170
  function fileImportsOtelHook(raw, specifiers) {
7027
7171
  const lines = raw.split(/\r?\n/);
7028
7172
  for (const line of lines) {
@@ -7048,7 +7192,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
7048
7192
  if (!await exists2(otelServerFile)) {
7049
7193
  generatedFiles.push({
7050
7194
  file: otelServerFile,
7051
- contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
7195
+ contents: renderFrameworkOtelInitForPkg(
7196
+ useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
7197
+ pkg,
7198
+ serviceDir,
7199
+ project
7200
+ ),
7052
7201
  skipIfExists: true
7053
7202
  });
7054
7203
  }
@@ -7102,7 +7251,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
7102
7251
  if (!await exists2(otelInitFile)) {
7103
7252
  generatedFiles.push({
7104
7253
  file: otelInitFile,
7105
- contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7254
+ contents: renderFrameworkOtelInitForPkg(
7255
+ useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7256
+ pkg,
7257
+ serviceDir,
7258
+ project
7259
+ ),
7106
7260
  skipIfExists: true
7107
7261
  });
7108
7262
  }
@@ -7165,7 +7319,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7165
7319
  if (!await exists2(otelInitFile)) {
7166
7320
  generatedFiles.push({
7167
7321
  file: otelInitFile,
7168
- contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7322
+ contents: renderFrameworkOtelInitForPkg(
7323
+ useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7324
+ pkg,
7325
+ serviceDir,
7326
+ project
7327
+ ),
7169
7328
  skipIfExists: true
7170
7329
  });
7171
7330
  }
@@ -7221,7 +7380,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
7221
7380
  if (!await exists2(otelInitFile)) {
7222
7381
  generatedFiles.push({
7223
7382
  file: otelInitFile,
7224
- contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7383
+ contents: renderFrameworkOtelInitForPkg(
7384
+ useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7385
+ pkg,
7386
+ serviceDir,
7387
+ project
7388
+ ),
7225
7389
  skipIfExists: true
7226
7390
  });
7227
7391
  }
@@ -7269,51 +7433,73 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
7269
7433
  framework: "astro"
7270
7434
  };
7271
7435
  }
7272
- async function plan(serviceDir, opts) {
7273
- const pkg = await readPackageJson(serviceDir);
7274
- const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
7275
- const project = opts?.project;
7276
- const empty = {
7277
- language: "javascript",
7278
- serviceDir,
7279
- dependencyEdits: [],
7280
- entrypointEdits: [],
7281
- envEdits: [],
7282
- generatedFiles: []
7283
- };
7284
- if (!pkg) return empty;
7436
+ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
7285
7437
  if (hasNextDependency(pkg)) {
7286
7438
  const nextConfig = await findNextConfig(serviceDir);
7287
7439
  if (nextConfig) {
7288
- return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
7440
+ return () => planNext(serviceDir, pkg, manifestPath, nextConfig, project);
7289
7441
  }
7290
7442
  }
7291
7443
  if (hasRemixDependency(pkg)) {
7292
7444
  const remixEntry = await findRemixEntry(serviceDir);
7293
7445
  if (remixEntry) {
7294
- return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
7446
+ return () => planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
7295
7447
  }
7296
7448
  }
7297
7449
  if (hasSvelteKitDependency(pkg)) {
7298
7450
  const hooks = await findSvelteKitHooks(serviceDir);
7299
7451
  const config = await findSvelteKitConfig(serviceDir);
7300
7452
  if (hooks || config) {
7301
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
7453
+ return () => planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
7302
7454
  }
7303
7455
  }
7304
7456
  if (hasNuxtDependency(pkg)) {
7305
7457
  const nuxtConfig = await findNuxtConfig(serviceDir);
7306
7458
  if (nuxtConfig) {
7307
- return planNuxt(serviceDir, pkg, manifestPath, project);
7459
+ return () => planNuxt(serviceDir, pkg, manifestPath, project);
7308
7460
  }
7309
7461
  }
7310
7462
  if (hasAstroDependency(pkg)) {
7311
7463
  const astroConfig = await findAstroConfig(serviceDir);
7312
7464
  if (astroConfig) {
7313
- return planAstro(serviceDir, pkg, manifestPath, project);
7465
+ return () => planAstro(serviceDir, pkg, manifestPath, project);
7314
7466
  }
7315
7467
  }
7316
- const entryFile = await resolveEntry(serviceDir, pkg);
7468
+ return null;
7469
+ }
7470
+ async function plan(serviceDir, opts) {
7471
+ const pkg = await readPackageJson(serviceDir);
7472
+ const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
7473
+ const project = opts?.project;
7474
+ const empty = {
7475
+ language: "javascript",
7476
+ serviceDir,
7477
+ dependencyEdits: [],
7478
+ entrypointEdits: [],
7479
+ envEdits: [],
7480
+ generatedFiles: []
7481
+ };
7482
+ if (!pkg) return empty;
7483
+ const frameworkDispatch = await findFrameworkDispatch(
7484
+ serviceDir,
7485
+ pkg,
7486
+ manifestPath,
7487
+ project
7488
+ );
7489
+ let entryFile = null;
7490
+ if (!frameworkDispatch) {
7491
+ entryFile = await resolveEntry(serviceDir, pkg);
7492
+ if (!entryFile) {
7493
+ return { ...empty, libOnly: true };
7494
+ }
7495
+ }
7496
+ if (frameworkDispatch) {
7497
+ return frameworkDispatch();
7498
+ }
7499
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
7500
+ if (runtimeKind !== "node") {
7501
+ return { ...empty, runtimeKind };
7502
+ }
7317
7503
  if (!entryFile) {
7318
7504
  return { ...empty, libOnly: true };
7319
7505
  }
@@ -7331,6 +7517,16 @@ async function plan(serviceDir, opts) {
7331
7517
  version: sdk.version
7332
7518
  });
7333
7519
  }
7520
+ const nonBundled = detectNonBundledInstrumentations(pkg);
7521
+ for (const inst of nonBundled) {
7522
+ if (inst.pkg in existingDeps) continue;
7523
+ dependencyEdits.push({
7524
+ file: manifestPath,
7525
+ kind: "add",
7526
+ name: inst.pkg,
7527
+ version: inst.version
7528
+ });
7529
+ }
7334
7530
  const entrypointEdits = [];
7335
7531
  try {
7336
7532
  const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
@@ -7347,18 +7543,26 @@ async function plan(serviceDir, opts) {
7347
7543
  } catch {
7348
7544
  return { ...empty, libOnly: true };
7349
7545
  }
7546
+ const svcName = serviceNodeName(pkg, serviceDir);
7547
+ const projectName = projectToken(pkg, serviceDir, project);
7548
+ const registrations = nonBundled.map((i) => i.registration);
7350
7549
  const generatedFiles = [];
7351
7550
  if (!await exists2(otelInitFile)) {
7352
7551
  generatedFiles.push({
7353
7552
  file: otelInitFile,
7354
- contents: otelInitContents(flavor),
7553
+ contents: renderNodeOtelInit(
7554
+ otelInitContents(flavor),
7555
+ svcName,
7556
+ projectName,
7557
+ registrations
7558
+ ),
7355
7559
  skipIfExists: true
7356
7560
  });
7357
7561
  }
7358
7562
  if (!await exists2(envNeatFile)) {
7359
7563
  generatedFiles.push({
7360
7564
  file: envNeatFile,
7361
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7565
+ contents: renderEnvNeat(svcName, projectName),
7362
7566
  skipIfExists: true
7363
7567
  });
7364
7568
  }
@@ -7415,6 +7619,22 @@ async function apply(installPlan) {
7415
7619
  writtenFiles: []
7416
7620
  };
7417
7621
  }
7622
+ if (installPlan.runtimeKind === "browser-bundle") {
7623
+ return {
7624
+ serviceDir,
7625
+ outcome: "browser-bundle",
7626
+ reason: "browser bundle; Node OTel SDK cannot run here",
7627
+ writtenFiles: []
7628
+ };
7629
+ }
7630
+ if (installPlan.runtimeKind === "react-native") {
7631
+ return {
7632
+ serviceDir,
7633
+ outcome: "react-native",
7634
+ reason: "React Native / Expo target; Node OTel SDK cannot run here",
7635
+ writtenFiles: []
7636
+ };
7637
+ }
7418
7638
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
7419
7639
  return {
7420
7640
  serviceDir,
@@ -7870,6 +8090,7 @@ function renderPatch(sections) {
7870
8090
  init_cjs_shims();
7871
8091
  var import_node_fs27 = require("fs");
7872
8092
  var import_node_http = __toESM(require("http"), 1);
8093
+ var import_node_net = __toESM(require("net"), 1);
7873
8094
  var import_node_path42 = __toESM(require("path"), 1);
7874
8095
  var import_node_child_process2 = require("child_process");
7875
8096
  var import_node_readline = __toESM(require("readline"), 1);
@@ -7901,11 +8122,13 @@ async function applyInstallersOver(services, project) {
7901
8122
  let instrumented = 0;
7902
8123
  let already = 0;
7903
8124
  let libOnly = 0;
8125
+ let browserBundle = 0;
8126
+ let reactNative = 0;
7904
8127
  for (const svc of services) {
7905
8128
  const installer = await pickInstaller(svc.dir);
7906
8129
  if (!installer) continue;
7907
8130
  const plan3 = await installer.plan(svc.dir, { project });
7908
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
8131
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
7909
8132
  already++;
7910
8133
  continue;
7911
8134
  }
@@ -7913,8 +8136,15 @@ async function applyInstallersOver(services, project) {
7913
8136
  if (outcome.outcome === "instrumented") instrumented++;
7914
8137
  else if (outcome.outcome === "already-instrumented") already++;
7915
8138
  else if (outcome.outcome === "lib-only") libOnly++;
8139
+ else if (outcome.outcome === "browser-bundle") {
8140
+ browserBundle++;
8141
+ console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
8142
+ } else if (outcome.outcome === "react-native") {
8143
+ reactNative++;
8144
+ console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
8145
+ }
7916
8146
  }
7917
- return { instrumented, alreadyInstrumented: already, libOnly };
8147
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
7918
8148
  }
7919
8149
  async function promptYesNo(question) {
7920
8150
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
@@ -8027,6 +8257,28 @@ async function waitForDaemonReady(restPort, timeoutMs) {
8027
8257
  stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
8028
8258
  };
8029
8259
  }
8260
+ var NEAT_PORTS = [8080, 4318, 6328];
8261
+ async function isPortFree(port) {
8262
+ return new Promise((resolve) => {
8263
+ const server = import_node_net.default.createServer();
8264
+ server.once("error", () => resolve(false));
8265
+ server.once("listening", () => server.close(() => resolve(true)));
8266
+ server.listen(port, "127.0.0.1");
8267
+ });
8268
+ }
8269
+ async function probePortsFree() {
8270
+ for (const port of NEAT_PORTS) {
8271
+ if (!await isPortFree(port)) return { free: false, held: port };
8272
+ }
8273
+ return { free: true };
8274
+ }
8275
+ function formatPortCollisionMessage(port) {
8276
+ return [
8277
+ `neat: port ${port} is in use; the NEAT daemon needs it.`,
8278
+ ` run \`neatd stop\` to release the previous daemon, or`,
8279
+ ` \`lsof -i :${port}\` to find the holding process.`
8280
+ ];
8281
+ }
8030
8282
  function spawnDaemonDetached() {
8031
8283
  const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
8032
8284
  const candidates = [
@@ -8118,13 +8370,15 @@ async function runOrchestrator(opts) {
8118
8370
  if (gi.action !== "unchanged") {
8119
8371
  console.log(`${gi.action} .gitignore (neat-out/)`);
8120
8372
  }
8373
+ let currentProjectName = opts.project;
8121
8374
  try {
8122
- await addProject({
8375
+ const entry2 = await addProject({
8123
8376
  name: opts.project,
8124
8377
  path: opts.scanPath,
8125
8378
  languages,
8126
8379
  status: "active"
8127
8380
  });
8381
+ currentProjectName = entry2.name;
8128
8382
  } catch (err) {
8129
8383
  if (!(err instanceof ProjectNameCollisionError)) throw err;
8130
8384
  console.error(`neat: ${err.message}`);
@@ -8132,6 +8386,20 @@ async function runOrchestrator(opts) {
8132
8386
  result.exitCode = 1;
8133
8387
  return result;
8134
8388
  }
8389
+ const siblings = await listProjects();
8390
+ const paused = [];
8391
+ for (const p of siblings) {
8392
+ if (p.name !== currentProjectName && p.status === "active") {
8393
+ await setStatus(p.name, "paused");
8394
+ paused.push(p.name);
8395
+ }
8396
+ }
8397
+ if (paused.length > 0) {
8398
+ const plural = paused.length === 1 ? "" : "s";
8399
+ console.log(
8400
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
8401
+ );
8402
+ }
8135
8403
  if (!runApply) {
8136
8404
  result.steps.apply.skipped = true;
8137
8405
  console.log("skipped instrumentation (--no-instrument)");
@@ -8147,6 +8415,14 @@ async function runOrchestrator(opts) {
8147
8415
  if (await checkDaemonHealth(restPort)) {
8148
8416
  result.steps.daemon = "already-running";
8149
8417
  } else {
8418
+ const probe = await probePortsFree();
8419
+ if (!probe.free) {
8420
+ for (const line of formatPortCollisionMessage(probe.held)) {
8421
+ console.error(line);
8422
+ }
8423
+ result.exitCode = 3;
8424
+ return result;
8425
+ }
8150
8426
  try {
8151
8427
  spawnDaemonDetached();
8152
8428
  } catch (err) {
@@ -8804,7 +9080,7 @@ async function runSync(opts) {
8804
9080
  snapshotPath = target;
8805
9081
  }
8806
9082
  const skipApply = opts.dryRun || opts.noInstrument;
8807
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
9083
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8808
9084
  const warnings = [];
8809
9085
  let daemonState = "skipped";
8810
9086
  let exitCode = 0;
@@ -8954,7 +9230,8 @@ function usage() {
8954
9230
  console.log(" 0 success");
8955
9231
  console.log(" 1 server error (4xx/5xx body printed to stderr)");
8956
9232
  console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
8957
- console.log(" 3 daemon not reachable (connection refused / timeout)");
9233
+ console.log(" 3 environmental \u2014 daemon unreachable, or one of ports 8080 / 4318 / 6328");
9234
+ console.log(" is held by another process when `neat <path>` tries to spawn the daemon");
8958
9235
  console.log("");
8959
9236
  console.log("environment:");
8960
9237
  console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
@@ -9152,7 +9429,7 @@ async function buildPatchSections(services, project) {
9152
9429
  const installer = await pickInstaller(svc.dir);
9153
9430
  if (!installer) continue;
9154
9431
  const plan3 = await installer.plan(svc.dir, { project });
9155
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
9432
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
9156
9433
  sections.push({ installer: installer.name, plan: plan3 });
9157
9434
  }
9158
9435
  return sections;
@@ -9196,13 +9473,15 @@ async function runInit(opts) {
9196
9473
  written.push(gitignoreResult.file);
9197
9474
  }
9198
9475
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9476
+ let currentProjectName = opts.project;
9199
9477
  try {
9200
- await addProject({
9478
+ const entry2 = await addProject({
9201
9479
  name: opts.project,
9202
9480
  path: opts.scanPath,
9203
9481
  languages,
9204
9482
  status: "active"
9205
9483
  });
9484
+ currentProjectName = entry2.name;
9206
9485
  } catch (err) {
9207
9486
  if (err instanceof ProjectNameCollisionError) {
9208
9487
  console.error(`neat init: ${err.message}`);
@@ -9211,11 +9490,27 @@ async function runInit(opts) {
9211
9490
  }
9212
9491
  throw err;
9213
9492
  }
9493
+ const siblings = await listProjects();
9494
+ const paused = [];
9495
+ for (const p of siblings) {
9496
+ if (p.name !== currentProjectName && p.status === "active") {
9497
+ await setStatus(p.name, "paused");
9498
+ paused.push(p.name);
9499
+ }
9500
+ }
9501
+ if (paused.length > 0) {
9502
+ const plural = paused.length === 1 ? "" : "s";
9503
+ console.log(
9504
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
9505
+ );
9506
+ }
9214
9507
  if (!opts.noInstall) {
9215
9508
  if (opts.apply) {
9216
9509
  let instrumented = 0;
9217
9510
  let alreadyInstrumented = 0;
9218
9511
  let libOnly = 0;
9512
+ let browserBundle = 0;
9513
+ let reactNative = 0;
9219
9514
  for (const section of sections) {
9220
9515
  const installer = INSTALLERS.find((i) => i.name === section.installer);
9221
9516
  if (!installer) continue;
@@ -9227,13 +9522,24 @@ async function runInit(opts) {
9227
9522
  alreadyInstrumented++;
9228
9523
  } else if (outcome.outcome === "lib-only") {
9229
9524
  libOnly++;
9525
+ } else if (outcome.outcome === "browser-bundle") {
9526
+ browserBundle++;
9527
+ console.log(`skipping ${section.plan.serviceDir}: browser bundle; browser-OTel support lands in a future release.`);
9528
+ } else if (outcome.outcome === "react-native") {
9529
+ reactNative++;
9530
+ console.log(`skipping ${section.plan.serviceDir}: React Native target; browser-OTel support lands in a future release.`);
9230
9531
  }
9231
9532
  }
9232
9533
  if (sections.length > 0) {
9233
9534
  console.log("");
9234
- console.log(
9235
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
9236
- );
9535
+ const parts = [
9536
+ `instrumented ${instrumented}`,
9537
+ `already-instrumented ${alreadyInstrumented}`,
9538
+ `lib-only ${libOnly}`
9539
+ ];
9540
+ if (browserBundle > 0) parts.push(`browser-bundle ${browserBundle}`);
9541
+ if (reactNative > 0) parts.push(`react-native ${reactNative}`);
9542
+ console.log(`apply: ${parts.join(", ")}`);
9237
9543
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
9238
9544
  }
9239
9545
  } else {