hobots 2.4.0 → 3.0.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 +112 -369
- package/dist/index.cjs +1217 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -214
- package/dist/index.d.ts +218 -214
- package/dist/index.js +1217 -852
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +15 -2
- package/dist/protocol.d.ts +15 -2
- package/docs/README.md +16 -33
- package/docs/api-reference.md +74 -213
- package/docs/concepts.md +60 -393
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +28 -19
- package/docs/tasks.md +92 -251
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/docs/tasks.md
CHANGED
|
@@ -1,314 +1,155 @@
|
|
|
1
|
-
# Tasks
|
|
1
|
+
# Tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Uma **task** é o que o robô sabe fazer, identificada por um slug. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de registrar uma task; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Ciclo de vida
|
|
5
|
+
## Os dois modos
|
|
8
6
|
|
|
9
7
|
```js
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Hobots.init({
|
|
13
|
-
clientSecret: 'hb_<64 hex>',
|
|
14
|
-
instanceId: 'vm-cliente-01',
|
|
15
|
-
tasks: true, // ou { pollIntervalMs, concurrency, taskTimeoutMs, logFlushIntervalMs }
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
19
|
-
ctx.log.info(`processando lote ${params.lote}…`);
|
|
20
|
-
ctx.throwIfCanceled(); // cancelamento cooperativo
|
|
21
|
-
ctx.log.info('12 notas processadas');
|
|
22
|
-
});
|
|
8
|
+
// sob demanda: uma execução por solicitação do app (formulário com este task_slug ou agendamento)
|
|
9
|
+
hobots.register('processar-nfe', async params => { /* … */ }, { concurrency: 2 });
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
process.on('SIGINT', () => {
|
|
27
|
-
void Hobots.close(15_000).then(() => process.exit(0));
|
|
28
|
-
});
|
|
11
|
+
// sempre ligado: o robô abre a própria execução e roda o handler (o seu loop) uma vez
|
|
12
|
+
hobots.start('monitor-pasta', async () => { /* while (true) { … } */ });
|
|
29
13
|
```
|
|
30
14
|
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
-
|
|
34
|
-
- `stop(timeoutMs?)` — para **só o polling de tasks**: encerra o polling e aguarda as runs ativas ([default](./api-reference.md#tempos-e-intervalos)); a telemetria continua. Retorna `false` se estourou o timeout.
|
|
35
|
-
- `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final.
|
|
15
|
+
- `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. Registre antes de haver solicitações; o polling começa sozinho.
|
|
16
|
+
- `start` — ao iniciar, o SDK cria a execução (`POST /v1/tasks/runs`) e roda o handler com `params = {}`. Enquanto a plataforma não responde, insiste com backoff — o handler só começa quando a execução existe. Quando o handler retorna, a execução fecha como `succeeded` (ou `failed` após os retries) e o processo segue vivo pelo heartbeat. **Uma execução por processo** — para "recomeçar", reinicie o robô.
|
|
17
|
+
- Os dois coexistem no mesmo processo. Um slug só pode ser registrado uma vez (lança).
|
|
36
18
|
|
|
37
|
-
|
|
19
|
+
> No modo start, lembre do teto de **1000 linhas de log por execução** no servidor: logue novidades e use `hobots.items` para o trabalho feito (até 10.000 itens).
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## O handler e o `RunContext`
|
|
21
|
+
## O handler e o objeto `hobots`
|
|
42
22
|
|
|
43
23
|
```ts
|
|
44
|
-
type TaskHandler<P = Record<string, unknown>> =
|
|
45
|
-
(params: P, ctx: RunContext) => void | Promise<void>;
|
|
46
|
-
|
|
47
|
-
interface RunContext {
|
|
48
|
-
runId: string;
|
|
49
|
-
task: string;
|
|
50
|
-
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
51
|
-
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
52
|
-
tx: Transaction;
|
|
53
|
-
signal: AbortSignal;
|
|
54
|
-
throwIfCanceled(): void;
|
|
55
|
-
sleep(ms: number): Promise<void>;
|
|
56
|
-
}
|
|
24
|
+
type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
|
|
57
25
|
```
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
- **`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 em tempo quase real. Cada linha também é **impressa no terminal local** como `[hobots:<task>] INFO mensagem`, colorida por nível — útil para acompanhar o robô rodando na VM. A cor é desligada automaticamente fora de um TTY e quando a env `NO_COLOR` está definida; o eco local é independente do envio ao servidor.
|
|
61
|
-
- **`ctx.items`** — registro dos **itens que a execução processou/gerou** (notas fiscais baixadas, boletos emitidos…), com status e atributos. Aparecem na aba "itens" da solicitação, com totalizadores (veja a seção abaixo).
|
|
62
|
-
- **`ctx.tx`** — a [transaction de performance](./concepts.md#transaction-de-run-ctxtx) desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild()` para medir as etapas (veja abaixo).
|
|
63
|
-
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**. Cancelamento é cooperativo (veja abaixo).
|
|
64
|
-
- **`ctx.throwIfCanceled()`** — lança `RunCanceled` se o `signal` já disparou. O ponto de parada de 1 linha para chamar entre as etapas do trabalho.
|
|
65
|
-
- **`ctx.sleep(ms)`** — sleep que respeita o `signal`: rejeita com `RunCanceled` no cancelamento em vez de esperar até o fim. Use no lugar do seu `sleep` em esperas longas.
|
|
66
|
-
- **Retorno** — **ignorado**. O SDK não captura nem envia o valor retornado pelo handler ao servidor — resultados de execução podem conter dados sensíveis e não são coletados.
|
|
67
|
-
- **Erro lançado** — a run é reportada como `failed` com tipo/mensagem/stack (mensagem cap 10k, stack 20k).
|
|
68
|
-
|
|
69
|
-
Você pode tipar os params: `Hobots.register<{ lote: number }>(...)`.
|
|
70
|
-
|
|
71
|
-
> **Dados sensíveis:** o que você escreve em `ctx.log` e as mensagens/stack de erros lançados **são enviados ao servidor** e ficam visíveis na execução no app. Não inclua senhas, tokens, documentos, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
|
|
72
|
-
|
|
73
|
-
## Itens de execução (`ctx.items`)
|
|
74
|
-
|
|
75
|
-
Logs contam **como** o bot trabalhou; itens contam **o que** ele entregou. Cada item é uma unidade de negócio processada pela execução — uma nota fiscal baixada, um boleto emitido — registrada com um dos três status:
|
|
76
|
-
|
|
77
|
-
- **`succeeded`** — processado com sucesso.
|
|
78
|
-
- **`failed`** — falha técnica ao processar o item (ex.: o download quebrou).
|
|
79
|
-
- **`occurrence`** — ocorrência de negócio, que não é falha do bot (ex.: "nota nº 10 não encontrada", "CNPJ do prestador incorreto").
|
|
27
|
+
Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto global `hobots` resolve a execução atual:
|
|
80
28
|
|
|
81
29
|
```js
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
await baixar(nota);
|
|
90
|
-
ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
|
|
91
|
-
} catch (erro) {
|
|
92
|
-
ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Na plataforma, os itens aparecem na aba **itens** da solicitação, com totalizadores (total, sucesso, falhas, ocorrências), filtro por status e paginação. As chaves do `payload` que viram **colunas** da tabela são configuradas por formulário em Configurações → Formulários → itens da execução — o `payload` aceita qualquer atributo que você julgar útil (só as chaves configuradas são exibidas; o restante fica no registro).
|
|
99
|
-
|
|
100
|
-
### API
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
interface RunItems {
|
|
104
|
-
add(item: { status: 'succeeded' | 'failed' | 'occurrence'; id?: string; message?: string; payload?: Record<string, unknown> }): void;
|
|
105
|
-
succeeded(payload?, options?: { id?: string }): void;
|
|
106
|
-
failed(message, payload?, options?: { id?: string }): void;
|
|
107
|
-
occurrence(message, payload?, options?: { id?: string }): void;
|
|
30
|
+
// notas.js — não recebe ctx nenhum
|
|
31
|
+
export async function baixar(nota) {
|
|
32
|
+
const step = hobots.transaction.startChild('http.client', `GET /notas/${nota}`);
|
|
33
|
+
const xml = await portal.baixar(nota);
|
|
34
|
+
step.setData('bytes', xml.length).finish();
|
|
35
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota });
|
|
108
36
|
}
|
|
109
37
|
```
|
|
110
38
|
|
|
111
|
-
|
|
112
|
-
- **`message`** — o motivo, esperado em `failed`/`occurrence` (cap 2.000 caracteres). Passa por `scrubSecrets`, como os logs.
|
|
113
|
-
- **`payload`** — atributos livres do item (cap 16 KB serializado; acima disso o payload é descartado com aviso e o item mantido). Strings passam por `scrubSecrets`.
|
|
114
|
-
- **Registro único** — o item chega com o status final; não existe "atualizar item". Se o bot reprocessa, use outra run (retry cria uma run nova, com itens próprios).
|
|
115
|
-
|
|
116
|
-
### Como funciona (e por que não trava a run)
|
|
39
|
+
Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto async (`AsyncLocalStorage`). Com uma única execução ativa, funciona até fora da cadeia async (um listener criado antes do handler, por exemplo). O `ctx` do 2º argumento é o mesmo objeto — use se preferir explícito. Tipar params: `hobots.register<{ lote: number }>(…)`.
|
|
117
40
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Diferente de logs e status, **uma run cancelada continua aceitando itens**: item é fato de trabalho já feito, e o handler pode continuar rodando após o cancelamento cooperativo.
|
|
121
|
-
|
|
122
|
-
### Limites
|
|
123
|
-
|
|
124
|
-
| Limite | Valor |
|
|
41
|
+
| Membro | Notas |
|
|
125
42
|
| --- | --- |
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
43
|
+
| `hobots.log.<debug\|info\|warn\|error\|success>(msg)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal. Nunca lança. |
|
|
44
|
+
| `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
|
|
45
|
+
| `hobots.progress` | Ver [Progresso](#progresso-hobotsprogress) |
|
|
46
|
+
| `hobots.transaction` | Transaction da execução, já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
|
|
47
|
+
| `hobots.attach(imagem, opts?)` | Ver [Anexos](#anexos-screenshots) |
|
|
48
|
+
| `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
|
|
49
|
+
| `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
|
|
50
|
+
| `hobots.sleep(ms)` · `hobots.signal` · `hobots.throwIfCanceled()` | Ver [Cancelamento](#cancelamento-cooperativo) |
|
|
51
|
+
| `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução |
|
|
52
|
+
|
|
53
|
+
- **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
|
|
54
|
+
- **Erro lançado**: nova tentativa (ver Retry) e, esgotadas, `failed` com tipo, mensagem (cap 10k) e stack (cap 20k), passando pelo scrub de segredos.
|
|
130
55
|
|
|
131
|
-
> **Dados sensíveis:**
|
|
56
|
+
> **Dados sensíveis:** logs, itens, erros e prints são enviados ao servidor e ficam visíveis no app.
|
|
132
57
|
|
|
133
|
-
##
|
|
58
|
+
## Itens de execução (`hobots.items`)
|
|
134
59
|
|
|
135
|
-
|
|
60
|
+
A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas configuráveis por formulário no app), totalizadores na solicitação.
|
|
136
61
|
|
|
137
62
|
```js
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await baixar(nota);
|
|
143
|
-
ctx.progress.advance(); // a cada item
|
|
144
|
-
}
|
|
145
|
-
});
|
|
63
|
+
hobots.items.succeeded({ numero_nota: nota.numero, cnpj: nota.cnpj }, { id: nota.numero });
|
|
64
|
+
hobots.items.failed('portal fora do ar', { numero_nota: nota.numero }, { id: nota.numero }); // falha técnica
|
|
65
|
+
hobots.items.occurrence('nota não encontrada', { numero_nota: '10' }, { id: '10' }); // ocorrência de negócio
|
|
66
|
+
hobots.items.add({ status: 'succeeded', id: '11', payload: { … } }); // forma genérica
|
|
146
67
|
```
|
|
147
68
|
|
|
148
|
-
|
|
69
|
+
- Chamadas síncronas e bufferizadas (lotes de até 500); o SDK drena o buffer antes do reporte final.
|
|
70
|
+
- `id` é a identidade de negócio do item na execução — repetido, o servidor ignora (retry de rede e retry do handler idempotentes). Sem `id`, cada chamada é um item novo.
|
|
71
|
+
- Limites: 10.000 itens/execução, payload 16 KB, mensagem 2.000 caracteres. Uma execução cancelada ainda aceita os itens já bufferizados (trabalho feito).
|
|
72
|
+
- Cada chamada é um **ponto de parada cooperativo** (lança `RunCanceled` se a execução foi cancelada).
|
|
149
73
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
74
|
+
## Progresso (`hobots.progress`)
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
hobots.progress.total(notas.length);
|
|
78
|
+
for (const nota of notas) {
|
|
79
|
+
await baixar(nota);
|
|
80
|
+
hobots.progress.advance(); // barra, percentual e tempo restante no app
|
|
157
81
|
}
|
|
158
82
|
```
|
|
159
83
|
|
|
160
|
-
|
|
161
|
-
- **Independente de `ctx.items`** — use um, outro ou ambos; nenhum avança o outro (um item pode gerar vários registros, ou nenhum).
|
|
162
|
-
- **Total inválido** (`0`, negativo, não-inteiro) é ignorado com um aviso único no terminal.
|
|
163
|
-
- **Semântica de snapshot** — o servidor sobrescreve `current`/`total` com o último valor recebido; não há soma. Ajustar o `total` no meio da run (a lista cresceu) é permitido.
|
|
164
|
-
|
|
165
|
-
### Como funciona (e por que não gera tráfego)
|
|
166
|
-
|
|
167
|
-
As chamadas são **síncronas e baratas**: o snapshot mais recente pega carona em **qualquer** PATCH que saia — logs incrementais, heartbeat de cancelamento, reporte terminal. Só quando nada mais está a caminho o SDK dispara um PATCH `running` próprio, no máximo **1 por segundo**. `advance()` num loop de 10.000 itens em 5 segundos gera ~5 PATCHes, não 10.000. O PATCH terminal (`succeeded`/`failed`) **sempre** leva o valor final — uma run que falhou aparece como `87/100`.
|
|
168
|
-
|
|
169
|
-
O servidor grava `progress_started_at` no primeiro PATCH com progresso (relógio do servidor) — é a âncora da estimativa de término, calculada no app: `restante = decorrido × (total − current) ÷ current`.
|
|
84
|
+
Opcional e independente de `items`. Sem `total()` nada é enviado. Snapshot absoluto que pega carona nos PATCHes de log (no pior caso 1/s); o PATCH terminal leva o valor final. `set(atual, total?)`, `current` e `snapshot` completam a API. `total/advance/set` são pontos de parada cooperativos; `current`/`snapshot` nunca lançam.
|
|
170
85
|
|
|
171
86
|
## Anexos (screenshots)
|
|
172
87
|
|
|
173
|
-
Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura — `Hobots.captureException(error, hint)` e `Hobots.captureMessage(message, level, hint)` — 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, e aparece como miniatura logo abaixo dela na aba "logs do agente" da execução — clicável para ampliar.
|
|
174
|
-
|
|
175
88
|
```js
|
|
176
|
-
|
|
177
|
-
await entrar();
|
|
178
|
-
Hobots.captureMessage('portal após o login', 'info', {
|
|
179
|
-
attachment: await page.screenshot(),
|
|
180
|
-
});
|
|
89
|
+
await hobots.attach(await page.screenshot(), { caption: 'portal após o login' });
|
|
181
90
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
throw erro;
|
|
190
|
-
}
|
|
191
|
-
});
|
|
91
|
+
try {
|
|
92
|
+
await emitir(params);
|
|
93
|
+
} catch (erro) {
|
|
94
|
+
hobots.captureException(erro, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
|
|
95
|
+
throw erro;
|
|
96
|
+
}
|
|
192
97
|
```
|
|
193
98
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### O que aceita
|
|
197
|
-
|
|
198
|
-
O `attachment` pode ser a imagem direto, ou um objeto com metadados:
|
|
199
|
-
|
|
200
|
-
| Entrada | Exemplo |
|
|
201
|
-
| --- | --- |
|
|
202
|
-
| bytes | `Uint8Array`, `ArrayBuffer` ou `Buffer` — `await page.screenshot()` |
|
|
203
|
-
| caminho de arquivo | `'C:\\prints\\tela.png'` (só em Node) |
|
|
204
|
-
| objeto | `{ image, caption?, contentType? }` |
|
|
205
|
-
|
|
206
|
-
| Campo do objeto | Tipo | Default | O que faz |
|
|
207
|
-
| --- | --- | --- | --- |
|
|
208
|
-
| `image` | bytes ou caminho | — | a imagem em si |
|
|
209
|
-
| `caption` | `string` | — | legenda curta exibida sob a imagem ampliada (máx. 200 caracteres) |
|
|
210
|
-
| `contentType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | detectado | só é usado quando os *magic bytes* não bastam |
|
|
211
|
-
|
|
212
|
-
O texto da linha de log é a própria mensagem capturada (`message` do `captureMessage`, ou `Nome: mensagem` do erro no `captureException`); o nível vem do `type` da captura (`'fatal'` vira `'error'` no log da run).
|
|
213
|
-
|
|
214
|
-
### Limites
|
|
215
|
-
|
|
216
|
-
| Limite | Valor |
|
|
217
|
-
| --- | --- |
|
|
218
|
-
| formatos | PNG, JPEG e WEBP |
|
|
219
|
-
| tamanho por imagem | 5 MB |
|
|
220
|
-
| anexos por execução | 20 |
|
|
221
|
-
| anexos por linha de log | 4 |
|
|
222
|
-
| uploads simultâneos | 3 (o resto fica na fila) |
|
|
223
|
-
|
|
224
|
-
### Como funciona (e por que não trava a run)
|
|
225
|
-
|
|
226
|
-
A linha de log é registrada **na hora**, com o id do anexo; o upload roda em segundo plano. O binário vai direto para o S3 por uma URL presignada — não passa pela API do Hobots. Enquanto o upload não termina, o app mostra a miniatura como "enviando imagem…".
|
|
227
|
-
|
|
228
|
-
O anexo **nunca faz a captura lançar**. Todo insucesso vira um aviso no log da run:
|
|
229
|
-
|
|
230
|
-
| Motivo | Quando |
|
|
231
|
-
| --- | --- |
|
|
232
|
-
| `too-large` | acima de 5 MB |
|
|
233
|
-
| `quota` | passou de 20 anexos na execução |
|
|
234
|
-
| `unsupported-format` | não é PNG/JPEG/WEBP |
|
|
235
|
-
| `read-failed` | o caminho de arquivo não pôde ser lido |
|
|
236
|
-
| `unsupported` | a instalação do Hobots não tem anexos habilitados |
|
|
237
|
-
| `upload-failed` | falha de rede depois dos retries |
|
|
238
|
-
| `canceled` | a run foi cancelada ou encerrada durante o upload |
|
|
239
|
-
|
|
240
|
-
O upload é sempre fire-and-forget — a captura retorna na hora. Antes de reportar o desfecho da run, o SDK aguarda os uploads pendentes por até 15 s, para a execução não terminar com miniaturas em "enviando…".
|
|
241
|
-
|
|
242
|
-
> **Fora de uma execução:** `attachment` só funciona dentro de um handler de task — fora dele não existe log de run para pendurar a imagem, e o SDK descarta o anexo com um aviso no console (o evento em si é capturado normalmente).
|
|
99
|
+
O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo. A linha de log entra na hora; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por linha de log. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
|
|
243
100
|
|
|
244
|
-
|
|
101
|
+
## Cancelamento cooperativo
|
|
245
102
|
|
|
246
|
-
|
|
103
|
+
Cancelar a solicitação no app **não mata** o handler: o SDK detecta (o PATCH periódico recebe 409), dispara `hobots.signal` e o próximo **ponto de parada** lança `RunCanceled`:
|
|
247
104
|
|
|
248
|
-
|
|
105
|
+
- `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
|
|
106
|
+
- **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
|
|
249
107
|
|
|
250
108
|
```js
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
109
|
+
hobots.register('processar-nfe', async params => {
|
|
110
|
+
const browser = await abrir();
|
|
111
|
+
try {
|
|
112
|
+
for (const nota of params.notas) {
|
|
113
|
+
await processar(nota);
|
|
114
|
+
hobots.items.succeeded({ numero_nota: nota }, { id: nota }); // para aqui se cancelada
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
hobots.log.info('fechando navegador'); // seguro: log nunca lança
|
|
118
|
+
await browser.close();
|
|
261
119
|
}
|
|
262
120
|
});
|
|
263
121
|
```
|
|
264
122
|
|
|
265
|
-
|
|
123
|
+
`hobots.throwIfCanceled()` e `hobots.signal` (um `AbortSignal`, útil para passar a `fetch`) ficam para controle fino. O `RunCanceled` de um cancelamento vindo do app é suprimido — nada é reportado por cima. Se o handler ignorar o signal por `cancelGraceMs` (10 s), o SDK libera o slot e o handler segue em segundo plano, com todos os reportes descartados.
|
|
266
124
|
|
|
267
|
-
|
|
268
|
-
- `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale só para as transactions abertas com `startTransaction()` fora de uma run.
|
|
269
|
-
- Step que você esquecer aberta é fechada no `finish()` da transaction, então o dado não se perde.
|
|
270
|
-
- Marque etapas com ressalva usando `step.setStatus('warning')` (ex.: retry que deu certo, resultado parcial): elas aparecem em âmbar no waterfall, distintas de sucesso e de falha (`'error'`).
|
|
125
|
+
> Não use `if (hobots.signal.aborted) return;` — retornar reporta `succeeded`. É preciso lançar.
|
|
271
126
|
|
|
272
|
-
##
|
|
127
|
+
## Retry
|
|
128
|
+
|
|
129
|
+
O handler que lança é **re-invocado na mesma execução**: aviso no log da execução (`tentativa 1/2 falhou: <erro> — nova tentativa em 5000ms`), espera cancelável, mesmos `params`. Default `retries: 1` (2 tentativas), `retryDelayMs: 5_000` (ms ou `(tentativa) => ms`). Esgotadas → `failed` com o último erro.
|
|
273
130
|
|
|
274
|
-
|
|
131
|
+
Nunca há retry em `RunCanceled`, `TaskTimeout` (o timeout é por tentativa e terminal) ou execução já cancelada no app. Itens e progresso já enviados permanecem — o handler precisa ser **idempotente** (use o `id` de negócio nos itens).
|
|
132
|
+
|
|
133
|
+
Para uma chamada instável dentro do handler:
|
|
275
134
|
|
|
276
135
|
```js
|
|
277
|
-
|
|
278
|
-
for (const nota of params.notas) {
|
|
279
|
-
ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
|
|
280
|
-
await processar(nota);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
136
|
+
const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayMs: attempt => attempt * 500, shouldRetry: e => e.code !== 'ENOTFOUND' });
|
|
283
137
|
```
|
|
284
138
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Se o handler **nunca** checar `ctx.signal` (loop sem `throwIfCanceled()`, `sleep()` genérico em vez de `ctx.sleep()`), o SDK ainda assim não trava o agente: depois de `cancelGraceMs` (default `10_000`) a partir do disparo de `ctx.signal`, o agente desiste de esperar aquele handler e libera o slot de concorrência para novas runs — logando um aviso. Isso não mata o handler (ele pode continuar rodando em segundo plano, sem efeito no app já que a run está cancelada), só evita que uma run "zumbi" trave o agente inteiro num `concurrency` baixo.
|
|
139
|
+
`hobots.retry` usa o signal da execução: cancelada, ele lança `RunCanceled` em vez de insistir. Fora de uma execução funciona igual, sem cancelamento.
|
|
288
140
|
|
|
289
|
-
##
|
|
141
|
+
## Concorrência (`register`)
|
|
290
142
|
|
|
291
|
-
|
|
143
|
+
`concurrency` (1..5, default 1) por task. O agente só pede à plataforma o que consegue rodar (tasks com slot livre, `max` = soma dos slots, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
|
|
292
144
|
|
|
293
|
-
|
|
294
|
-
- **Morte dura** (kill -9, queda de energia, rede fora): nada roda no cliente. O heartbeat de cancelamento é também o **sinal de vida da run** — o servidor marca a run como falha ao notar silêncio prolongado (o [sweep](./api-reference.md#tempos-e-intervalos) server-side).
|
|
145
|
+
## Timeout
|
|
295
146
|
|
|
296
|
-
`
|
|
147
|
+
`timeoutMs` (default: nenhum) é por tentativa e **client-side**: no estouro o SDK dispara o signal, libera o slot e reporta `failed`/`TaskTimeout` — sem nova tentativa. O handler não é morto; respeite o signal para parar de verdade.
|
|
297
148
|
|
|
298
149
|
## Idempotência (at-least-once até `running`)
|
|
299
150
|
|
|
300
|
-
Se o agente morrer logo
|
|
301
|
-
|
|
302
|
-
## Concorrência
|
|
303
|
-
|
|
304
|
-
`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](./concepts.md#drain-mode)**: enquanto vêm runs, re-pesquisa imediatamente; quando não vem nada, espera `pollIntervalMs`. Ou seja, o intervalo de poll é o teto da espera **quando não há trabalho** — não a latência típica para começar uma run.
|
|
151
|
+
Se o agente morrer logo após capturar uma execução (antes de reportar `running`), ela é re-entregue quando um agente voltar (até 3 tentativas; depois `AgentLost`). A partir de `running`, a morte do agente **não** re-entrega — a execução vira `AgentTerminated`, porque o trabalho pode ter efeitos parciais. Somado ao retry do handler, a regra é uma: processar a mesma coisa duas vezes não pode duplicar efeito.
|
|
305
152
|
|
|
306
|
-
##
|
|
307
|
-
|
|
308
|
-
`taskTimeoutMs` é um timeout **client-side**. No estouro o SDK **não mata** o handler — ele dispara `ctx.signal`, libera o slot de concorrência e reporta a run como falha (`TaskTimeout`). O handler pode continuar rodando em background; respeite o `signal` para parar de verdade. Sem `taskTimeoutMs`, o backstop é o servidor.
|
|
309
|
-
|
|
310
|
-
Todas as opções em [Referência de API → TasksOptions](./api-reference.md#tasksoptions).
|
|
311
|
-
|
|
312
|
-
## Como o claim funciona (resumo)
|
|
153
|
+
## Encerramento inesperado
|
|
313
154
|
|
|
314
|
-
|
|
155
|
+
Sem handler próprio de `SIGINT`/`SIGTERM` (ou em erro fatal), o SDK reporta as execuções ativas como `failed`/`AgentTerminated` antes de sair. Com handler próprio, chame `hobots.close()` nele. Se o processo morrer sem reportar, o servidor marca a execução após ~120 s sem sinal de vida.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hobots",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "SDK JavaScript da Hobots —
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "SDK JavaScript da Hobots — tudo é uma task: register() atende solicitações da plataforma, start() roda sempre e reporta logs, itens e progresso. Credenciais por env, heartbeat sempre ligado, objeto global `hobots`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|
package/docs/telemetry.md
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# Telemetria
|
|
2
|
-
|
|
3
|
-
Capability de telemetria: erros (módulo **Issues**), performance e uptime (módulo **Agentes**). Sempre ativa no `init()` único:
|
|
4
|
-
|
|
5
|
-
```js
|
|
6
|
-
import * as Hobots from 'hobots';
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Todas as funções abaixo são no-op seguros se `Hobots.init()` não foi chamado (com aviso no console). Assinaturas completas em [Referência de API](./api-reference.md); o que cada conceito **é**, em [Conceitos](./concepts.md).
|
|
10
|
-
|
|
11
|
-
## init
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
Hobots.init({
|
|
15
|
-
clientSecret: 'hb_<64 hex>',
|
|
16
|
-
environment: 'production',
|
|
17
|
-
release: 'robo-nfe@1.4.2',
|
|
18
|
-
instanceId: 'vm-cliente-01',
|
|
19
|
-
tags: { squad: 'automacoes' },
|
|
20
|
-
heartbeat: true,
|
|
21
|
-
});
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Cria o cliente, instala handlers globais de erro (a menos que `captureUnhandled: false`) e inicia o heartbeat se configurado. Chamar de novo substitui o cliente. Todos os campos em [Referência de API → InitOptions](./api-reference.md#initoptions).
|
|
25
|
-
|
|
26
|
-
## Captura de erros e mensagens
|
|
27
|
-
|
|
28
|
-
```js
|
|
29
|
-
// exceções
|
|
30
|
-
Hobots.captureException(error, {
|
|
31
|
-
tags: { cliente: 'acme' },
|
|
32
|
-
type: 'error',
|
|
33
|
-
fingerprint: ['sefaz-timeout'], // agrupa manualmente; sem isso o servidor agrupa pelo stack
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// mensagens sem exceção
|
|
37
|
-
Hobots.captureMessage('Lote processado com sucesso', 'info');
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Ambas retornam o `event_id` (ou `undefined` se não inicializado). O segundo argumento é um [`CaptureHint`](./api-reference.md#capturehint) (`type`, `tags`, `extra`, `fingerprint`).
|
|
41
|
-
|
|
42
|
-
> **Atenção:** o `event_id` retornado **não é confirmação de envio** — se o evento foi descartado por amostragem ou por `beforeSend`, você recebe um id normalmente e nada foi enfileirado.
|
|
43
|
-
|
|
44
|
-
Erros **não tratados** (`uncaughtException` / `unhandledRejection`) são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`.
|
|
45
|
-
|
|
46
|
-
Conceitos: [Evento](./concepts.md#evento) · [Type](./concepts.md#type-tipo-do-evento) · [Fingerprint](./concepts.md#fingerprint).
|
|
47
|
-
|
|
48
|
-
## Breadcrumbs
|
|
49
|
-
|
|
50
|
-
Trilha do que aconteceu antes do erro; enviada junto do próximo evento.
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
O buffer é limitado por `maxBreadcrumbs` (default 100, mais antigos descartados).
|
|
57
|
-
|
|
58
|
-
Dois pontos que costumam surpreender: a lista **não é limpa** ao enviar um evento (o próximo evento leva as mesmas migalhas de novo), e ela é **global** — para uma trilha que não vaze, adicione dentro de um `withScope`. Conceito: [Breadcrumb](./concepts.md#breadcrumb).
|
|
59
|
-
|
|
60
|
-
## Escopo: tags, usuário, contexto, extras
|
|
61
|
-
|
|
62
|
-
Dados anexados a **todos** os eventos seguintes:
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
Hobots.setTag('regiao', 'nordeste');
|
|
66
|
-
Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
|
|
67
|
-
Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
|
|
68
|
-
Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
|
|
69
|
-
Hobots.setExtra('ultimoLote', 128);
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Qual usar para quê: **tag** é rótulo curto e indexável (você vai filtrar por isso), **context** é grupo nomeado de dados que andam juntos (você vai ler), **extra** é depósito livre para o resto. Detalhes e critérios em [Tag](./concepts.md#tag) · [Context](./concepts.md#context) · [Extra](./concepts.md#extra).
|
|
73
|
-
|
|
74
|
-
Três comportamentos que valem saber:
|
|
75
|
-
|
|
76
|
-
- Tags podem vir de quatro lugares e a última vence: `init({ tags })` → scope → evento → `hint` da chamada.
|
|
77
|
-
- `setContext(nome, null)` **apaga** o grupo. O SDK já injeta os grupos `runtime` e `os` em todo evento.
|
|
78
|
-
- `setExtra` não é indexado — se você quiser filtrar por aquele campo, ele deveria ser uma tag.
|
|
79
|
-
|
|
80
|
-
Para um escopo **isolado** (sem vazar para o global), use `withScope`:
|
|
81
|
-
|
|
82
|
-
```js
|
|
83
|
-
Hobots.withScope((scope) => {
|
|
84
|
-
scope.setTag('lote', '128').setFingerprint(['reprocessamento']);
|
|
85
|
-
Hobots.captureMessage('Reprocessando lote 128');
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
> **Atenção:** `withScope` é **síncrono** — ele troca o scope global e restaura no fim do callback. Se você der `await` lá dentro, o scope isolado vale para qualquer código que capturar eventos enquanto aquele `await` está pendente. Em código concorrente (várias runs em paralelo), passe as tags no `hint`: `captureException(error, { tags: { lote: '128' } })`. Conceito: [withScope](./concepts.md#withscope).
|
|
90
|
-
|
|
91
|
-
## Performance: transactions e etapas
|
|
92
|
-
|
|
93
|
-
Uma **transaction** é uma unidade de trabalho nomeada e medida ("quanto minha rotina demorou"); as **etapas** são as unidades dentro dela ("onde o tempo foi gasto"). Conceitos: [Transaction](./concepts.md#transaction) · [Step](./concepts.md#step).
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
|
|
97
|
-
|
|
98
|
-
const step = tx.startChild('http.client', 'GET /api/notas');
|
|
99
|
-
await api.buscarNotas();
|
|
100
|
-
step.setData('notas', 42).finish();
|
|
101
|
-
|
|
102
|
-
const filho = step.startChild('db.query', 'INSERT INTO notas'); // etapas aninham
|
|
103
|
-
await gravar();
|
|
104
|
-
filho.finish();
|
|
105
|
-
|
|
106
|
-
tx.setStatus('ok'); // ou 'error'
|
|
107
|
-
tx.finish(); // só aqui a transaction é enviada (respeitando tracesSampleRate)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- `startChild(op?, description?)` existe tanto em `Transaction` quanto em `Step` (aninhamento arbitrário).
|
|
111
|
-
- Métodos encadeáveis: `setStatus`, `setData` (step), `setTag`/`setName` (transaction).
|
|
112
|
-
- Status de step: `'ok'` (default), `'warning'` (atenção, não é falha) ou `'error'` — cores distintas no waterfall.
|
|
113
|
-
- `finish()` na transaction fecha etapas abertas e envia o payload; nada é enviado se ela não foi amostrada. É **idempotente** — chamar duas vezes não envia duas.
|
|
114
|
-
- A decisão de amostragem acontece no **`startTransaction()`**, não no `finish()`. Esquecer o `finish()` de uma step não perde o dado: ela é fechada no instante em que a transaction fecha.
|
|
115
|
-
- Uma transaction leva as tags do `init()` e as que você marcar com `tx.setTag()` — ela **não** herda as tags do scope.
|
|
116
|
-
|
|
117
|
-
Dentro de um handler de task, `startTransaction()` **retorna a transaction da própria run** ([`ctx.tx`](./concepts.md#transaction-de-run-ctxtx)) em vez de criar uma segunda — cada execução tem exatamente **uma** transaction. Fora de um handler, ela cria uma transaction nova, sem correlação com run nenhuma.
|
|
118
|
-
|
|
119
|
-
## Heartbeat (uptime)
|
|
120
|
-
|
|
121
|
-
Bate "estou vivo" periodicamente; o servidor acusa o robô **offline** quando os batimentos param.
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
Hobots.init({
|
|
125
|
-
clientSecret,
|
|
126
|
-
heartbeat: { intervalMs: 30_000, name: 'Robô NFe — VM 01' },
|
|
127
|
-
});
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
`heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`. Campos em [HeartbeatOptions](./api-reference.md#heartbeatoptions); conceito em [Heartbeat](./concepts.md#heartbeat).
|
|
131
|
-
|
|
132
|
-
O primeiro batimento sai **na hora** em que o `init()` roda — não depois do primeiro intervalo. O batimento também carrega o intervalo configurado: é assim que o servidor sabe quanto silêncio é normal antes de declarar o robô offline.
|
|
133
|
-
|
|
134
|
-
Essa identidade é a mesma que o poll de tasks envia (`agent.instance_id`) — os dois canais derivam do `instanceId` do `init()`, então o robô aparece como **um agente só** no módulo Agentes automaticamente. Por isso, com `tasks: true` o heartbeat é **opcional** (o poll já marca a presença do agente); ele é essencial para agentes só de telemetria.
|
|
135
|
-
|
|
136
|
-
## Recursos da máquina (CPU, memória, disco)
|
|
137
|
-
|
|
138
|
-
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.
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
|
|
142
|
-
Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./protocol.md#machinesnapshot).
|
|
146
|
-
|
|
147
|
-
## Flush e close
|
|
148
|
-
|
|
149
|
-
Nada é enviado na hora: cada evento, transaction e heartbeat entra numa **fila em memória** e sai em lote. É por isso que essas duas funções existem — quando o processo morre, a fila morre com ele. Conceito: [Fila, flush e close](./concepts.md#fila-flush-e-close).
|
|
150
|
-
|
|
151
|
-
```js
|
|
152
|
-
await Hobots.flush(2000); // envia o que está em buffer, mantém o SDK vivo
|
|
153
|
-
await Hobots.close(2000); // para o polling de tasks (se ativo) + heartbeat + flush final do transport
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
O `timeoutMs` é opcional nas duas — o default é 5s (veja [Tempos e intervalos](./api-reference.md#tempos-e-intervalos)). As duas devolvem `false` quando estouram o prazo, o que significa que o SDK parou de esperar, **não** que os dados foram perdidos: os envios em voo continuam tentando enquanto o processo existir.
|
|
157
|
-
|
|
158
|
-
Chame `close` antes do processo sair. Os timers da telemetria são `unref`'d, então ela **não** segura o processo vivo sozinha (só o `start()` das tasks segura).
|
|
159
|
-
|
|
160
|
-
> **Atenção:** `close()` é **terminal**. Depois dele, todo evento, transaction ou heartbeat novo é **descartado** com aviso no console — não existe `reopen()`. Se o programa precisar voltar a emitir telemetria, chame `Hobots.init()` de novo.
|
|
161
|
-
|
|
162
|
-
> **Atenção:** o `timeoutMs` do `close()` é gasto **duas vezes, em sequência** — uma esperando as runs de tasks, outra no flush final. `close(15_000)` pode levar até ~30s no pior caso. Dimensione por aí se o seu SIGTERM tem prazo curto.
|