hobots 2.0.1 → 2.3.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 +33 -2
- package/dist/index.cjs +278 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -3
- package/dist/index.d.ts +60 -3
- package/dist/index.js +278 -44
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +69 -1
- package/dist/protocol.d.ts +69 -1
- package/docs/api-reference.md +13 -0
- package/docs/protocol.md +25 -1
- package/docs/tasks.md +38 -0
- package/docs/telemetry.md +11 -0
- package/package.json +5 -3
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:
|
|
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` | `
|
|
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 |
|
|
@@ -261,6 +273,7 @@ interface RunContext {
|
|
|
261
273
|
task: string;
|
|
262
274
|
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
263
275
|
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
276
|
+
progress: RunProgressReporter; // ctx.progress.total/advance/set — barra + tempo restante no app
|
|
264
277
|
tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
|
|
265
278
|
signal: AbortSignal;
|
|
266
279
|
throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
|
|
@@ -271,6 +284,7 @@ interface RunContext {
|
|
|
271
284
|
- **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
|
|
272
285
|
- **`ctx.log.<level>(message)`** — níveis `debug`, `info`, `warn`, `error` e `success`; logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na solicitação quase em tempo real; também impressos no terminal local, coloridos por nível.
|
|
273
286
|
- **`ctx.items`** — registra os **itens que a execução processou/gerou** (a entrega do bot: notas fiscais baixadas, boletos emitidos…), cada um com status `succeeded`, `failed` (falha técnica) ou `occurrence` (ocorrência de negócio, ex.: "nota nº 10 não encontrada") e um `payload` de atributos livres. Aparecem na aba **itens** da solicitação, com totalizadores; as colunas exibidas são configuradas por formulário no app. Guia: [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
287
|
+
- **`ctx.progress`** — **opcional**: declare quantos itens a run vai processar (`ctx.progress.total(n)`) e avance a cada item (`ctx.progress.advance()`); o app mostra barra, percentual e tempo restante estimado na solicitação. Independente de `ctx.items`. Guia: [Progresso](./docs/tasks.md#progresso-da-execução-ctxprogress).
|
|
274
288
|
- **`ctx.tx`** — a transaction de performance desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild(op, description)` para medir as etapas — elas viram o waterfall da execução no app. `startTransaction()` chamado dentro do handler retorna essa mesma transaction. Conceito: [ctx.tx](./docs/concepts.md#transaction-de-run-ctxtx).
|
|
275
289
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
276
290
|
- **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
|
|
@@ -299,6 +313,23 @@ Hobots.register('baixar-notas', async (params, ctx) => {
|
|
|
299
313
|
|
|
300
314
|
Chamadas síncronas e bufferizadas (lotes de até 500; dreno antes do reporte final). O `id` opcional é a identidade de negócio do item na run — repetido, é ignorado pelo servidor (registro único, retry idempotente). Limites: 10.000 itens/run, payload 16 KB, mensagem 2.000 caracteres. Detalhes em [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
301
315
|
|
|
316
|
+
### Progresso (`ctx.progress`)
|
|
317
|
+
|
|
318
|
+
Quando a run percorre uma lista conhecida, declare o total e avance a cada item — a solicitação no app ganha barra de progresso, percentual (`10/100 · 10%`) e **tempo restante estimado**:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
Hobots.register('baixar-notas', async (params, ctx) => {
|
|
322
|
+
const notas = await listarNotas(params);
|
|
323
|
+
ctx.progress.total(notas.length);
|
|
324
|
+
for (const nota of notas) {
|
|
325
|
+
await baixar(nota);
|
|
326
|
+
ctx.progress.advance();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Opcional e independente de `ctx.items` (use um, outro ou ambos). Sem `total()` nada é enviado. Chamadas síncronas e baratas: o snapshot pega carona nos PATCHes de log e, no pior caso, sai 1 vez por segundo; o PATCH terminal sempre leva o valor final. `ctx.progress.set(current, total?)` define um valor absoluto; `ctx.progress.current` e `ctx.progress.snapshot` leem o estado. Detalhes em [Progresso da execução](./docs/tasks.md#progresso-da-execução-ctxprogress).
|
|
332
|
+
|
|
302
333
|
### Anexos (screenshots)
|
|
303
334
|
|
|
304
335
|
Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura (`Hobots.captureException` e `Hobots.captureMessage`) aceitam um `attachment` no hint: dentro de uma execução de task, a imagem vira anexo da linha de log da run que espelha a captura — no app ela aparece como miniatura logo abaixo dessa linha, clicável para ampliar.
|
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.0
|
|
164
|
+
var SDK_VERSION = true ? "2.3.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,6 +509,8 @@ 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",
|
|
@@ -512,7 +519,7 @@ var Client = class {
|
|
|
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
|
};
|
|
@@ -547,8 +554,9 @@ var DEFAULT_INTERVAL_MS = 3e4;
|
|
|
547
554
|
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
548
555
|
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
549
556
|
var Heartbeat = class {
|
|
550
|
-
constructor(client2, options = {}, instanceId) {
|
|
557
|
+
constructor(client2, options = {}, instanceId, sampler) {
|
|
551
558
|
this.client = client2;
|
|
559
|
+
this.sampler = sampler;
|
|
552
560
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
553
561
|
this.instanceId = instanceId;
|
|
554
562
|
this.name = options.name ?? instanceId;
|
|
@@ -587,7 +595,9 @@ var Heartbeat = class {
|
|
|
587
595
|
name: this.name,
|
|
588
596
|
interval_ms: Math.min(Math.max(this.intervalMs, MIN_REPORTED_INTERVAL_MS), MAX_REPORTED_INTERVAL_MS),
|
|
589
597
|
timestamp: nowISO(),
|
|
590
|
-
metadata: this.metadata
|
|
598
|
+
metadata: this.metadata,
|
|
599
|
+
// recursos da máquina a cada beat — a série da instância no app
|
|
600
|
+
machine: this.sampler?.sample()
|
|
591
601
|
});
|
|
592
602
|
}
|
|
593
603
|
};
|
|
@@ -618,50 +628,184 @@ function hasInstalledGlobalHandlers() {
|
|
|
618
628
|
return installed;
|
|
619
629
|
}
|
|
620
630
|
function installGlobalHandlers(client2, getScope) {
|
|
621
|
-
if (installed) return;
|
|
631
|
+
if (installed) return () => void 0;
|
|
622
632
|
installed = true;
|
|
623
633
|
if (isNode()) {
|
|
624
|
-
|
|
625
|
-
"uncaughtException",
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return;
|
|
634
|
+
const onUncaught = markInternal((error) => {
|
|
635
|
+
client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
|
|
636
|
+
void client2.flush(2e3).then(async () => {
|
|
637
|
+
if (externalListenerCount("uncaughtException") === 0) {
|
|
638
|
+
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
639
|
+
console.error(error);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
const onRejection = markInternal((reason) => {
|
|
645
|
+
client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
|
|
646
|
+
void client2.flush(2e3);
|
|
647
|
+
});
|
|
648
|
+
const onBeforeExit = markInternal(() => {
|
|
649
|
+
void client2.flush(2e3);
|
|
650
|
+
});
|
|
651
|
+
process.on("uncaughtException", onUncaught);
|
|
652
|
+
process.on("unhandledRejection", onRejection);
|
|
653
|
+
process.on("beforeExit", onBeforeExit);
|
|
654
|
+
return () => {
|
|
655
|
+
process.off("uncaughtException", onUncaught);
|
|
656
|
+
process.off("unhandledRejection", onRejection);
|
|
657
|
+
process.off("beforeExit", onBeforeExit);
|
|
658
|
+
installed = false;
|
|
659
|
+
};
|
|
651
660
|
}
|
|
652
661
|
if (typeof window !== "undefined") {
|
|
653
|
-
|
|
662
|
+
const onError = (event) => {
|
|
654
663
|
client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
|
|
655
|
-
}
|
|
656
|
-
|
|
664
|
+
};
|
|
665
|
+
const onRejection = (event) => {
|
|
657
666
|
client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
|
|
658
|
-
}
|
|
659
|
-
|
|
667
|
+
};
|
|
668
|
+
const onPageHide = () => {
|
|
660
669
|
void client2.flush(1e3);
|
|
661
|
-
}
|
|
670
|
+
};
|
|
671
|
+
window.addEventListener("error", onError);
|
|
672
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
673
|
+
window.addEventListener("pagehide", onPageHide);
|
|
674
|
+
return () => {
|
|
675
|
+
window.removeEventListener("error", onError);
|
|
676
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
677
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
678
|
+
installed = false;
|
|
679
|
+
};
|
|
662
680
|
}
|
|
681
|
+
return () => {
|
|
682
|
+
installed = false;
|
|
683
|
+
};
|
|
663
684
|
}
|
|
664
685
|
|
|
686
|
+
// src/machine.ts
|
|
687
|
+
var MIN_CPU_WINDOW_MS = 1e3;
|
|
688
|
+
var round1 = (value) => Math.round(value * 10) / 10;
|
|
689
|
+
var clampPercent = (value) => Math.min(100, Math.max(0, value));
|
|
690
|
+
var MachineSampler = class {
|
|
691
|
+
constructor(instanceId, logger) {
|
|
692
|
+
this.instanceId = instanceId;
|
|
693
|
+
this.logger = logger;
|
|
694
|
+
}
|
|
695
|
+
os;
|
|
696
|
+
fs;
|
|
697
|
+
ready = false;
|
|
698
|
+
prevCpu;
|
|
699
|
+
prevProcessCpu;
|
|
700
|
+
prevAt = 0;
|
|
701
|
+
lastCpuPercent = null;
|
|
702
|
+
lastProcessCpuPercent = null;
|
|
703
|
+
/** Carrega os módulos e tira a leitura-base de CPU (a primeira amostra ainda sai com cpu_percent null). */
|
|
704
|
+
async start() {
|
|
705
|
+
if (!isNode()) return;
|
|
706
|
+
try {
|
|
707
|
+
this.os = await import("os");
|
|
708
|
+
this.fs = await import("fs");
|
|
709
|
+
this.prevCpu = this.cpuTotals();
|
|
710
|
+
this.prevProcessCpu = process.cpuUsage();
|
|
711
|
+
this.prevAt = Date.now();
|
|
712
|
+
this.ready = true;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
this.logger.debug("m\xE9tricas de m\xE1quina indispon\xEDveis neste runtime", error);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
sample() {
|
|
718
|
+
if (!this.ready || !this.os) return void 0;
|
|
719
|
+
try {
|
|
720
|
+
const os = this.os;
|
|
721
|
+
const cpuCount = Math.max(1, os.cpus().length);
|
|
722
|
+
this.updateCpu(cpuCount);
|
|
723
|
+
const snapshot = {
|
|
724
|
+
sampled_at: nowISO(),
|
|
725
|
+
instance_id: this.instanceId,
|
|
726
|
+
hostname: os.hostname(),
|
|
727
|
+
os: process.platform,
|
|
728
|
+
arch: process.arch,
|
|
729
|
+
cpu_count: cpuCount,
|
|
730
|
+
cpu_percent: this.lastCpuPercent,
|
|
731
|
+
process: {
|
|
732
|
+
pid: process.pid,
|
|
733
|
+
uptime_s: Math.round(process.uptime()),
|
|
734
|
+
cpu_percent: this.lastProcessCpuPercent
|
|
735
|
+
},
|
|
736
|
+
source: "stdlib"
|
|
737
|
+
};
|
|
738
|
+
if (process.platform !== "win32") {
|
|
739
|
+
const load = os.loadavg()[0];
|
|
740
|
+
if (typeof load === "number" && Number.isFinite(load)) snapshot.load_1 = round1(load);
|
|
741
|
+
}
|
|
742
|
+
const memTotal = os.totalmem();
|
|
743
|
+
const memFree = os.freemem();
|
|
744
|
+
if (memTotal > 0) {
|
|
745
|
+
snapshot.mem_total_bytes = memTotal;
|
|
746
|
+
snapshot.mem_available_bytes = memFree;
|
|
747
|
+
snapshot.mem_percent = round1(clampPercent((memTotal - memFree) / memTotal * 100));
|
|
748
|
+
}
|
|
749
|
+
const disk = this.diskUsage();
|
|
750
|
+
if (disk) Object.assign(snapshot, disk);
|
|
751
|
+
const mem = process.memoryUsage();
|
|
752
|
+
snapshot.process.rss_bytes = mem.rss;
|
|
753
|
+
snapshot.process.heap_used_bytes = mem.heapUsed;
|
|
754
|
+
return snapshot;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
this.logger.debug("falha ao coletar m\xE9tricas de m\xE1quina", error);
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
cpuTotals() {
|
|
761
|
+
let busy = 0;
|
|
762
|
+
let total = 0;
|
|
763
|
+
for (const cpu of this.os.cpus()) {
|
|
764
|
+
const t = cpu.times;
|
|
765
|
+
const cpuBusy = t.user + t.nice + t.sys + t.irq;
|
|
766
|
+
busy += cpuBusy;
|
|
767
|
+
total += cpuBusy + t.idle;
|
|
768
|
+
}
|
|
769
|
+
return { busy, total };
|
|
770
|
+
}
|
|
771
|
+
/** Média desde a leitura anterior; leituras com menos de 1s de janela reutilizam a última média. */
|
|
772
|
+
updateCpu(cpuCount) {
|
|
773
|
+
const now = Date.now();
|
|
774
|
+
const elapsedMs = now - this.prevAt;
|
|
775
|
+
if (elapsedMs < MIN_CPU_WINDOW_MS) return;
|
|
776
|
+
const cpu = this.cpuTotals();
|
|
777
|
+
if (this.prevCpu) {
|
|
778
|
+
const total = cpu.total - this.prevCpu.total;
|
|
779
|
+
if (total > 0) this.lastCpuPercent = round1(clampPercent((cpu.busy - this.prevCpu.busy) / total * 100));
|
|
780
|
+
}
|
|
781
|
+
this.prevCpu = cpu;
|
|
782
|
+
const usage = process.cpuUsage(this.prevProcessCpu);
|
|
783
|
+
const usedMicros = usage.user + usage.system;
|
|
784
|
+
this.lastProcessCpuPercent = round1(clampPercent(usedMicros / (elapsedMs * 1e3 * cpuCount) * 100));
|
|
785
|
+
this.prevProcessCpu = process.cpuUsage();
|
|
786
|
+
this.prevAt = now;
|
|
787
|
+
}
|
|
788
|
+
diskUsage() {
|
|
789
|
+
const statfs = this.fs?.statfsSync;
|
|
790
|
+
if (typeof statfs !== "function") return void 0;
|
|
791
|
+
try {
|
|
792
|
+
const path = process.cwd();
|
|
793
|
+
const stat = statfs(path);
|
|
794
|
+
const total = stat.bsize * stat.blocks;
|
|
795
|
+
const free = stat.bsize * stat.bavail;
|
|
796
|
+
if (total <= 0) return void 0;
|
|
797
|
+
return {
|
|
798
|
+
disk_path: path,
|
|
799
|
+
disk_total_bytes: total,
|
|
800
|
+
disk_free_bytes: free,
|
|
801
|
+
disk_percent: round1(clampPercent((total - free) / total * 100))
|
|
802
|
+
};
|
|
803
|
+
} catch {
|
|
804
|
+
return void 0;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
665
809
|
// src/scope.ts
|
|
666
810
|
var Scope = class _Scope {
|
|
667
811
|
tags = {};
|
|
@@ -1201,6 +1345,7 @@ var MAX_ITEM_MESSAGE_CHARS = 2e3;
|
|
|
1201
1345
|
var MAX_ITEM_PAYLOAD_BYTES = 16384;
|
|
1202
1346
|
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1203
1347
|
var ITEMS_DRAIN_MS = 15e3;
|
|
1348
|
+
var PROGRESS_FLUSH_MS = 1e3;
|
|
1204
1349
|
function scrubPayloadValue(value, depth) {
|
|
1205
1350
|
if (typeof value === "string") return scrubSecrets(value);
|
|
1206
1351
|
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
@@ -1235,12 +1380,28 @@ var RunReporter = class {
|
|
|
1235
1380
|
failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
|
|
1236
1381
|
occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
|
|
1237
1382
|
};
|
|
1383
|
+
const progress = Object.defineProperties(
|
|
1384
|
+
{
|
|
1385
|
+
total: (total) => this.setProgress(this.progressCurrent, total),
|
|
1386
|
+
advance: (count = 1) => this.setProgress(this.progressCurrent + count, this.progressTotal),
|
|
1387
|
+
set: (current, total) => this.setProgress(current, total ?? this.progressTotal)
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
current: { enumerable: true, get: () => this.progressCurrent },
|
|
1391
|
+
snapshot: {
|
|
1392
|
+
enumerable: true,
|
|
1393
|
+
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1397
|
+
);
|
|
1238
1398
|
const signal = this.abortController.signal;
|
|
1239
1399
|
this.context = {
|
|
1240
1400
|
runId,
|
|
1241
1401
|
task,
|
|
1242
1402
|
log,
|
|
1243
1403
|
items,
|
|
1404
|
+
progress,
|
|
1244
1405
|
tx,
|
|
1245
1406
|
signal,
|
|
1246
1407
|
throwIfCanceled: () => {
|
|
@@ -1284,6 +1445,14 @@ var RunReporter = class {
|
|
|
1284
1445
|
uploadQueue = [];
|
|
1285
1446
|
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1286
1447
|
attachmentsUnsupported = false;
|
|
1448
|
+
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
1449
|
+
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
1450
|
+
progressCurrent = 0;
|
|
1451
|
+
progressTotal = null;
|
|
1452
|
+
/** Snapshot ainda não entregue ao servidor. */
|
|
1453
|
+
progressDirty = false;
|
|
1454
|
+
progressTimer;
|
|
1455
|
+
progressWarned = false;
|
|
1287
1456
|
get isDead() {
|
|
1288
1457
|
return this.dead;
|
|
1289
1458
|
}
|
|
@@ -1301,6 +1470,7 @@ var RunReporter = class {
|
|
|
1301
1470
|
this.abort();
|
|
1302
1471
|
this.dead = true;
|
|
1303
1472
|
this.clearFlushTimer();
|
|
1473
|
+
this.clearProgressTimer();
|
|
1304
1474
|
this.buffer = [];
|
|
1305
1475
|
this.itemsClosed = true;
|
|
1306
1476
|
this.clearItemsTimer();
|
|
@@ -1380,13 +1550,63 @@ var RunReporter = class {
|
|
|
1380
1550
|
reportSuccess() {
|
|
1381
1551
|
this.stopCancelWatch();
|
|
1382
1552
|
this.clearFlushTimer();
|
|
1553
|
+
this.forceFinalProgress();
|
|
1383
1554
|
this.enqueue({ status: "succeeded", logs: this.takeLogs() });
|
|
1384
1555
|
}
|
|
1385
1556
|
reportFailure(error) {
|
|
1386
1557
|
this.stopCancelWatch();
|
|
1387
1558
|
this.clearFlushTimer();
|
|
1559
|
+
this.forceFinalProgress();
|
|
1388
1560
|
this.enqueue({ status: "failed", error, logs: this.takeLogs() });
|
|
1389
1561
|
}
|
|
1562
|
+
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
1563
|
+
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
1564
|
+
setProgress(current, total) {
|
|
1565
|
+
if (this.dead) return;
|
|
1566
|
+
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
1567
|
+
if (!this.progressWarned) {
|
|
1568
|
+
this.progressWarned = true;
|
|
1569
|
+
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
1570
|
+
}
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
1574
|
+
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
1575
|
+
if (next === this.progressCurrent && total === this.progressTotal) return;
|
|
1576
|
+
this.progressCurrent = next;
|
|
1577
|
+
this.progressTotal = total;
|
|
1578
|
+
if (total == null) return;
|
|
1579
|
+
this.progressDirty = true;
|
|
1580
|
+
if (this.progressTimer) return;
|
|
1581
|
+
this.progressTimer = setTimeout(() => {
|
|
1582
|
+
this.progressTimer = void 0;
|
|
1583
|
+
this.flushProgress();
|
|
1584
|
+
}, PROGRESS_FLUSH_MS);
|
|
1585
|
+
unrefTimer(this.progressTimer);
|
|
1586
|
+
}
|
|
1587
|
+
flushProgress() {
|
|
1588
|
+
this.clearProgressTimer();
|
|
1589
|
+
if (this.dead || !this.progressDirty) return;
|
|
1590
|
+
if (this.buffer.length > 0) this.flushLogs();
|
|
1591
|
+
else this.enqueue({ status: "running" });
|
|
1592
|
+
}
|
|
1593
|
+
clearProgressTimer() {
|
|
1594
|
+
if (this.progressTimer) {
|
|
1595
|
+
clearTimeout(this.progressTimer);
|
|
1596
|
+
this.progressTimer = void 0;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
/** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
|
|
1600
|
+
forceFinalProgress() {
|
|
1601
|
+
this.clearProgressTimer();
|
|
1602
|
+
this.progressDirty = this.progressTotal != null;
|
|
1603
|
+
}
|
|
1604
|
+
/** Consome o snapshot pendente para pegar carona no próximo PATCH. */
|
|
1605
|
+
takeProgress() {
|
|
1606
|
+
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
1607
|
+
this.progressDirty = false;
|
|
1608
|
+
return { current: this.progressCurrent, total: this.progressTotal };
|
|
1609
|
+
}
|
|
1390
1610
|
/**
|
|
1391
1611
|
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
1392
1612
|
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
@@ -1649,11 +1869,14 @@ var RunReporter = class {
|
|
|
1649
1869
|
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1650
1870
|
]);
|
|
1651
1871
|
}
|
|
1652
|
-
enqueue(
|
|
1872
|
+
enqueue(updateIn) {
|
|
1653
1873
|
if (this.dead) {
|
|
1654
|
-
this.salvageLogs(
|
|
1874
|
+
this.salvageLogs(updateIn.logs);
|
|
1655
1875
|
return;
|
|
1656
1876
|
}
|
|
1877
|
+
this.clearProgressTimer();
|
|
1878
|
+
const progress = this.takeProgress();
|
|
1879
|
+
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
1657
1880
|
this.pendingUpdates++;
|
|
1658
1881
|
this.chain = this.chain.then(async () => {
|
|
1659
1882
|
if (this.dead) {
|
|
@@ -1679,6 +1902,7 @@ var RunReporter = class {
|
|
|
1679
1902
|
this.buffer.unshift(...update.logs);
|
|
1680
1903
|
this.scheduleFlush();
|
|
1681
1904
|
}
|
|
1905
|
+
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
1682
1906
|
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
1683
1907
|
}
|
|
1684
1908
|
}).finally(() => {
|
|
@@ -1900,7 +2124,10 @@ var TaskAgent = class {
|
|
|
1900
2124
|
// o servidor calcula a tolerância de offline (3×) a partir disto em
|
|
1901
2125
|
// agentes poll-only — sem o campo, um poll lento flaparia offline/online.
|
|
1902
2126
|
// Clampado à faixa aceita pelo schema do poll (1s..15min).
|
|
1903
|
-
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5)
|
|
2127
|
+
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
|
|
2128
|
+
// recursos da máquina — alimenta a série da instância em agentes sem heartbeat
|
|
2129
|
+
// (o servidor grava no máx. 1 ponto/30s por instância)
|
|
2130
|
+
machine: this.options.machineSampler?.sample()
|
|
1904
2131
|
},
|
|
1905
2132
|
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
1906
2133
|
tasks: [...this.registry.keys()]
|
|
@@ -2048,7 +2275,9 @@ var DEFAULT_MAX_BREADCRUMBS = 100;
|
|
|
2048
2275
|
var client;
|
|
2049
2276
|
var scope = new Scope();
|
|
2050
2277
|
var heartbeat;
|
|
2278
|
+
var machineSampler;
|
|
2051
2279
|
var agent;
|
|
2280
|
+
var uninstallGlobalHandlers;
|
|
2052
2281
|
var registry = /* @__PURE__ */ new Map();
|
|
2053
2282
|
var warnedTasksDisabled = false;
|
|
2054
2283
|
function init(options) {
|
|
@@ -2061,21 +2290,26 @@ function init(options) {
|
|
|
2061
2290
|
void agent.stop(0);
|
|
2062
2291
|
agent = void 0;
|
|
2063
2292
|
}
|
|
2293
|
+
uninstallGlobalHandlers?.();
|
|
2294
|
+
uninstallGlobalHandlers = void 0;
|
|
2064
2295
|
warnedTasksDisabled = false;
|
|
2065
2296
|
const config = resolveConfig(options);
|
|
2066
2297
|
client = new Client(options, config);
|
|
2298
|
+
machineSampler = options.machineMetrics === false ? void 0 : new MachineSampler(options.instanceId, client.logger);
|
|
2299
|
+
void machineSampler?.start();
|
|
2300
|
+
client.attachMachineSampler(machineSampler);
|
|
2067
2301
|
if (options.captureUnhandled !== false) {
|
|
2068
|
-
installGlobalHandlers(client, () => scope);
|
|
2302
|
+
uninstallGlobalHandlers = installGlobalHandlers(client, () => scope);
|
|
2069
2303
|
}
|
|
2070
2304
|
if (options.heartbeat) {
|
|
2071
2305
|
const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
|
|
2072
|
-
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
2306
|
+
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId, machineSampler);
|
|
2073
2307
|
heartbeat.start();
|
|
2074
2308
|
}
|
|
2075
2309
|
if (options.tasks) {
|
|
2076
2310
|
const tasksOptions = options.tasks === true ? {} : options.tasks;
|
|
2077
2311
|
agent = new TaskAgent(
|
|
2078
|
-
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client },
|
|
2312
|
+
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client, machineSampler },
|
|
2079
2313
|
registry
|
|
2080
2314
|
);
|
|
2081
2315
|
}
|