hobots 2.2.0 → 2.4.0

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/README.md CHANGED
@@ -44,7 +44,7 @@ Hobots.init({
44
44
  clientSecret: 'hb_<64 hex>',
45
45
  instanceId: 'vm-cliente-01',
46
46
  heartbeat: true, // telemetria: "estou vivo" a cada 30s
47
- tasks: { pollIntervalMs: 5_000 }, // execução sob demanda
47
+ tasks: { pollIntervalMs: 2_000 }, // execução sob demanda
48
48
  });
49
49
 
50
50
  Hobots.register('processar-nfe', async (params, ctx) => {
@@ -99,6 +99,7 @@ Hobots.init({
99
99
  | `tracesSampleRate` | `number` | `1` | amostragem de transactions (0..1) |
100
100
  | `maxBreadcrumbs` | `number` | `100` | tamanho do buffer de breadcrumbs |
101
101
  | `captureUnhandled` | `boolean` | `true` | handlers globais de erro (`uncaughtException`/`unhandledRejection`) |
102
+ | `machineMetrics` | `boolean` | `true` | CPU/memória/disco no heartbeat, no poll e em `contexts.machine` dos eventos |
102
103
  | `heartbeat` | `boolean \| HeartbeatOptions` | `false` | heartbeat periódico (`true` usa defaults) |
103
104
  | `tasks` | `boolean \| TasksOptions` | `false` | habilita a execução sob demanda (`true` usa defaults) — veja [Tasks](#tasks-execução-sob-demanda) |
104
105
  | `beforeSend` | `(event) => event \| null` | — | edita/descarta evento (retorne `null` para descartar) |
@@ -196,6 +197,17 @@ Hobots.init({
196
197
 
197
198
  O heartbeat é **essencial** para agentes só de telemetria; com `tasks: true` ele é **opcional**, porque o poll de tasks já marca a presença do agente no painel. Conceito: [Heartbeat](./docs/concepts.md#heartbeat).
198
199
 
200
+ ### Recursos da máquina (CPU, memória, disco)
201
+
202
+ Ligado por padrão em Node (`machineMetrics: true`), o SDK coleta CPU do sistema, memória, disco do volume do bot e RSS/CPU do próprio processo — **sem dependência nova** e sem timer próprio: a leitura pega carona em cada heartbeat e em cada poll de tasks (série da instância em **Instâncias → recursos da máquina**) e vai como `contexts.machine` em cada evento (card **"máquina no momento do erro"** no detalhe da issue). Assim dá para responder se o ambiente tem recurso suficiente e se um erro coincidiu com CPU/memória no limite.
203
+
204
+ ```js
205
+ Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
206
+ Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
207
+ ```
208
+
209
+ O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./docs/protocol.md#machinesnapshot).
210
+
199
211
  ### Flush e close
200
212
 
201
213
  ```js
@@ -241,7 +253,7 @@ process.on('SIGINT', () => {
241
253
 
242
254
  | Opção (`TasksOptions`) | Tipo | Default | Descrição |
243
255
  | --- | --- | --- | --- |
244
- | `pollIntervalMs` | `number` | `5000` | intervalo entre polls sem trabalho (mínimo `1000`) |
256
+ | `pollIntervalMs` | `number` | `2000` | intervalo entre polls sem trabalho (mínimo `1000`) |
245
257
  | `concurrency` | `number` | `1` | runs simultâneas (clamp 1..5) |
246
258
  | `taskTimeoutMs` | `number` | — | timeout client-side por run (não mata o handler; servidor é o backstop) |
247
259
  | `logFlushIntervalMs` | `number` | `2000` | intervalo do envio incremental de logs |
package/dist/index.cjs CHANGED
@@ -161,7 +161,7 @@ function scrubSecrets(text) {
161
161
 
162
162
  // src/utils.ts
163
163
  var SDK_NAME = "hobots";
164
- var SDK_VERSION = true ? "2.2.0" : "0.0.0-dev";
164
+ var SDK_VERSION = true ? "2.4.0" : "0.0.0-dev";
165
165
  function isNode() {
166
166
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
167
167
  }
@@ -454,6 +454,7 @@ var Client = class {
454
454
  #options;
455
455
  #config;
456
456
  transport;
457
+ machine;
457
458
  constructor(options, config = resolveConfig(options)) {
458
459
  this.#options = options;
459
460
  this.debug = !!options.debug;
@@ -473,6 +474,10 @@ var Client = class {
473
474
  get config() {
474
475
  return this.#config;
475
476
  }
477
+ /** Sampler de recursos da máquina — cada evento passa a levar `contexts.machine`. */
478
+ attachMachineSampler(sampler) {
479
+ this.machine = sampler;
480
+ }
476
481
  /** Serialização explícita nunca inclui o secret. */
477
482
  toJSON() {
478
483
  return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
@@ -504,15 +509,17 @@ var Client = class {
504
509
  prepareEvent(event, hint, scope2) {
505
510
  const tags = { ...this.options.tags, ...scope2.tags, ...event.tags, ...hint?.tags };
506
511
  const extra = { ...scope2.extra, ...event.extra, ...hint?.extra };
512
+ const sampled = this.machine?.sample();
513
+ const machine = sampled ? { machine: sampled } : {};
507
514
  return {
508
515
  ...event,
509
516
  type: hint?.type ?? event.type ?? scope2.type ?? "error",
510
517
  release: event.release ?? this.options.release,
511
- environment: event.environment ?? this.options.environment ?? "production",
518
+ environment: this.resolveEnvironment(event.environment),
512
519
  tags: Object.keys(tags).length > 0 ? tags : void 0,
513
520
  extra: Object.keys(extra).length > 0 ? extra : void 0,
514
521
  user: event.user ?? scope2.user,
515
- contexts: { ...runtimeContexts(), ...scope2.contexts, ...event.contexts },
522
+ contexts: { ...runtimeContexts(), ...machine, ...scope2.contexts, ...event.contexts },
516
523
  breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
517
524
  fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
518
525
  };
@@ -523,7 +530,7 @@ var Client = class {
523
530
  payload: {
524
531
  ...payload,
525
532
  release: payload.release ?? this.options.release,
526
- environment: payload.environment ?? this.options.environment ?? "production",
533
+ environment: this.resolveEnvironment(payload.environment),
527
534
  tags: { ...this.options.tags, ...payload.tags }
528
535
  }
529
536
  });
@@ -531,9 +538,22 @@ var Client = class {
531
538
  sendHeartbeat(payload) {
532
539
  this.transport.enqueue({
533
540
  type: "heartbeat",
534
- payload: { ...payload, release: payload.release ?? this.options.release }
541
+ payload: {
542
+ ...payload,
543
+ release: payload.release ?? this.options.release,
544
+ // o ambiente viaja no beat: é assim que a instância se classifica no app
545
+ environment: this.resolveEnvironment(payload.environment)
546
+ }
535
547
  });
536
548
  }
549
+ /**
550
+ * Ambiente do item: o dele, senão o do init(), senão produção. Centralizado
551
+ * porque evento, transaction e heartbeat precisam responder igual — o servidor
552
+ * normaliza apelidos legados, então aqui basta repassar.
553
+ */
554
+ resolveEnvironment(value) {
555
+ return value ?? this.options.environment ?? "production";
556
+ }
537
557
  flush(timeoutMs) {
538
558
  return this.transport.flush(timeoutMs);
539
559
  }
@@ -547,8 +567,9 @@ var DEFAULT_INTERVAL_MS = 3e4;
547
567
  var MIN_REPORTED_INTERVAL_MS = 1e3;
548
568
  var MAX_REPORTED_INTERVAL_MS = 9e5;
549
569
  var Heartbeat = class {
550
- constructor(client2, options = {}, instanceId) {
570
+ constructor(client2, options = {}, instanceId, sampler) {
551
571
  this.client = client2;
572
+ this.sampler = sampler;
552
573
  this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
553
574
  this.instanceId = instanceId;
554
575
  this.name = options.name ?? instanceId;
@@ -587,7 +608,9 @@ var Heartbeat = class {
587
608
  name: this.name,
588
609
  interval_ms: Math.min(Math.max(this.intervalMs, MIN_REPORTED_INTERVAL_MS), MAX_REPORTED_INTERVAL_MS),
589
610
  timestamp: nowISO(),
590
- metadata: this.metadata
611
+ metadata: this.metadata,
612
+ // recursos da máquina a cada beat — a série da instância no app
613
+ machine: this.sampler?.sample()
591
614
  });
592
615
  }
593
616
  };
@@ -618,50 +641,184 @@ function hasInstalledGlobalHandlers() {
618
641
  return installed;
619
642
  }
620
643
  function installGlobalHandlers(client2, getScope) {
621
- if (installed) return;
644
+ if (installed) return () => void 0;
622
645
  installed = true;
623
646
  if (isNode()) {
624
- process.on(
625
- "uncaughtException",
626
- markInternal((error) => {
627
- client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
628
- void client2.flush(2e3).then(async () => {
629
- if (externalListenerCount("uncaughtException") === 0) {
630
- await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
631
- console.error(error);
632
- process.exit(1);
633
- }
634
- });
635
- })
636
- );
637
- process.on(
638
- "unhandledRejection",
639
- markInternal((reason) => {
640
- client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
641
- void client2.flush(2e3);
642
- })
643
- );
644
- process.on(
645
- "beforeExit",
646
- markInternal(() => {
647
- void client2.flush(2e3);
648
- })
649
- );
650
- return;
647
+ const onUncaught = markInternal((error) => {
648
+ client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
649
+ void client2.flush(2e3).then(async () => {
650
+ if (externalListenerCount("uncaughtException") === 0) {
651
+ await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
652
+ console.error(error);
653
+ process.exit(1);
654
+ }
655
+ });
656
+ });
657
+ const onRejection = markInternal((reason) => {
658
+ client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
659
+ void client2.flush(2e3);
660
+ });
661
+ const onBeforeExit = markInternal(() => {
662
+ void client2.flush(2e3);
663
+ });
664
+ process.on("uncaughtException", onUncaught);
665
+ process.on("unhandledRejection", onRejection);
666
+ process.on("beforeExit", onBeforeExit);
667
+ return () => {
668
+ process.off("uncaughtException", onUncaught);
669
+ process.off("unhandledRejection", onRejection);
670
+ process.off("beforeExit", onBeforeExit);
671
+ installed = false;
672
+ };
651
673
  }
652
674
  if (typeof window !== "undefined") {
653
- window.addEventListener("error", (event) => {
675
+ const onError = (event) => {
654
676
  client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
655
- });
656
- window.addEventListener("unhandledrejection", (event) => {
677
+ };
678
+ const onRejection = (event) => {
657
679
  client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
658
- });
659
- window.addEventListener("pagehide", () => {
680
+ };
681
+ const onPageHide = () => {
660
682
  void client2.flush(1e3);
661
- });
683
+ };
684
+ window.addEventListener("error", onError);
685
+ window.addEventListener("unhandledrejection", onRejection);
686
+ window.addEventListener("pagehide", onPageHide);
687
+ return () => {
688
+ window.removeEventListener("error", onError);
689
+ window.removeEventListener("unhandledrejection", onRejection);
690
+ window.removeEventListener("pagehide", onPageHide);
691
+ installed = false;
692
+ };
662
693
  }
694
+ return () => {
695
+ installed = false;
696
+ };
663
697
  }
664
698
 
699
+ // src/machine.ts
700
+ var MIN_CPU_WINDOW_MS = 1e3;
701
+ var round1 = (value) => Math.round(value * 10) / 10;
702
+ var clampPercent = (value) => Math.min(100, Math.max(0, value));
703
+ var MachineSampler = class {
704
+ constructor(instanceId, logger) {
705
+ this.instanceId = instanceId;
706
+ this.logger = logger;
707
+ }
708
+ os;
709
+ fs;
710
+ ready = false;
711
+ prevCpu;
712
+ prevProcessCpu;
713
+ prevAt = 0;
714
+ lastCpuPercent = null;
715
+ lastProcessCpuPercent = null;
716
+ /** Carrega os módulos e tira a leitura-base de CPU (a primeira amostra ainda sai com cpu_percent null). */
717
+ async start() {
718
+ if (!isNode()) return;
719
+ try {
720
+ this.os = await import("os");
721
+ this.fs = await import("fs");
722
+ this.prevCpu = this.cpuTotals();
723
+ this.prevProcessCpu = process.cpuUsage();
724
+ this.prevAt = Date.now();
725
+ this.ready = true;
726
+ } catch (error) {
727
+ this.logger.debug("m\xE9tricas de m\xE1quina indispon\xEDveis neste runtime", error);
728
+ }
729
+ }
730
+ sample() {
731
+ if (!this.ready || !this.os) return void 0;
732
+ try {
733
+ const os = this.os;
734
+ const cpuCount = Math.max(1, os.cpus().length);
735
+ this.updateCpu(cpuCount);
736
+ const snapshot = {
737
+ sampled_at: nowISO(),
738
+ instance_id: this.instanceId,
739
+ hostname: os.hostname(),
740
+ os: process.platform,
741
+ arch: process.arch,
742
+ cpu_count: cpuCount,
743
+ cpu_percent: this.lastCpuPercent,
744
+ process: {
745
+ pid: process.pid,
746
+ uptime_s: Math.round(process.uptime()),
747
+ cpu_percent: this.lastProcessCpuPercent
748
+ },
749
+ source: "stdlib"
750
+ };
751
+ if (process.platform !== "win32") {
752
+ const load = os.loadavg()[0];
753
+ if (typeof load === "number" && Number.isFinite(load)) snapshot.load_1 = round1(load);
754
+ }
755
+ const memTotal = os.totalmem();
756
+ const memFree = os.freemem();
757
+ if (memTotal > 0) {
758
+ snapshot.mem_total_bytes = memTotal;
759
+ snapshot.mem_available_bytes = memFree;
760
+ snapshot.mem_percent = round1(clampPercent((memTotal - memFree) / memTotal * 100));
761
+ }
762
+ const disk = this.diskUsage();
763
+ if (disk) Object.assign(snapshot, disk);
764
+ const mem = process.memoryUsage();
765
+ snapshot.process.rss_bytes = mem.rss;
766
+ snapshot.process.heap_used_bytes = mem.heapUsed;
767
+ return snapshot;
768
+ } catch (error) {
769
+ this.logger.debug("falha ao coletar m\xE9tricas de m\xE1quina", error);
770
+ return void 0;
771
+ }
772
+ }
773
+ cpuTotals() {
774
+ let busy = 0;
775
+ let total = 0;
776
+ for (const cpu of this.os.cpus()) {
777
+ const t = cpu.times;
778
+ const cpuBusy = t.user + t.nice + t.sys + t.irq;
779
+ busy += cpuBusy;
780
+ total += cpuBusy + t.idle;
781
+ }
782
+ return { busy, total };
783
+ }
784
+ /** Média desde a leitura anterior; leituras com menos de 1s de janela reutilizam a última média. */
785
+ updateCpu(cpuCount) {
786
+ const now = Date.now();
787
+ const elapsedMs = now - this.prevAt;
788
+ if (elapsedMs < MIN_CPU_WINDOW_MS) return;
789
+ const cpu = this.cpuTotals();
790
+ if (this.prevCpu) {
791
+ const total = cpu.total - this.prevCpu.total;
792
+ if (total > 0) this.lastCpuPercent = round1(clampPercent((cpu.busy - this.prevCpu.busy) / total * 100));
793
+ }
794
+ this.prevCpu = cpu;
795
+ const usage = process.cpuUsage(this.prevProcessCpu);
796
+ const usedMicros = usage.user + usage.system;
797
+ this.lastProcessCpuPercent = round1(clampPercent(usedMicros / (elapsedMs * 1e3 * cpuCount) * 100));
798
+ this.prevProcessCpu = process.cpuUsage();
799
+ this.prevAt = now;
800
+ }
801
+ diskUsage() {
802
+ const statfs = this.fs?.statfsSync;
803
+ if (typeof statfs !== "function") return void 0;
804
+ try {
805
+ const path = process.cwd();
806
+ const stat = statfs(path);
807
+ const total = stat.bsize * stat.blocks;
808
+ const free = stat.bsize * stat.bavail;
809
+ if (total <= 0) return void 0;
810
+ return {
811
+ disk_path: path,
812
+ disk_total_bytes: total,
813
+ disk_free_bytes: free,
814
+ disk_percent: round1(clampPercent((total - free) / total * 100))
815
+ };
816
+ } catch {
817
+ return void 0;
818
+ }
819
+ }
820
+ };
821
+
665
822
  // src/scope.ts
666
823
  var Scope = class _Scope {
667
824
  tags = {};
@@ -1845,7 +2002,7 @@ function installShutdownHandlers(agent2) {
1845
2002
  }
1846
2003
 
1847
2004
  // src/tasks/agent.ts
1848
- var DEFAULT_POLL_INTERVAL_MS = 5e3;
2005
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
1849
2006
  var MIN_POLL_INTERVAL_MS = 1e3;
1850
2007
  var MAX_CONCURRENCY = 5;
1851
2008
  var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
@@ -1976,11 +2133,17 @@ var TaskAgent = class {
1976
2133
  instance_id: this.instanceId,
1977
2134
  version: SDK_VERSION,
1978
2135
  host: hostname(),
2136
+ // ambiente da instância: em agentes poll-only (sem heartbeat) este é o
2137
+ // único canal por onde o servidor descobre onde o robô está rodando
2138
+ environment: this.options.environment,
1979
2139
  tasks: [...this.registry.keys()],
1980
2140
  // o servidor calcula a tolerância de offline (3×) a partir disto em
1981
2141
  // agentes poll-only — sem o campo, um poll lento flaparia offline/online.
1982
2142
  // Clampado à faixa aceita pelo schema do poll (1s..15min).
1983
- poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5)
2143
+ poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
2144
+ // recursos da máquina — alimenta a série da instância em agentes sem heartbeat
2145
+ // (o servidor grava no máx. 1 ponto/30s por instância)
2146
+ machine: this.options.machineSampler?.sample()
1984
2147
  },
1985
2148
  max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
1986
2149
  tasks: [...this.registry.keys()]
@@ -2128,7 +2291,9 @@ var DEFAULT_MAX_BREADCRUMBS = 100;
2128
2291
  var client;
2129
2292
  var scope = new Scope();
2130
2293
  var heartbeat;
2294
+ var machineSampler;
2131
2295
  var agent;
2296
+ var uninstallGlobalHandlers;
2132
2297
  var registry = /* @__PURE__ */ new Map();
2133
2298
  var warnedTasksDisabled = false;
2134
2299
  function init(options) {
@@ -2141,21 +2306,26 @@ function init(options) {
2141
2306
  void agent.stop(0);
2142
2307
  agent = void 0;
2143
2308
  }
2309
+ uninstallGlobalHandlers?.();
2310
+ uninstallGlobalHandlers = void 0;
2144
2311
  warnedTasksDisabled = false;
2145
2312
  const config = resolveConfig(options);
2146
2313
  client = new Client(options, config);
2314
+ machineSampler = options.machineMetrics === false ? void 0 : new MachineSampler(options.instanceId, client.logger);
2315
+ void machineSampler?.start();
2316
+ client.attachMachineSampler(machineSampler);
2147
2317
  if (options.captureUnhandled !== false) {
2148
- installGlobalHandlers(client, () => scope);
2318
+ uninstallGlobalHandlers = installGlobalHandlers(client, () => scope);
2149
2319
  }
2150
2320
  if (options.heartbeat) {
2151
2321
  const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
2152
- heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
2322
+ heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId, machineSampler);
2153
2323
  heartbeat.start();
2154
2324
  }
2155
2325
  if (options.tasks) {
2156
2326
  const tasksOptions = options.tasks === true ? {} : options.tasks;
2157
2327
  agent = new TaskAgent(
2158
- { ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client },
2328
+ { ...tasksOptions, config, instanceId: options.instanceId, environment: options.environment, debug: options.debug, client, machineSampler },
2159
2329
  registry
2160
2330
  );
2161
2331
  }