hobots 1.9.0 → 1.9.2

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
@@ -264,7 +264,6 @@ interface RunContext {
264
264
  signal: AbortSignal;
265
265
  throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
266
266
  sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
267
- attach(image, options?): Promise<AttachResult>; // anexa um print a uma linha de log
268
267
  }
269
268
  ```
270
269
 
@@ -274,7 +273,6 @@ interface RunContext {
274
273
  - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
275
274
  - **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
276
275
  - **`ctx.sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim).
277
- - **`ctx.attach(image, options?)`** — anexa um print da tela à linha de log (veja [Anexos](#anexos-screenshots) abaixo).
278
276
  - **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).
279
277
  - **Erro lançado** — a run é reportada como `failed` (mensagem cap 10k, stack 20k).
280
278
 
@@ -282,23 +280,27 @@ interface RunContext {
282
280
 
283
281
  ### Anexos (screenshots)
284
282
 
285
- Texto de log conta o que o robô fez; um print conta **como estava a tela**. `ctx.attach()` anexa uma imagem a uma linha de log — no app ela aparece como miniatura logo abaixo dessa linha, clicável para ampliar.
283
+ 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.
286
284
 
287
285
  ```js
288
286
  Hobots.register('processar-nfe', async (params, ctx) => {
289
287
  await entrar();
290
- await ctx.attach(await page.screenshot(), { caption: 'portal após o login' });
288
+ Hobots.captureMessage('portal após o login', 'info', {
289
+ attachment: await page.screenshot(),
290
+ });
291
291
 
292
292
  try {
293
293
  await emitir(params);
294
294
  } catch (erro) {
295
- await ctx.attach(await page.screenshot(), { caption: 'tela no momento do erro', level: 'error' });
295
+ Hobots.captureException(erro, {
296
+ attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' },
297
+ });
296
298
  throw erro;
297
299
  }
298
300
  });
299
301
  ```
300
302
 
301
- **O SDK não tira o print** — ele recebe a imagem pronta, de onde você quiser (Playwright, Puppeteer, uma lib de captura de desktop, um arquivo em disco). Aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node).
303
+ **O SDK não tira o print** — ele recebe a imagem pronta, de onde você quiser (Playwright, Puppeteer, uma lib de captura de desktop, um arquivo em disco). O `attachment` aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`), um caminho de arquivo (só em Node) ou um objeto `{ image, caption?, contentType? }`.
302
304
 
303
305
  | Limite | Valor |
304
306
  | --- | --- |
@@ -307,7 +309,7 @@ Hobots.register('processar-nfe', async (params, ctx) => {
307
309
  | anexos por execução | 20 |
308
310
  | anexos por linha de log | 4 |
309
311
 
310
- A linha de log é registrada **na hora**; o upload roda em segundo plano e vai direto para o S3 por URL presignada, sem passar pela API. `attach()` **nunca lança** — falhas viram `{ ok: false, reason }` e um aviso no log da run, então `void ctx.attach(...)` é seguro. Detalhes e a lista de `reason` em [Anexos](./docs/tasks.md#anexos-screenshots).
312
+ A linha de log é registrada **na hora**; o upload roda em segundo plano e vai direto para o S3 por URL presignada, sem passar pela API — a captura **nunca bloqueia nem lança** por causa do anexo (falhas viram um aviso no log da run). Fora de uma execução de task não há onde pendurar a imagem: o `attachment` é descartado com um aviso. Detalhes em [Anexos](./docs/tasks.md#anexos-screenshots).
311
313
 
312
314
  > **Dados sensíveis:** o print é enviado **exatamente como está** — o SDK não borra nem recorta nada, e telas de portal costumam conter CPF, nome e valores. Capture só o necessário. Solicitantes só veem os anexos quando o projeto está com "mostrar logs para solicitantes" ligado, o mesmo gate dos logs.
313
315
 
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 ? "1.9.0" : "0.0.0-dev";
164
+ var SDK_VERSION = true ? "1.9.1" : "0.0.0-dev";
165
165
  function isNode() {
166
166
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
167
167
  }
@@ -1172,8 +1172,7 @@ var RunReporter = class {
1172
1172
  throwIfCanceled: () => {
1173
1173
  if (signal.aborted) throw runCanceledError();
1174
1174
  },
1175
- sleep: (ms) => cancelableSleep(ms, signal),
1176
- attach: (image, options) => this.attach(image, options)
1175
+ sleep: (ms) => cancelableSleep(ms, signal)
1177
1176
  };
1178
1177
  }
1179
1178
  context;
@@ -1318,7 +1317,8 @@ var RunReporter = class {
1318
1317
  /**
1319
1318
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1320
1319
  * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1321
- * com um aviso no log da run.
1320
+ * com um aviso no log da run. Chega ao handler pelos capture* do SDK — o hint
1321
+ * `attachment` de Hobots.captureException/captureMessage cai aqui via run-context-store.
1322
1322
  */
1323
1323
  async attach(image, options = {}) {
1324
1324
  const id = uuid4();
@@ -1505,6 +1505,9 @@ async function runWithRunContext(store, fn) {
1505
1505
  function currentRunTransaction() {
1506
1506
  return storage?.getStore()?.tx;
1507
1507
  }
1508
+ function currentRunAttach() {
1509
+ return storage?.getStore()?.attach;
1510
+ }
1508
1511
 
1509
1512
  // src/tasks/shutdown.ts
1510
1513
  var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
@@ -1556,6 +1559,7 @@ var MIN_POLL_INTERVAL_MS = 1e3;
1556
1559
  var MAX_CONCURRENCY = 5;
1557
1560
  var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1558
1561
  var AUTH_FAILURE_INTERVAL_MS = 3e4;
1562
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
1559
1563
  function hostname() {
1560
1564
  if (!isNode()) return void 0;
1561
1565
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
@@ -1574,6 +1578,7 @@ var TaskAgent = class {
1574
1578
  this.taskTimeoutMs = options.taskTimeoutMs;
1575
1579
  this.logFlushIntervalMs = options.logFlushIntervalMs;
1576
1580
  this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
1581
+ this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
1577
1582
  this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
1578
1583
  }
1579
1584
  logger;
@@ -1585,6 +1590,7 @@ var TaskAgent = class {
1585
1590
  taskTimeoutMs;
1586
1591
  logFlushIntervalMs;
1587
1592
  cancelCheckIntervalMs;
1593
+ cancelGraceMs;
1588
1594
  state = "idle";
1589
1595
  timer;
1590
1596
  pollAbort;
@@ -1746,7 +1752,10 @@ var TaskAgent = class {
1746
1752
  }
1747
1753
  this.logger.debug(`executando run ${run.id} (${run.task})`);
1748
1754
  try {
1749
- await runWithRunContext({ runId: run.id, tx }, () => this.invokeWithTimeout(handler, run, reporter));
1755
+ await runWithRunContext(
1756
+ { runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
1757
+ () => this.invokeWithTimeout(handler, run, reporter)
1758
+ );
1750
1759
  await reporter.finalizeAttachments();
1751
1760
  tx.setStatus("ok");
1752
1761
  tx.finish();
@@ -1766,14 +1775,26 @@ var TaskAgent = class {
1766
1775
  }
1767
1776
  async invokeWithTimeout(handler, run, reporter) {
1768
1777
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
1769
- if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
1778
+ const races = [];
1779
+ if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
1780
+ races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
1781
+ }
1782
+ if (this.cancelGraceMs > 0) {
1783
+ races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
1784
+ }
1785
+ if (races.length === 0) {
1770
1786
  await invocation;
1771
1787
  return;
1772
1788
  }
1789
+ const outcome = await Promise.race([invocation, ...races]);
1790
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
1791
+ if (outcome === CANCEL_SENTINEL) {
1792
+ this.logger.warn(
1793
+ `run ${run.id} cancelada \u2014 handler n\xE3o respondeu a ctx.signal em ${this.cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
1794
+ );
1795
+ return;
1796
+ }
1773
1797
  const timeoutMs = this.taskTimeoutMs;
1774
- const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
1775
- const outcome = await Promise.race([invocation, timeout]);
1776
- if (outcome !== TIMEOUT_SENTINEL) return;
1777
1798
  reporter.abort();
1778
1799
  invocation.then(() => this.logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => this.logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
1779
1800
  throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
@@ -1782,6 +1803,16 @@ var TaskAgent = class {
1782
1803
  }
1783
1804
  };
1784
1805
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
1806
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
1807
+ function waitForCancelGrace(signal, graceMs) {
1808
+ return new Promise((resolve) => {
1809
+ if (signal.aborted) {
1810
+ setTimeout(resolve, graceMs);
1811
+ return;
1812
+ }
1813
+ signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
1814
+ });
1815
+ }
1785
1816
  var MAX_ERROR_MESSAGE_CHARS = 1e4;
1786
1817
  var MAX_ERROR_STACK_CHARS = 2e4;
1787
1818
  function toRunError(error) {
@@ -1836,10 +1867,34 @@ function init(options) {
1836
1867
  function getClient() {
1837
1868
  return client;
1838
1869
  }
1870
+ function toRunLogLevel(level) {
1871
+ return level === "fatal" ? "error" : level;
1872
+ }
1873
+ function forwardAttachment(attachment, message, level) {
1874
+ const attach = currentRunAttach();
1875
+ if (!attach) {
1876
+ client?.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task (register/start)");
1877
+ return;
1878
+ }
1879
+ const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
1880
+ void attach(withMeta.image, {
1881
+ message,
1882
+ level: toRunLogLevel(level),
1883
+ caption: withMeta.caption,
1884
+ contentType: withMeta.contentType
1885
+ });
1886
+ }
1839
1887
  function captureException(error, hint) {
1888
+ if (hint?.attachment) {
1889
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1890
+ forwardAttachment(hint.attachment, message, hint.level ?? "error");
1891
+ }
1840
1892
  return client?.captureException(error, hint, scope);
1841
1893
  }
1842
1894
  function captureMessage(message, level = "info", hint) {
1895
+ if (hint?.attachment) {
1896
+ forwardAttachment(hint.attachment, message, hint.level ?? level);
1897
+ }
1843
1898
  return client?.captureMessage(message, level, hint, scope);
1844
1899
  }
1845
1900
  function addBreadcrumb(crumb) {