hobots 1.3.4 → 1.3.6

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)
@@ -120,12 +120,12 @@ try {
120
120
  throw error;
121
121
  }
122
122
 
123
- Hobots.captureMessage('Lote processado com sucesso', 'info');
123
+ Hobots.captureMessage('Lote processado com sucesso', 'success');
124
124
  ```
125
125
 
126
126
  Ambas retornam o `event_id` (ou `undefined` se não inicializado). O hint aceita `level`, `tags`, `extra` e `fingerprint`. Erros **não tratados** são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`.
127
127
 
128
- `Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug'`.
128
+ `Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success'`. Use `'success'` para reportar itens concluídos com êxito — contam como eventos, mas **não** viram issue nem disparam alertas.
129
129
 
130
130
  ### Breadcrumbs
131
131
 
@@ -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
@@ -111,7 +111,7 @@ function parseStacktrace(stack) {
111
111
 
112
112
  // src/utils.ts
113
113
  var SDK_NAME = "hobots";
114
- var SDK_VERSION = true ? "1.3.3" : "0.0.0-dev";
114
+ var SDK_VERSION = true ? "1.3.5" : "0.0.0-dev";
115
115
  function isNode() {
116
116
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
117
117
  }
@@ -141,11 +141,26 @@ function sleep(ms) {
141
141
  var PREFIX = "[hobots-monitor]";
142
142
  function makeLogger(enabled, prefix = PREFIX) {
143
143
  return {
144
- debug: (...args) => enabled && console.debug(prefix, ...args),
145
- warn: (...args) => enabled && console.warn(prefix, ...args),
146
- error: (...args) => console.error(prefix, ...args)
144
+ debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
145
+ warn: (...args) => enabled && console.warn(colorize(prefix, "yellow"), ...args),
146
+ error: (...args) => console.error(colorize(prefix, "red"), ...args)
147
147
  };
148
148
  }
149
+ var ANSI = {
150
+ reset: "\x1B[0m",
151
+ gray: "\x1B[90m",
152
+ cyan: "\x1B[36m",
153
+ yellow: "\x1B[33m",
154
+ red: "\x1B[31m"
155
+ };
156
+ function colorEnabled() {
157
+ if (!isNode()) return false;
158
+ return Boolean(process.stdout?.isTTY) && !process.env.NO_COLOR;
159
+ }
160
+ function colorize(text, color) {
161
+ if (!colorEnabled()) return text;
162
+ return `${ANSI[color]}${text}${ANSI.reset}`;
163
+ }
149
164
 
150
165
  // src/eventbuilder.ts
151
166
  function safeString(value) {
@@ -617,11 +632,12 @@ var Span = class {
617
632
  }
618
633
  };
619
634
  var Transaction = class {
620
- constructor(client2, name, op, sampled = true) {
635
+ constructor(client2, name, op, sampled = true, runId) {
621
636
  this.client = client2;
622
637
  this.name = name;
623
638
  this.op = op;
624
639
  this.sampled = sampled;
640
+ this.runId = runId;
625
641
  }
626
642
  eventId = uuid4();
627
643
  spanId = spanId();
@@ -664,7 +680,8 @@ var Transaction = class {
664
680
  start_timestamp: this.startTimestamp,
665
681
  timestamp: nowISO(),
666
682
  spans: this.spans.map((span) => span.toJSON()),
667
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0
683
+ tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
684
+ run_id: this.runId
668
685
  };
669
686
  this.client.sendTransaction(payload);
670
687
  }
@@ -799,6 +816,17 @@ var ManagerApi = class {
799
816
  };
800
817
 
801
818
  // src/manager/context.ts
819
+ var LOG_LEVEL_COLOR = {
820
+ debug: "gray",
821
+ info: "cyan",
822
+ warning: "yellow",
823
+ error: "red"
824
+ };
825
+ function printRunLog(task, level, message) {
826
+ const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
827
+ const method = level === "warning" ? "warn" : level;
828
+ console[method](line, message);
829
+ }
802
830
  function runCanceledError() {
803
831
  return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
804
832
  }
@@ -828,7 +856,7 @@ var MAX_LOGS_PER_RUN = 500;
828
856
  var MAX_LOGS_PER_PATCH = 100;
829
857
  var FLUSH_AT_BUFFERED = 20;
830
858
  var RunReporter = class {
831
- constructor(api, runId, task, logger, flushIntervalMs, cancelCheckIntervalMs) {
859
+ constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
832
860
  this.api = api;
833
861
  this.runId = runId;
834
862
  this.logger = logger;
@@ -848,6 +876,7 @@ var RunReporter = class {
848
876
  runId,
849
877
  task,
850
878
  log,
879
+ tx,
851
880
  signal,
852
881
  throwIfCanceled: () => {
853
882
  if (signal.aborted) throw runCanceledError();
@@ -888,6 +917,7 @@ var RunReporter = class {
888
917
  }
889
918
  log(level, message) {
890
919
  if (this.dead) return;
920
+ printRunLog(this.context.task, level, message);
891
921
  if (this.loggedCount >= MAX_LOGS_PER_RUN) {
892
922
  this.droppedCount++;
893
923
  return;
@@ -1062,6 +1092,7 @@ var ManagerAgent = class {
1062
1092
  this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
1063
1093
  const config = resolveConfig(options, "[hobots-manager]");
1064
1094
  this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
1095
+ this.client = options.client;
1065
1096
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
1066
1097
  this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
1067
1098
  this.instanceId = options.instanceId;
@@ -1072,6 +1103,7 @@ var ManagerAgent = class {
1072
1103
  }
1073
1104
  logger;
1074
1105
  api;
1106
+ client;
1075
1107
  pollIntervalMs;
1076
1108
  concurrency;
1077
1109
  instanceId;
@@ -1209,7 +1241,8 @@ var ManagerAgent = class {
1209
1241
  this.activeRuns.add(promise);
1210
1242
  }
1211
1243
  async executeRun(run) {
1212
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
1244
+ const tx = new Transaction(this.client, `run:${run.task}`, "manager.run", true, run.id);
1245
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
1213
1246
  this.reporters.add(reporter);
1214
1247
  try {
1215
1248
  const handler = this.registry.get(run.task);
@@ -1219,22 +1252,29 @@ var ManagerAgent = class {
1219
1252
  message: `tarefa n\xE3o registrada no agente: ${run.task}`
1220
1253
  });
1221
1254
  await reporter.finalize();
1255
+ tx.setStatus("error");
1256
+ tx.finish();
1222
1257
  return;
1223
1258
  }
1224
1259
  const ok = await reporter.reportRunning();
1225
1260
  if (!ok) {
1226
1261
  this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
1262
+ tx.setStatus("cancelled");
1263
+ tx.finish();
1227
1264
  return;
1228
1265
  }
1229
1266
  this.logger.debug(`executando run ${run.id} (${run.task})`);
1230
1267
  try {
1231
1268
  await this.invokeWithTimeout(handler, run, reporter);
1232
1269
  reporter.reportSuccess();
1270
+ tx.setStatus("ok");
1233
1271
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1234
1272
  } catch (error) {
1235
1273
  reporter.reportFailure(toRunError(error));
1274
+ tx.setStatus("error");
1236
1275
  this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
1237
1276
  }
1277
+ tx.finish();
1238
1278
  await reporter.finalize();
1239
1279
  } finally {
1240
1280
  this.reporters.delete(reporter);
@@ -1305,7 +1345,7 @@ function init(options) {
1305
1345
  if (options.manager) {
1306
1346
  const managerOptions = options.manager === true ? {} : options.manager;
1307
1347
  agent = new ManagerAgent(
1308
- { ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug },
1348
+ { ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug, client },
1309
1349
  registry
1310
1350
  );
1311
1351
  }