hobots 1.3.5 → 1.5.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
@@ -49,7 +49,7 @@ Hobots.init({
49
49
 
50
50
  Hobots.register('processar-nfe', async (params, ctx) => {
51
51
  ctx.log.info('começando…');
52
- return { processadas: 12 };
52
+ ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
53
53
  });
54
54
 
55
55
  Hobots.start(); // inicia o polling do Manager (segura o processo vivo)
@@ -135,6 +135,8 @@ Trilha do que aconteceu antes do erro; enviada junto do próximo evento (buffer
135
135
  Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
136
136
  ```
137
137
 
138
+ A lista **não** é limpa ao enviar um evento e é global. Conceito: [Breadcrumb](./docs/concepts.md#breadcrumb).
139
+
138
140
  ### Escopo: tags, usuário, contexto, extras
139
141
 
140
142
  Dados anexados a **todos** os eventos seguintes:
@@ -175,7 +177,9 @@ tx.finish(); // só aqui a transaction é enviada (respeitando tracesSam
175
177
 
176
178
  - `startChild(op?, description?)` existe em `Transaction` **e** `Span` (aninhamento arbitrário).
177
179
  - Métodos encadeáveis: `setStatus`, `setData` (span), `setTag`/`setName` (transaction).
178
- - `finish()` na transaction fecha os spans abertos e envia o payload; nada é enviado se ela não foi amostrada.
180
+ - `finish()` na transaction fecha os spans abertos e envia o payload; nada é enviado se ela não foi amostrada. A amostragem é decidida no `startTransaction()`.
181
+
182
+ Dentro de um handler do Manager não abra transaction — já existe uma em `ctx.tx`. Conceitos: [Transaction](./docs/concepts.md#transaction) · [Span](./docs/concepts.md#span).
179
183
 
180
184
  ### Crons (check-ins)
181
185
 
@@ -208,7 +212,7 @@ Hobots.init({
208
212
  });
209
213
  ```
210
214
 
211
- `heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`. Campos: `intervalMs` (30000), `name` (default: o `instanceId`), `metadata`.
215
+ `heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`; os campos são `intervalMs`, `name` (default: o `instanceId`) e `metadata` — valores em [Tempos e intervalos](./docs/api-reference.md#tempos-e-intervalos). O primeiro batimento sai **na hora** em que o `init()` roda, não depois do primeiro intervalo. Conceito: [Heartbeat](./docs/concepts.md#heartbeat).
212
216
 
213
217
  ### Flush e close
214
218
 
@@ -236,7 +240,7 @@ Hobots.init({
236
240
 
237
241
  Hobots.register('processar-nfe', async (params, ctx) => {
238
242
  ctx.log.info(`processando lote ${params.lote}…`);
239
- if (ctx.signal.aborted) return; // cancelamento cooperativo
243
+ ctx.throwIfCanceled(); // cancelamento cooperativo
240
244
  ctx.log.info('12 notas processadas');
241
245
  });
242
246
 
@@ -259,6 +263,8 @@ process.on('SIGINT', () => {
259
263
  | `concurrency` | `number` | `1` | runs simultâneas (clamp 1..5) |
260
264
  | `taskTimeoutMs` | `number` | — | timeout client-side por run (não mata o handler; servidor é o backstop) |
261
265
  | `logFlushIntervalMs` | `number` | `2000` | intervalo do envio incremental de logs |
266
+ | `cancelCheckIntervalMs` | `number` | `5000` | heartbeat de cancelamento por run (clamp 1000..15000) — também é o sinal de vida da run |
267
+ | `installExitHandlers` | `boolean` | `true` | handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs ativas como `AgentTerminated` antes do processo morrer; nunca interferem quando o programa tem handler próprio |
262
268
 
263
269
  O Client Secret, o `debug` e a identidade do agente (`instanceId`) vêm do `init()` — o heartbeat do Monitor e o agente do Manager convergem em **um agente** no painel automaticamente.
264
270
 
@@ -272,13 +278,19 @@ interface RunContext {
272
278
  runId: string;
273
279
  task: string;
274
280
  log(level: 'debug' | 'info' | 'warning' | 'error', message: string): void;
281
+ tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
275
282
  signal: AbortSignal;
283
+ throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
284
+ sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
276
285
  }
277
286
  ```
278
287
 
279
288
  - **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
280
- - **`ctx.log.<level>(message)`** — logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na run quase em tempo real.
289
+ - **`ctx.log.<level>(message)`** — logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na run quase em tempo real; também impressos no terminal local, coloridos por nível.
290
+ - **`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. Conceito: [ctx.tx](./docs/concepts.md#transaction-de-run-ctxtx).
281
291
  - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
292
+ - **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
293
+ - **`ctx.sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim).
282
294
  - **Retorno** — **ignorado**: o SDK não captura nem envia o valor retornado pelo handler (resultados podem conter dados sensíveis e não são coletados).
283
295
  - **Erro lançado** — a run é reportada como `failed` (mensagem cap 10k, stack 20k).
284
296
 
@@ -286,20 +298,21 @@ interface RunContext {
286
298
 
287
299
  ### Cancelamento cooperativo
288
300
 
289
- Uma run cancelada no app **não mata** o handler — o SDK apenas dispara `ctx.signal`. **Verifique `ctx.signal.aborted`** em loops longos:
301
+ Uma run cancelada no app **não mata** o handler — o SDK apenas dispara `ctx.signal`. Chame **`ctx.throwIfCanceled()`** em loops longos e pontos de parada (e troque esperas longas por `ctx.sleep()`):
290
302
 
291
303
  ```js
292
304
  Hobots.register('processar-nfe', async (params, ctx) => {
293
305
  for (const nota of params.notas) {
294
- if (ctx.signal.aborted) {
295
- ctx.log.warn('cancelamento detectado — parando');
296
- return;
297
- }
306
+ ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
298
307
  await processar(nota);
299
308
  }
300
309
  });
301
310
  ```
302
311
 
312
+ Quando o cancelamento veio do app, o `RunCanceled` lançado é **suprimido** — a run já está `cancelada` no servidor e nada é reportado por cima. Para limpeza antes de parar (fechar navegador, desfazer estado), use `try/finally`.
313
+
314
+ > **Atenção:** não use `if (ctx.signal.aborted) return;`. Retornar normalmente faz o SDK reportar a run como **`succeeded`** — uma run cancelada apareceria como sucesso no app. É preciso **lançar**, e é o que o `throwIfCanceled()` faz.
315
+
303
316
  ### Concorrência
304
317
 
305
318
  `concurrency` (default 1, máx 5) controla quantas runs o agente executa em paralelo. Com 1, as runs são sequenciais. O agente só reivindica o que consegue rodar (`max = concurrency - runs ativas`) e entra em **drain mode**: re-pesquisa imediatamente enquanto vem trabalho; quando não vem, espera `pollIntervalMs`.
@@ -308,9 +321,11 @@ Hobots.register('processar-nfe', async (params, ctx) => {
308
321
 
309
322
  `taskTimeoutMs` é **client-side**. No estouro o SDK **não mata** o handler — dispara `ctx.signal`, libera o slot e reporta a run como falha (`TaskTimeout`). Respeite o `signal` para parar de verdade. Sem ele, o backstop é o servidor.
310
323
 
311
- ### Idempotência (at-least-once)
324
+ ### Idempotência (at-least-once até `running`)
325
+
326
+ Se o agente morrer logo **após capturar** uma run (antes de reportar `running`), ela é re-entregue quando um agente voltar (até 3 tentativas; depois `AgentLost`). A partir do momento em que a run está `running`, a morte do agente **não re-entrega** — a run vira falha `AgentTerminated`, porque o trabalho pode ter efeitos colaterais parciais e re-executar automaticamente não é seguro (reexecute manualmente pelo app).
312
327
 
313
- A entrega é **at-least-once**: se o agente morre logo após capturar uma run (antes de reportar a conclusão), ela é re-entregue quando o agente voltar. Escreva handlers **idempotentes** (use uma chave de deduplicação sua, como o número da nota).
328
+ Ainda assim, escreva handlers **idempotentes**: a janela de re-entrega pós-claim existe, e processar a mesma run duas vezes não pode causar efeito duplicado (use uma chave de deduplicação sua, como o número da nota).
314
329
 
315
330
  ---
316
331
 
@@ -343,9 +358,9 @@ Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para to
343
358
 
344
359
  ---
345
360
 
346
- ## Migração 1.x → 2.x
361
+ ## Migração da API antiga (`hobots/manager`)
347
362
 
348
- A 2.0 unifica o Manager no import raiz — `hobots/manager` **foi removido**:
363
+ O Manager foi unificado no import raiz — `hobots/manager` **foi removido**:
349
364
 
350
365
  ```diff
351
366
  -import * as Monitor from 'hobots';
@@ -368,16 +383,19 @@ A 2.0 unifica o Manager no import raiz — `hobots/manager` **foi removido**:
368
383
 
369
384
  - A `dsn` (mecanismo antigo, `https://<secretKey>@host/<slug>`) não existe mais — passe `clientSecret` (em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL`). Copie o Client Secret em **Configurações → Projetos**.
370
385
  - `agentName` e `serverName` não existem mais — a identidade do agente é o `instanceId` (obrigatório, sem fallback).
371
- - As opções do Manager (`pollIntervalMs`, `concurrency`, `taskTimeoutMs`, `logFlushIntervalMs`) vão no campo `manager` do `init()`; `clientSecret` e `debug` não se repetem.
386
+ - As opções do Manager (`pollIntervalMs`, `concurrency`, `taskTimeoutMs`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `installExitHandlers`) vão no campo `manager` do `init()`; `clientSecret` e `debug` não se repetem.
372
387
  - Tipos (`ManagerTaskHandler`, `RunContext`, `ManagerOptions`) e classes (`ManagerAgent`, `ManagerApi`) agora vêm de `hobots`; o wire format vem de `hobots/protocol`.
373
388
 
374
389
  ---
375
390
 
376
391
  ## Documentação completa
377
392
 
378
- Guias detalhados acompanham o pacote em [`docs/`](./docs/README.md):
393
+ Guias detalhados acompanham o pacote em [`docs/`](./docs/README.md).
394
+
395
+ Novo no vocabulário de observabilidade (transaction, span, breadcrumb, tag, context, extra, flush, close)? Comece por [Conceitos](./docs/concepts.md) — as outras páginas assumem esses termos.
379
396
 
380
- - [Getting started](./docs/getting-started.md) — instalação, Client Secret, primeiro `init`, Monitor vs Hobots.
397
+ - [Conceitos](./docs/concepts.md) — o que cada coisa é, por que existe e quando escolher uma em vez da outra.
398
+ - [Getting started](./docs/getting-started.md) — instalação, Client Secret, primeiro `init`, Monitor e Manager.
381
399
  - [Monitor](./docs/monitor.md) — erros, breadcrumbs, escopo, transactions/spans, crons, heartbeat.
382
400
  - [Manager](./docs/manager.md) — handlers, `RunContext`, cancelamento, concorrência, idempotência.
383
401
  - [Referência de API](./docs/api-reference.md) — todas as funções e tabelas de opções com defaults.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -111,7 +121,7 @@ function parseStacktrace(stack) {
111
121
 
112
122
  // src/utils.ts
113
123
  var SDK_NAME = "hobots";
114
- var SDK_VERSION = true ? "1.3.4" : "0.0.0-dev";
124
+ var SDK_VERSION = true ? "1.5.0" : "0.0.0-dev";
115
125
  function isNode() {
116
126
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
117
127
  }
@@ -141,11 +151,26 @@ function sleep(ms) {
141
151
  var PREFIX = "[hobots-monitor]";
142
152
  function makeLogger(enabled, prefix = PREFIX) {
143
153
  return {
144
- debug: (...args) => enabled && console.debug(prefix, ...args),
145
- warn: (...args) => enabled && console.warn(prefix, ...args),
146
- error: (...args) => console.error(prefix, ...args)
154
+ debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
155
+ warn: (...args) => enabled && console.warn(colorize(prefix, "yellow"), ...args),
156
+ error: (...args) => console.error(colorize(prefix, "red"), ...args)
147
157
  };
148
158
  }
159
+ var ANSI = {
160
+ reset: "\x1B[0m",
161
+ gray: "\x1B[90m",
162
+ cyan: "\x1B[36m",
163
+ yellow: "\x1B[33m",
164
+ red: "\x1B[31m"
165
+ };
166
+ function colorEnabled() {
167
+ if (!isNode()) return false;
168
+ return Boolean(process.stdout?.isTTY) && !process.env.NO_COLOR;
169
+ }
170
+ function colorize(text, color) {
171
+ if (!colorEnabled()) return text;
172
+ return `${ANSI[color]}${text}${ANSI.reset}`;
173
+ }
149
174
 
150
175
  // src/eventbuilder.ts
151
176
  function safeString(value) {
@@ -617,11 +642,12 @@ var Span = class {
617
642
  }
618
643
  };
619
644
  var Transaction = class {
620
- constructor(client2, name, op, sampled = true) {
645
+ constructor(client2, name, op, sampled = true, runId) {
621
646
  this.client = client2;
622
647
  this.name = name;
623
648
  this.op = op;
624
649
  this.sampled = sampled;
650
+ this.runId = runId;
625
651
  }
626
652
  eventId = uuid4();
627
653
  spanId = spanId();
@@ -664,7 +690,8 @@ var Transaction = class {
664
690
  start_timestamp: this.startTimestamp,
665
691
  timestamp: nowISO(),
666
692
  spans: this.spans.map((span) => span.toJSON()),
667
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0
693
+ tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
694
+ run_id: this.runId
668
695
  };
669
696
  this.client.sendTransaction(payload);
670
697
  }
@@ -799,6 +826,17 @@ var ManagerApi = class {
799
826
  };
800
827
 
801
828
  // src/manager/context.ts
829
+ var LOG_LEVEL_COLOR = {
830
+ debug: "gray",
831
+ info: "cyan",
832
+ warning: "yellow",
833
+ error: "red"
834
+ };
835
+ function printRunLog(task, level, message) {
836
+ const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
837
+ const method = level === "warning" ? "warn" : level;
838
+ console[method](line, message);
839
+ }
802
840
  function runCanceledError() {
803
841
  return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
804
842
  }
@@ -828,7 +866,7 @@ var MAX_LOGS_PER_RUN = 500;
828
866
  var MAX_LOGS_PER_PATCH = 100;
829
867
  var FLUSH_AT_BUFFERED = 20;
830
868
  var RunReporter = class {
831
- constructor(api, runId, task, logger, flushIntervalMs, cancelCheckIntervalMs) {
869
+ constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
832
870
  this.api = api;
833
871
  this.runId = runId;
834
872
  this.logger = logger;
@@ -848,6 +886,7 @@ var RunReporter = class {
848
886
  runId,
849
887
  task,
850
888
  log,
889
+ tx,
851
890
  signal,
852
891
  throwIfCanceled: () => {
853
892
  if (signal.aborted) throw runCanceledError();
@@ -888,6 +927,7 @@ var RunReporter = class {
888
927
  }
889
928
  log(level, message) {
890
929
  if (this.dead) return;
930
+ printRunLog(this.context.task, level, message);
891
931
  if (this.loggedCount >= MAX_LOGS_PER_RUN) {
892
932
  this.droppedCount++;
893
933
  return;
@@ -1001,6 +1041,24 @@ var RunReporter = class {
1001
1041
  }
1002
1042
  };
1003
1043
 
1044
+ // src/manager/run-context-store.ts
1045
+ var storage;
1046
+ var initPromise;
1047
+ async function ensureStorage() {
1048
+ if (storage || !isNode()) return;
1049
+ initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
1050
+ storage = new ALS();
1051
+ });
1052
+ await initPromise;
1053
+ }
1054
+ async function runWithRunId(runId, fn) {
1055
+ await ensureStorage();
1056
+ return storage ? storage.run({ runId }, fn) : fn();
1057
+ }
1058
+ function currentRunId() {
1059
+ return storage?.getStore()?.runId;
1060
+ }
1061
+
1004
1062
  // src/manager/shutdown.ts
1005
1063
  var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
1006
1064
  function installShutdownHandlers(agent2) {
@@ -1062,6 +1120,7 @@ var ManagerAgent = class {
1062
1120
  this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
1063
1121
  const config = resolveConfig(options, "[hobots-manager]");
1064
1122
  this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
1123
+ this.client = options.client;
1065
1124
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
1066
1125
  this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
1067
1126
  this.instanceId = options.instanceId;
@@ -1072,6 +1131,7 @@ var ManagerAgent = class {
1072
1131
  }
1073
1132
  logger;
1074
1133
  api;
1134
+ client;
1075
1135
  pollIntervalMs;
1076
1136
  concurrency;
1077
1137
  instanceId;
@@ -1209,7 +1269,8 @@ var ManagerAgent = class {
1209
1269
  this.activeRuns.add(promise);
1210
1270
  }
1211
1271
  async executeRun(run) {
1212
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
1272
+ const tx = new Transaction(this.client, `run:${run.task}`, "manager.run", true, run.id);
1273
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
1213
1274
  this.reporters.add(reporter);
1214
1275
  try {
1215
1276
  const handler = this.registry.get(run.task);
@@ -1219,22 +1280,29 @@ var ManagerAgent = class {
1219
1280
  message: `tarefa n\xE3o registrada no agente: ${run.task}`
1220
1281
  });
1221
1282
  await reporter.finalize();
1283
+ tx.setStatus("error");
1284
+ tx.finish();
1222
1285
  return;
1223
1286
  }
1224
1287
  const ok = await reporter.reportRunning();
1225
1288
  if (!ok) {
1226
1289
  this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
1290
+ tx.setStatus("cancelled");
1291
+ tx.finish();
1227
1292
  return;
1228
1293
  }
1229
1294
  this.logger.debug(`executando run ${run.id} (${run.task})`);
1230
1295
  try {
1231
- await this.invokeWithTimeout(handler, run, reporter);
1296
+ await runWithRunId(run.id, () => this.invokeWithTimeout(handler, run, reporter));
1232
1297
  reporter.reportSuccess();
1298
+ tx.setStatus("ok");
1233
1299
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1234
1300
  } catch (error) {
1235
1301
  reporter.reportFailure(toRunError(error));
1302
+ tx.setStatus("error");
1236
1303
  this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
1237
1304
  }
1305
+ tx.finish();
1238
1306
  await reporter.finalize();
1239
1307
  } finally {
1240
1308
  this.reporters.delete(reporter);
@@ -1305,7 +1373,7 @@ function init(options) {
1305
1373
  if (options.manager) {
1306
1374
  const managerOptions = options.manager === true ? {} : options.manager;
1307
1375
  agent = new ManagerAgent(
1308
- { ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug },
1376
+ { ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug, client },
1309
1377
  registry
1310
1378
  );
1311
1379
  }
@@ -1350,7 +1418,7 @@ function withScope(callback) {
1350
1418
  function startTransaction(name, op) {
1351
1419
  const rate = client?.options.tracesSampleRate ?? 1;
1352
1420
  const sampled = rate >= 1 || Math.random() < rate;
1353
- return new Transaction(client, name, op, sampled);
1421
+ return new Transaction(client, name, op, sampled, currentRunId());
1354
1422
  }
1355
1423
  function checkIn(options) {
1356
1424
  return client ? sendCheckIn(client, options) : void 0;