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/docs/concepts.md CHANGED
@@ -1,455 +1,122 @@
1
1
  # Conceitos
2
2
 
3
- Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam sem parar: evento, scope, tag, context, extra, breadcrumb, transaction, step, heartbeat, run, claim, flush, close. As outras páginas mostram **como usar**; aqui está o **por que existe** e **quando você escolhe um em vez do outro**.
3
+ Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam: task, execução, item, evento, issue, transaction, step, heartbeat, instância, claim. As outras páginas mostram **como usar**; aqui está o **por que existe**.
4
4
 
5
- Não é preciso ler de ponta a ponta. Se você só quer o mapa, o quadro abaixo resolve; se um termo apareceu numa página e você travou, pule direto para a seção dele.
6
-
7
- Os defaults numéricos **não** estão aqui — eles vivem em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos) e nas [tabelas de opções](./api-reference.md#tabelas-de-opções), em um lugar só.
5
+ Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos) e nas [tabelas de opções](./api-reference.md#tabelas-de-opções).
8
6
 
9
7
  ## Mapa rápido
10
8
 
11
- | Conceito | Em uma frase | Quando você usa |
12
- | ------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- |
13
- | [Evento](#evento) | Uma foto de algo que aconteceu — um erro ou uma mensagem | `captureException` / `captureMessage` |
14
- | [Type](#type-tipo-do-evento) | O que o evento é | Para separar o que acorda alguém do que é só registro |
15
- | [Fingerprint](#fingerprint) | A chave de agrupamento dos eventos em issues | Quando o agrupamento automático erra |
16
- | [Scope](#scope) | A mochila de contexto que viaja com todo evento | Você raramente mexe direto; mexe pelos `set*` |
17
- | [withScope](#withscope) | Um scope temporário e isolado | Contexto que **não** deve vazar para o resto do programa |
18
- | [Tag](#tag) | Rótulo curto e indexável (`string` → `string`) | Você vai **filtrar** ou **agrupar** por isso |
19
- | [Context](#context) | Grupo nomeado de dados estruturados | Você vai **ler** isso ao abrir o evento |
20
- | [Extra](#extra) | Depósito livre, sem estrutura | Não é filtro nem grupo coerente — é anexo |
21
- | [Breadcrumb](#breadcrumb) | Migalha da trilha que levou até o erro | Marcar cada passo relevante do robô |
22
- | [Transaction](#transaction) | Unidade de trabalho nomeada, com duração | Medir quanto uma rotina inteira demorou |
23
- | [Step](#step) | Uma etapa medida dentro da transaction | Descobrir **onde** o tempo foi gasto |
24
- | [ctx.tx](#transaction-de-run-ctxtx) | A transaction que o SDK já abriu para a run | Medir as etapas de um handler de task |
25
- | [Amostragem](#amostragem-samplerate-e-tracessamplerate) | Enviar só uma fração do que acontece | Volume alto demais para enviar tudo |
26
- | [Heartbeat](#heartbeat) | Batimento "estou vivo" | Saber que o robô está de pé, mesmo parado |
27
- | [instanceId](#instanceid) | A identidade da instância do robô | Sempre — é obrigatório no `init()` |
28
- | [Release e environment](#release-e-environment) | Qual versão, em qual ambiente | Separar produção de homologação; achar a versão que quebrou |
29
- | [Run](#run) | Uma execução solicitada no app | As tasks inteiras giram em volta disso |
30
- | [Claim](#claim) | O ato de um agente pegar uma run para si | Entender por que dois agentes nunca colidem |
31
- | [Drain mode](#drain-mode) | O agente acelera o poll enquanto tem trabalho | Entender o ritmo do polling |
32
- | [Fila, flush e close](#fila-flush-e-close) | Nada é enviado na hora — vai para uma fila | Encerrar o processo sem perder telemetria |
33
- | [beforeSend](#beforesend) | O último gancho antes do envio | Censurar ou descartar eventos |
34
-
35
- > **Nota de manutenção:** esta página é a fonte canônica das explicações de conceito. As outras páginas da doc, o `README.md` do pacote e os tooltips do app parafraseiam daqui — quando um conceito muda, muda aqui primeiro.
9
+ | Conceito | Em uma frase | Quando você usa |
10
+ | --- | --- | --- |
11
+ | [Task](#task) | O que o robô sabe fazer, por slug | `hobots.register` / `hobots.start` |
12
+ | [Modo sob demanda](#modo-sob-demanda-register) | Uma execução por solicitação do app | O trabalho é disparado por alguém (formulário, agendamento) |
13
+ | [Modo sempre ligado](#modo-sempre-ligado-start) | O robô abre a própria execução e roda o seu loop | O robô vive rodando e você só quer ver o que ele faz |
14
+ | [Execução (run)](#execução-run) | Uma rodada de uma task, com logs, itens e resultado | A unidade de tudo em Solicitações |
15
+ | [Item](#item) | Uma unidade de trabalho entregue | Cada nota, boleto, arquivo processado |
16
+ | [Progresso](#progresso) | Quantos de quantos | Barra e tempo restante no app |
17
+ | [Log da execução](#log-da-execução) | Narração do que a execução fez | `hobots.log` — nunca lança |
18
+ | [Anexo](#anexo) | Um print pendurado numa linha de log | "Como estava a tela?" |
19
+ | [Cancelamento cooperativo](#cancelamento-cooperativo) | O app pede, o handler para no próximo ponto de parada | Automático em items/progress/steps/sleep/retry |
20
+ | [Retry](#retry) | Tentar de novo na mesma execução | Instabilidade passageira |
21
+ | [Evento e issue](#evento-e-issue) | A foto de um erro, agrupada em issue | `hobots.captureException` e erros não tratados |
22
+ | [Transaction](#transaction) e [Step](#step) | Quanto demorou e onde o tempo foi | `hobots.transaction.startChild` |
23
+ | [Heartbeat](#heartbeat) | Batimento "estou vivo", sempre ligado | Saber que o robô está de pé |
24
+ | [instanceId](#instanceid) | A identidade da instância do robô | Env `HOBOTS_INSTANCE_ID` |
25
+ | [Release e environment](#release-e-environment) | Qual versão, em qual ambiente | Separar produção de homologação |
26
+ | [Claim](#claim) | O ato de um agente pegar uma execução | Por que dois agentes nunca colidem |
27
+ | [Drain mode](#drain-mode) | O agente acelera o poll enquanto tem trabalho | O ritmo do polling |
28
+ | [close](#close) | Encerrar sem perder nada | A última linha do programa |
36
29
 
37
30
  ## O SDK e os módulos do app
38
31
 
39
- Cada API do SDK alimenta um módulo do app da Hobots (app.hobots.app). O mapa:
40
-
41
32
  | No SDK | No app |
42
33
  | --- | --- |
43
- | **Agente** — dono do Client Secret (`hb_…`) que autentica tudo | **Configurações → Agentes** (é lá que você copia a credencial) |
44
- | **Instância** — o par `(agent_id, instance_id)`; heartbeat e poll de tasks convergem nele | Módulo **Instâncias** (uptime, performance) |
45
- | **[Evento](#evento)** agrupado em **issue** — `captureException` / `captureMessage` | Módulo **Issues** |
46
- | **[Transaction](#transaction) / etapas** — `startTransaction` (e [`ctx.tx`](#transaction-de-run-ctxtx) dentro de uma run) | Performance no módulo **Instâncias**; nas execuções, a aba de performance da solicitação |
47
- | **Task / [run](#run)** — `register('task-slug', …)` casa com o `task_slug` do formulário | Módulo **Solicitações** (formulários em **Configurações → Formulários**) |
34
+ | **Agente** — dono do Client Secret (`hb_…`) que autentica tudo | **Configurações → Agentes** |
35
+ | **Instância** — o par `(agente, instanceId)`; heartbeat e tasks convergem nela | Módulo **Instâncias** (uptime, recursos da máquina, performance) |
36
+ | **Task / execução** — `register('slug', …)` casa com o `task_slug` do formulário; `start('slug', …)` abre a própria execução | Módulo **Solicitações** (formulários em **Configurações → Formulários**) |
37
+ | **Evento** agrupado em **issue** — `captureException` e erros não tratados | Módulo **Issues** |
38
+ | **Transaction / steps** — `hobots.transaction` | Aba de performance da execução |
48
39
 
49
40
  ---
50
41
 
51
- ## Evento
52
-
53
- Um **evento** é a foto de um instante que vale a pena registrar: uma exceção que estourou ou uma mensagem que você mandou. É a unidade básica da telemetria.
54
-
55
- Quando você chama `captureException` ou `captureMessage`, o SDK monta o evento com o que a exceção traz (tipo, mensagem, stack trace com os frames em ordem) e **anexa tudo que está no [scope](#scope)** naquele momento: tags, usuário, contexts, extras e a trilha de [breadcrumbs](#breadcrumb). Depois disso o evento vai para a [fila](#fila-flush-e-close) e é enviado em lote.
56
-
57
- No servidor, eventos parecidos são agrupados numa **issue** — o que você vê no módulo Issues do app. É por isso que 4.000 falhas do mesmo timeout não viram 4.000 linhas na sua tela. Quem decide o que é "parecido" é o [fingerprint](#fingerprint).
58
-
59
- ```js
60
- Hobots.captureException(error);
61
- Hobots.captureMessage('Lote processado com sucesso', 'success');
62
- ```
63
-
64
- As duas devolvem o `event_id` (um uuid), útil para você logar junto e depois achar o evento no app.
65
-
66
- > **Atenção:** o `event_id` devolvido **não é confirmação de envio**. Se o evento foi descartado por [amostragem](#amostragem-samplerate-e-tracessamplerate) ou por [`beforeSend`](#beforesend), você recebe um id normalmente e nada foi enfileirado. Elas devolvem `undefined` só quando o `init()` nunca foi chamado.
42
+ ## Task
67
43
 
68
- ## Type (tipo do evento)
44
+ Uma **task** é uma capacidade nomeada do robô — "processar NF-e", "monitorar a pasta de entrada" — identificada por um **slug** estável. Registrar uma task é a única forma de configurar o SDK: não há `init()`. A primeira chamada a `register()`/`start()` lê as credenciais do ambiente, liga o heartbeat e a captura de erros, e define as opções do processo (`environment`, `release`, `tags`…). Um slug só pode ser registrado uma vez.
69
45
 
70
- O **type** é o que o evento é. São quatro: `fatal`, `error`, `warning` e `success`.
46
+ ## Modo sob demanda (`register`)
71
47
 
72
- Serve para você separar o que precisa acordar alguém do que é só registro. `error` e `fatal` são problemas (e são os únicos que disparam alerta); `warning` é algo que ainda funciona mas merece olhar — aparece nas issues, sem e-mail.
48
+ O robô fica **online aguardando**: faz polling na plataforma e, quando alguém cria uma solicitação para aquele slug (pelo formulário ou por um agendamento), executa o handler com os `params` do formulário. É o modo para trabalho que **alguém pede**. Várias solicitações podem rodar em paralelo (`concurrency`).
73
49
 
74
- O `success` é o mais incomum e o mais útil no dia a dia de RPA: ele marca uma conclusão **bem-sucedida**. Conta como evento e aparece na timeline do robô, mas **não gera issue nem alerta** — é o jeito de registrar "as 120 notas do dia foram emitidas" sem que isso pareça um problema.
50
+ ## Modo sempre ligado (`start`)
75
51
 
76
- Não existe tipo `info` nem `debug`: narração não é problema e não abre issue. Para isso existem o [log da execução](./tasks.md#logs) (`ctx.log.debug/info/...`) e as [breadcrumbs](#breadcrumb), que têm o seu próprio nível.
52
+ O robô **não espera ninguém**: ao iniciar, abre a própria execução na plataforma (`source: agent`, sem formulário nem solicitante) e roda o handler dentro dela. O handler é o seu loop — um `while (true)` que vigia uma pasta, uma fila, um portal. A plataforma recebe logs, itens e progresso enquanto ele roda. Quando o handler retorna, a execução fecha; o processo continua vivo pelo heartbeat, então a instância segue online até `hobots.close()`. É **uma execução por processo**: para recomeçar, reinicie o robô. Como o servidor guarda no máximo 1000 linhas de log por execução, o trabalho feito vai em itens, não em log.
77
53
 
78
- A precedência é: o `type` do `hint` que você passou → o tipo que a função de captura já definiu → `error`. Na prática:
54
+ ## Execução (run)
79
55
 
80
- - `captureException(error)` sem `hint` é sempre `error`.
81
- - `captureMessage(msg)` sem tipo explícito é sempre `warning`.
82
- - Para qualquer outra coisa, passe o tipo: `captureException(error, { type: 'warning' })`.
56
+ Uma **execução** é uma rodada concreta de uma task — no app, uma solicitação. Ciclo de vida no servidor:
83
57
 
84
- > **Atenção:** `scope.setType()` (dentro de um [`withScope`](#withscope)) **não afeta** `captureException` nem `captureMessage` — as duas já definem o tipo do evento antes de o scope ser consultado. Para mudar o tipo, use o `hint` (`{ type: … }`) ou o segundo argumento do `captureMessage`.
85
-
86
- ## Fingerprint
87
-
88
- O **fingerprint** é a chave de agrupamento: eventos com o mesmo fingerprint caem na **mesma issue**.
89
-
90
- Quando você não passa nada, o **servidor** calcula o agrupamento pelo stack trace. Isso funciona bem na maioria dos casos e falha em dois cenários opostos:
91
-
92
- - **Uma causa, muitos stacks.** O portal da SEFAZ dá timeout em cinco pontos diferentes do seu código. São cinco issues para o mesmo problema. Passe `fingerprint: ['sefaz-timeout']` e vire uma.
93
- - **Um stack, muitas causas.** Todo erro passa pelo seu `executarPasso()` genérico, então tudo agrupa junto e você não distingue nada. Inclua algo que diferencie: `fingerprint: ['passo-falhou', nomeDoPasso]`.
94
-
95
- ```js
96
- Hobots.captureException(error, { fingerprint: ['sefaz-timeout'] });
97
58
  ```
98
-
99
- O fingerprint é um array de strings — o servidor concatena os pedaços. Precedência: o do `hint` → o do evento → o do [scope](#scope).
100
-
101
- > **Atenção:** fingerprint é agrupamento, não filtro. Se o que você quer é **procurar depois** por cliente ou região, isso é [tag](#tag). Fingerprint com valor de alta cardinalidade (um id de nota, um timestamp) cria uma issue nova a cada evento e destrói o agrupamento.
102
-
103
- ## Scope
104
-
105
- O **scope** é a mochila de contexto do SDK: tudo que ele sabe sobre o estado atual do seu programa e que deve viajar junto do próximo evento. Dentro dela: [tags](#tag), [usuário](#scope), [contexts](#context), [extras](#extra), tipo, [fingerprint](#fingerprint) e a lista de [breadcrumbs](#breadcrumb).
106
-
107
- Você não manipula o scope direto — você usa os `set*`, que escrevem nele:
108
-
109
- ```js
110
- Hobots.setTag('regiao', 'nordeste');
111
- Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
112
- Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
113
- Hobots.setExtra('ultimoLote', 128);
114
- ```
115
-
116
- A partir daí, **todo** evento capturado leva esses dados. É a diferença entre repetir os mesmos campos em vinte chamadas de `captureException` e dizer uma vez, no começo do programa.
117
-
118
- Dois pontos importantes:
119
-
120
- - **Existe um scope global, e só um.** Tudo que você escreve nele fica até você sobrescrever. Para contexto temporário, use [`withScope`](#withscope).
121
- - **`init()` não limpa o scope.** Chamar `Hobots.init()` de novo troca o cliente, mas as tags, o usuário e os breadcrumbs que você já tinha escrito continuam lá.
122
-
123
- ## withScope
124
-
125
- `withScope` executa um callback com uma **cópia isolada** do scope. O que você escrever lá dentro vale para os eventos capturados dentro do callback e **não vaza** para fora.
126
-
127
- ```js
128
- Hobots.withScope(scope => {
129
- scope.setTag('lote', '128').setFingerprint(['reprocessamento']);
130
- Hobots.captureMessage('Reprocessando lote 128');
131
- });
132
- // aqui a tag 'lote' e o fingerprint não existem mais
133
- ```
134
-
135
- Todos os métodos do scope são encadeáveis. Use quando o contexto é de um trecho, não do programa: um lote específico, um cliente específico, um retry.
136
-
137
- > **Atenção:** `withScope` é **síncrono** — ele troca o scope global, roda o callback e restaura no `finally`. Se você der `await` dentro do callback, o scope isolado continua valendo para **qualquer** código que capturar eventos enquanto aquele `await` está pendente. Em código concorrente (várias runs em paralelo, por exemplo), prefira passar as tags no `hint` de cada captura: `captureException(error, { tags: { lote: '128' } })`.
138
-
139
- ## Tag
140
-
141
- Uma **tag** é um rótulo curto e **indexável**: chave e valor, os dois `string`. É o que você usa para **filtrar** e **agrupar** no app — "só os erros do cliente acme", "quantos eventos por região".
142
-
143
- ```js
144
- Hobots.setTag('regiao', 'nordeste');
145
- Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
146
- ```
147
-
148
- Porque é indexada, tag pede **cardinalidade baixa**: `cliente`, `regiao`, `ambiente`, `versao-do-portal` são boas tags. Número de nota fiscal, id de run e timestamp são péssimas — cada valor vira uma entrada nova no índice e a filtragem perde sentido. Esses vão em [extra](#extra) ou [context](#context).
149
-
150
- Tags podem vir de quatro lugares, e em caso de conflito a última vence:
151
-
152
- `init({ tags })` → [scope](#scope) → o próprio evento → o `hint` da chamada
153
-
154
- Ou seja: uma tag no `hint` sobrescreve a mesma tag do scope, que sobrescreve a do `init()`. Use o `init()` para o que é verdade em todo o processo (squad, ambiente), o scope para o que é verdade num trecho, e o `hint` para o que é verdade só naquele evento.
155
-
156
- > **Atenção:** tags do scope entram em **eventos**, não em [transactions](#transaction). Uma transaction só leva as tags do `init()` mais as que você marcar nela com `tx.setTag()`. [Heartbeats](#heartbeat) não levam tag nenhuma.
157
-
158
- ## Context
159
-
160
- Um **context** é um grupo **nomeado** de dados estruturados anexado ao evento. Diferente de [tag](#tag), context não serve para filtrar — serve para **ler** quando você abre o evento e quer entender o ambiente em que ele aconteceu.
161
-
162
- ```js
163
- Hobots.setContext('vm', { cpu: 4, ram: '8GB', disco: '40GB' });
164
- Hobots.setContext('portal', { nome: 'SEFAZ-CE', versao: '2.4', homologacao: false });
165
- ```
166
-
167
- O nome do grupo (`'vm'`, `'portal'`) é o que organiza a visualização: cada context aparece como um bloco separado no evento, em vez de virar uma sopa de campos soltos. É a escolha certa quando os dados **andam juntos** e fazem sentido como conjunto.
168
-
169
- O SDK já injeta dois contexts sozinho em todo evento: `runtime` (nome e versão do Node, ou `browser`) e `os` (plataforma e arquitetura). Você pode sobrescrevê-los usando os mesmos nomes, mas normalmente não quer.
170
-
171
- Para **remover** um grupo, passe `null`:
172
-
173
- ```js
174
- Hobots.setContext('portal', null); // apaga o grupo inteiro
59
+ pending → claimed → running → succeeded | failed | canceled
175
60
  ```
176
61
 
177
- ## Extra
62
+ No modo sob demanda a execução nasce `pending` (alguém pediu), vira `claimed` quando um agente a [reivindica](#claim) e `running` quando o handler começa. No modo sempre ligado ela já nasce `running`, criada pelo próprio robô. O agente só transiciona para `running`, `succeeded` e `failed`; `canceled` vem de alguém cancelando no app. Cada execução tem a sua transaction de performance, seus logs, itens e progresso — e o objeto `hobots` aponta para a execução **atual**, em qualquer módulo, graças ao contexto async do Node.
178
63
 
179
- **Extra** é o depósito livre: chave → qualquer valor, sem grupo e sem tipo. Vai junto do evento e você lê no app.
64
+ ## Item
180
65
 
181
- ```js
182
- Hobots.setExtra('ultimoLote', 128);
183
- Hobots.setExtra('tentativas', [1, 2, 3]);
184
- ```
185
-
186
- É deliberadamente o menos estruturado dos três, e por isso o critério de escolha é simples:
66
+ Um **item** é uma unidade de trabalho que a execução entregou: uma nota baixada, um boleto emitido, um arquivo movido. Tem status (`succeeded`, `failed` = falha técnica, `occurrence` = ocorrência de negócio, como "nota não encontrada") e um `payload` de atributos livres que viram colunas na aba de itens da solicitação. O `id` é a identidade de negócio do item na execução — repetido, é ignorado: é isso que torna retries idempotentes. Diferente do log, o item é **dado estruturado**: dá para contar, filtrar e conferir.
187
67
 
188
- | Você vai… | Use |
189
- | ------------------------------------- | ------------------- |
190
- | filtrar, agrupar, contar por isso | [tag](#tag) |
191
- | ler isso como um conjunto coerente | [context](#context) |
192
- | nenhum dos dois — é só um dado a mais | **extra** |
68
+ ## Progresso
193
69
 
194
- Extra não é indexado: não dá para filtrar por ele nem construir gráfico com ele. Se você se pegar querendo isso, o campo era uma tag.
70
+ O **progresso** é "quantos de quantos" (`current/total`). Declare o total antes do loop e avance a cada item; o app mostra barra, percentual e tempo restante estimado. É um snapshot absoluto que pega carona nos outros envios — não gera tráfego próprio.
195
71
 
196
- > **Dados sensíveis:** extras, contexts, tags e breadcrumbs **vão todos para o servidor** e ficam visíveis no evento. Não coloque senha, token, CPF, CNPJ de cliente final ou conteúdo de documento em nenhum deles.
72
+ ## Log da execução
197
73
 
198
- ## Breadcrumb
74
+ O **log da execução** narra o que aconteceu, em linhas com nível (`debug`, `info`, `warning`, `error`, `success`), enviadas incrementalmente e visíveis no app quase em tempo real. Ele é o único membro de `hobots` que **nunca lança**, justamente para poder ir num `finally` de limpeza. O servidor guarda até 1000 linhas por execução.
199
75
 
200
- Um **breadcrumb** ("migalha de pão") é uma anotação curta de algo que o robô fez — baixou um arquivo, clicou num botão, chamou uma API. Sozinho não vale nada: ele existe para ser lido **depois**, quando um erro acontece.
76
+ ## Anexo
201
77
 
202
- O SDK mantém em memória a lista dos últimos breadcrumbs. Quando você captura um erro, a lista viaja **junto** com o evento — então em vez de ver só `TimeoutError`, você vê os oito passos que levaram até lá. É a diferença entre "quebrou" e "quebrou depois de baixar 3 notas e tentar a quarta duas vezes".
78
+ Um **anexo** é uma imagem — na prática, um print — vinculada a uma linha de log. Responde "como estava a tela?" quando texto não basta. Quem captura é o seu código (Playwright, Puppeteer, um arquivo); o SDK sobe direto para o S3 por URL presignada, em segundo plano, sem atrasar o log. Enquanto sobe, o app mostra "enviando imagem…".
203
79
 
204
- ```js
205
- Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
206
- ```
80
+ ## Cancelamento cooperativo
207
81
 
208
- Todos os campos são opcionais: `category` agrupa a origem, `message` é o texto lido por humano, `level` (`debug`, `info`, `warning`, `error`, `fatal`) colore a linha na timeline, `data` carrega um objeto livre, `type` classifica a migalha. O `timestamp` é preenchido pelo SDK.
82
+ Cancelar no app **não mata** o handler — o SDK não tem como interromper código arbitrário com segurança (um navegador aberto, uma transação pela metade). O que ele faz: detecta o cancelamento (o PATCH periódico recebe 409), dispara `hobots.signal` e faz o próximo **ponto de parada** lançar `RunCanceled`. Os pontos de parada são as fronteiras naturais de trabalho: registrar um item, avançar o progresso, abrir uma step, dormir, tentar de novo. Por isso, num loop comum, o cancelamento funciona sem nenhuma linha extra. `hobots.log` nunca é ponto de parada. Se o handler ignorar o signal por tempo demais (`cancelGraceMs`), o SDK libera o slot e o handler vira um zumbi cujos reportes são descartados.
209
83
 
210
- Dois pontos que costumam surpreender:
84
+ ## Retry
211
85
 
212
- - **Breadcrumbs não são consumidos.** Enviar um evento **não** limpa a lista — o próximo evento leva as mesmas migalhas de novo, mais as novas. A lista só encolhe quando estoura o `maxBreadcrumbs`, e aí as **mais antigas** são descartadas.
213
- - **A lista é global.** `Hobots.addBreadcrumb()` escreve no [scope](#scope) global. Para uma trilha que não vaze para o resto do programa, adicione dentro de um [`withScope`](#withscope) — ou limpe com `scope.clearBreadcrumbs()`.
86
+ Uma falha do handler costuma ser passageira (portal instável, rede). O SDK **tenta de novo na mesma execução**: aviso no log, espera, mesmos `params`. O que já foi enviado (itens, progresso) fica — a execução é a história completa, incluindo a tentativa que falhou. Por isso o handler precisa ser idempotente. Cancelamento e timeout são terminais: não faz sentido insistir. Para uma chamada específica, `hobots.retry()` aplica a mesma lógica no miúdo, ciente do cancelamento.
214
87
 
215
- > **Dados sensíveis:** breadcrumbs vão para o servidor e ficam visíveis na issue. Não coloque senha, token, CPF ou conteúdo de documento em `message` nem em `data`.
88
+ ## Evento e issue
216
89
 
217
- Tamanho do buffer: [`maxBreadcrumbs`](./api-reference.md#initoptions).
90
+ Um **evento** é a foto de um erro: tipo, mensagem, stack trace, tags, ambiente, release e como estava a máquina. `hobots.captureException(erro)` cria um; erros não tratados (`uncaughtException`, `unhandledRejection`) também. Eventos parecidos são agrupados em uma **issue** pelo servidor (por stack trace, ou pelo `fingerprint` que você definir). O `type` diz o que ele é: `fatal`, `error` e `warning` abrem issue e podem alertar; `success` é registrado e nunca alerta. Log não é evento — narração fica no log da execução.
218
91
 
219
92
  ## Transaction
220
93
 
221
- Uma **transaction** é uma unidade de trabalho **nomeada** e **medida**: ela começa, termina, e o que fica registrado é quanto tempo levou. É a resposta para "quanto demora minha rotina de emissão?" — e, quando você adiciona [etapas](#step), para "onde foram esses minutos?".
222
-
223
- Enquanto o [evento](#evento) responde _o que quebrou_, a transaction responde _quanto tempo levou_. São os dois eixos da telemetria.
224
-
225
- ```js
226
- const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
227
- await processarLote();
228
- tx.setStatus('ok'); // ou 'error'
229
- tx.finish();
230
- ```
231
-
232
- O primeiro argumento é o **nome** (o que agrupa transactions comparáveis no app — use algo estável, não um id) e o segundo é o **`op`**, uma categoria livre do tipo de operação (`rpa.job`, `http.client`, `db.query`).
233
-
234
- Três coisas que valem saber:
235
-
236
- - **Nada é enviado até o `finish()`.** A transaction vive em memória; o `finish()` é que monta o payload e enfileira. Se o processo morrer antes, ela simplesmente não existe.
237
- - **`finish()` é idempotente e fecha o resto.** Chamar duas vezes não envia duas; e ele **finaliza automaticamente** todas as etapas que ficaram abertas.
238
- - **A [amostragem](#amostragem-samplerate-e-tracessamplerate) é decidida no `startTransaction()`**, não no `finish()`. Se aquela transaction não foi sorteada, o `finish()` não envia nada — mas os `startChild` continuam funcionando normalmente e não custam quase nada.
239
-
240
- O `status` default é `'ok'`; encadeáveis: `setStatus`, `setName`, `setTag`.
241
-
242
- Dentro de um handler de task você **não** cria transaction — já existe uma em [`ctx.tx`](#transaction-de-run-ctxtx), e o `startTransaction()` chamado lá dentro devolve exatamente ela.
94
+ Uma **transaction** é uma unidade de trabalho **medida**: começa, termina, e o que fica é quanto tempo levou. Cada execução tem exatamente uma, aberta e finalizada pelo SDK (`hobots.transaction`), correlacionada à execução pelo `run_id`. Você não cria transactions — só adiciona etapas.
243
95
 
244
96
  ## Step
245
97
 
246
- Uma **step** (etapa) é medida **dentro** de uma [transaction](#transaction). A transaction diz que a rotina levou 4 minutos; as etapas dizem que 3min40 foram esperando o portal.
247
-
248
- ```js
249
- const step = tx.startChild('http.client', 'GET /api/notas');
250
- await api.buscarNotas();
251
- step.setData('notas', 42).finish();
252
- ```
253
-
254
- `startChild(op?, description?)` existe **tanto na transaction quanto na step**, então você aninha quanto quiser — uma step de "processar nota" com filhas de "baixar XML" e "gravar no banco". No app isso vira o **waterfall**: barras encaixadas, cada uma com sua duração.
255
-
256
- O `op` é a categoria (útil para agrupar "todas as queries") e a `description` é o detalhe humano ("`INSERT INTO notas`"). `setData(chave, valor)` anexa dados àquela step específica — contagens, tamanhos, ids.
257
-
258
- - Métodos encadeáveis: `setStatus`, `setData`, `finish`.
259
- - **Status da step:** `'ok'` (default), `'warning'` (terminou, mas merece atenção — não é falha) ou `'error'` (falhou). No waterfall do app cada um tem sua cor: verde/neutro, âmbar e vermelho. Use `step.setStatus('warning')` para etapas que passaram com ressalva (ex.: retry que deu certo, resultado parcial).
260
- - Esquecer o `finish()` de uma step **não perde o dado**: o `finish()` da transaction fecha as etapas abertas, marcando o fim naquele instante.
261
- - As etapas são guardadas numa lista plana ligada por `parent_step_id` — o aninhamento que você vê no app é reconstruído a partir dessas referências, não da ordem das chamadas.
262
-
263
- ## Transaction de run (ctx.tx)
264
-
265
- Dentro de um handler de task você **não precisa** criar uma transaction: o SDK já abriu uma para aquela execução e entregou em `ctx.tx`. Ela nasce quando o agente captura a run, é **correlacionada** à execução pelo `run_id`, recebe o status conforme o desfecho (`ok`, `error` ou `cancelled`) e é **finalizada automaticamente** quando o handler termina — você nunca chama `ctx.tx.finish()`.
266
-
267
- O que sobra para você é a parte interessante: marcar as **etapas** do trabalho com [steps](#step). Cada `startChild` vira uma barra no waterfall da aba de performance daquela execução no app.
268
-
269
- ```js
270
- Hobots.register('processar-nfe', async (params, ctx) => {
271
- const login = ctx.tx.startChild('browser', 'login no portal');
272
- await entrar();
273
- login.finish();
274
-
275
- for (const nota of params.notas) {
276
- const step = ctx.tx.startChild('nfe', `nota ${nota.numero}`);
277
- ctx.throwIfCanceled();
278
- await processar(nota);
279
- step.setData('valor', nota.valor).finish();
280
- }
281
- });
282
- ```
283
-
284
- Sem nenhum `startChild` a transaction ainda existe e ainda mede o tempo total da run — o waterfall só fica com uma barra. Cada step que você abre é uma resposta a mais para "onde foram os 4 minutos".
285
-
286
- - **`Hobots.startTransaction()` dentro de um handler devolve a própria `ctx.tx`.** Cada execução tem exatamente **uma** transaction: o SDK detecta o contexto da run e reutiliza a transaction dela em vez de criar uma segunda — as etapas caem no mesmo waterfall. Fora de um handler, `startTransaction()` cria uma transaction nova, sem `run_id`.
287
- - `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale para as transactions que **você** abre com `startTransaction()` fora de uma run.
288
- - O nome é `run:<task>` e o `op` é `hobots.task`, preenchidos pelo SDK.
289
-
290
- ## Amostragem (sampleRate e tracesSampleRate)
291
-
292
- **Amostrar** é enviar só uma fração do que acontece. Existe porque volume alto custa banda, custa armazenamento e não melhora a conclusão: para saber que uma rotina leva ~4 minutos, 10% das execuções contam a mesma história que 100%.
293
-
294
- São dois controles independentes, os dois de `0` a `1`:
295
-
296
- | Opção | Afeta | `0.1` significa |
297
- | ------------------ | ------------------------------------------------------------- | ---------------------------------- |
298
- | `sampleRate` | [eventos](#evento) (`captureException`, `captureMessage`) | ~10% dos erros são enviados |
299
- | `tracesSampleRate` | [transactions](#transaction) abertas por `startTransaction()` | ~10% das transactions são enviadas |
300
-
301
- O sorteio é por item, no momento em que ele é criado — nunca "1 a cada 10", e sim 10% de chance cada.
302
-
303
- Quem **nunca** é amostrado: [heartbeats](#heartbeat) e a [transaction de run](#transaction-de-run-ctxtx) (`ctx.tx`). São sinais de vida — amostrar transformaria "o robô está offline" num palpite.
304
-
305
- > **Atenção:** para erros, comece com `sampleRate` em 1 (o default) e só reduza se o volume virar problema de verdade. Amostrar erro significa não ver a falha que aconteceu três vezes — e três vezes é exatamente onde você ainda consegue agir. Para transactions o cálculo é o oposto: reduzir é quase sempre seguro.
306
-
307
- Nos dois casos, o descarte é silencioso do ponto de vista do seu código: `captureException` ainda devolve um `event_id`. Veja a ressalva em [Evento](#evento).
98
+ Uma **step** é uma etapa medida dentro da transaction. A transaction diz que a execução levou 4 minutos; as steps dizem que 3min40 foram esperando o portal. `hobots.transaction.startChild(op, descrição)` abre uma; `step.startChild` aninha; `setData` anexa números; `finish()` fecha (steps abertas são fechadas com a transaction). Status `ok`, `warning` (passou com ressalva) ou `error`. No app isso vira o **waterfall** da execução. Abrir uma step é um ponto de parada cooperativo.
308
99
 
309
100
  ## Heartbeat
310
101
 
311
- O **heartbeat** é um batimento periódico dizendo "estou vivo". O servidor conhece a cadência esperada e marca a instância como **offline** quando os batimentos param.
312
-
313
- Isso responde a uma pergunta que erro e performance não respondem: **"o robô está de pé?"**. Um robô que fica de plantão esperando trabalho pode passar horas sem lançar exceção — o silêncio é o sintoma, e heartbeat é como o silêncio fica visível.
314
-
315
- ```js
316
- Hobots.init({
317
- clientSecret,
318
- instanceId: 'vm-cliente-01',
319
- heartbeat: { intervalMs: 60_000, name: 'Robô NFe — VM 01' },
320
- });
321
- ```
322
-
323
- `heartbeat: true` usa os defaults. A identidade é sempre o [`instanceId`](#instanceid) do `init()`; o `name` é só o rótulo amigável no dashboard.
324
-
325
- Dois detalhes:
326
-
327
- - O primeiro batimento sai **na hora** em que o `init()` roda — não depois do primeiro intervalo. O robô aparece online imediatamente.
328
- - O batimento carrega o próprio intervalo configurado. É assim que o servidor sabe quanto silêncio é normal antes de declarar offline: quem bate a cada meio minuto é cobrado em outro ritmo de quem bate a cada cinco.
329
-
330
- O timer do heartbeat é `unref`'d — ele **não** segura o processo vivo.
331
-
332
- Com `tasks: true`, o heartbeat é **opcional**: o próprio poll de tasks já marca a presença do agente no painel. Ele continua essencial para agentes só de telemetria.
102
+ O **heartbeat** é o batimento "estou vivo", a cada 30 s. O servidor conhece a cadência e marca a instância **offline** quando ele para — a resposta para "o robô está de pé?", que erro e performance não respondem. Ele é sempre ligado e o seu timer **segura o processo vivo**: é o que mantém um robô do modo sempre ligado rodando depois que o handler retorna. `hobots.close()` o para. O primeiro batimento sai no boot; cada um leva os recursos da máquina (CPU, memória, disco).
333
103
 
334
104
  ## instanceId
335
105
 
336
- O **`instanceId`** é a identidade da instância do robô: qual VM, qual container, qual máquina. É **obrigatório** no `init()` e deve ser **estável entre reinícios** (`vm-cliente-01`, `container-nfe-3`) — se você gerar um valor novo a cada boot, o app vê uma frota de robôs fantasmas em vez de um robô que reiniciou.
337
-
338
- O que o torna especial é que ele é a identidade dos **dois** canais ao mesmo tempo: o [heartbeat](#heartbeat) e o agente de [tasks](./tasks.md) enviam o mesmo `instanceId`. O servidor correlaciona os dois, e o robô aparece como **um agente só** no módulo Agentes — sem nenhuma configuração extra.
106
+ A identidade da **instância** do robô: qual VM, qual container. Vem da env `HOBOTS_INSTANCE_ID` (ou da opção; sem os dois, o hostname) e deve ser **estável entre reinícios** — um valor novo a cada boot vira uma frota de robôs fantasmas. É a mesma identidade no heartbeat, no poll e na criação de execuções: um agente só no painel.
339
107
 
340
108
  ## Release e environment
341
109
 
342
- **`release`** é a versão do seu programa (`robo-nfe@1.4.2`) e **`environment`** é o ambiente lógico, um de quatro valores: `development`, `testing`, `staging` (homologação) e `production`. Os dois são anexados a **todo** item enviado: eventos e transactions — e o `environment` vai também na identidade da **instância**, pelo heartbeat e pelo poll.
343
-
344
- Servem a duas perguntas concretas:
345
-
346
- - _"quebrou desde quando?"_ — com `release`, você compara o antes e o depois de um deploy e sabe qual versão introduziu o erro.
347
- - _"isso é real ou é teste?"_ — com `environment`, o ruído da homologação não polui os alertas de produção.
348
-
349
- `environment` default é `production`. Se você roda homologação sem definir isso, os dois ambientes se misturam na mesma issue — vale configurar no primeiro dia.
350
-
351
- E não é só organização: é o `environment` que decide **quem é notificado**. Cada regra de alerta lista os ambientes em que dispara, e por padrão só produção — um erro de homologação não acorda a lista de plantão, e uma máquina de testes que desliga no fim do expediente não vira alerta de madrugada. Para ser avisado em outro ambiente, marque-o na regra em **Alertas**.
352
-
353
- Valores antigos de texto livre (`prod`, `qa`, `homologacao`) continuam sendo aceitos — o servidor os classifica num dos quatro; o que ele não reconhece vira produção, para nunca silenciar um alerta por engano. O ambiente de uma instância também pode ser corrigido no app, na página dela: a partir daí ele ignora o que o robô declara, até você desfazer.
354
-
355
- ## Run
356
-
357
- Uma **run** (execução) é um pedido concreto de trabalho criado no app: alguém preencheu o formulário da task, ou uma automação disparou, e agora existe uma solicitação esperando um agente pegar.
358
-
359
- É a unidade em volta da qual as [tasks](./tasks.md) inteiras giram — no app, cada run é uma solicitação no módulo **Solicitações**. O ciclo de vida no servidor:
360
-
361
- ```
362
- pending → claimed → running → succeeded | failed | canceled
363
- ```
364
-
365
- - **`pending`** — criada, esperando um agente.
366
- - **`claimed`** — um agente reivindicou ([claim](#claim)), mas o handler ainda não confirmou o início.
367
- - **`running`** — o handler está executando. Daqui para frente o agente reporta logs incrementais.
368
- - **`succeeded` / `failed` / `canceled`** — terminal.
369
-
370
- O agente só transiciona para `running`, `succeeded` e `failed`. `pending` e `claimed` são do servidor; `canceled` vem de alguém cancelando no app.
371
-
372
- Cada run carrega os **`params`** (o JSON validado do formulário) e chega ao seu código como o primeiro argumento do handler, junto de um [`RunContext`](./api-reference.md#taskhandler--runcontext) com logs, sinal de cancelamento e a [transaction](#transaction-de-run-ctxtx).
373
-
374
- ## Anexo
375
-
376
- Um **anexo** é uma imagem — na prática, um print da tela — vinculada a uma linha de log de uma [run](#run). Serve para responder "como estava a tela quando isso aconteceu?", pergunta que texto de log sozinho não responde. No app, aparece como miniatura logo abaixo da linha de log correspondente, na aba "logs do agente" da execução.
377
-
378
- Quem captura é o seu código; o SDK só recebe a imagem pronta, via o campo [`attachment`](./tasks.md#anexos-screenshots) de `captureException`/`captureMessage`.
379
-
380
- **O binário não passa pela API do Hobots.** O agente pede uma URL presignada, sobe direto no S3 e depois confirma. Isso mantém a API livre de tráfego pesado e permite imagens bem maiores do que o limite de corpo de um PATCH. O ciclo de vida de um anexo é:
381
-
382
- ```
383
- pending → uploaded
384
- ↘ failed
385
- ```
386
-
387
- O `pending` existe porque a linha de log é registrada **antes** do upload terminar — é isso que impede o print de atrasar o log ou bagunçar a ordem. Enquanto o anexo está `pending`, o app mostra a miniatura como "enviando imagem…".
388
-
389
- Anexos herdam o mesmo gate de visibilidade dos logs: solicitantes (papel `requester`) só os veem quando o projeto está com "mostrar logs para solicitantes" ligado.
110
+ `release` é a versão do programa (`robo-nfe@1.4.2`); `environment` é o ambiente lógico (`development`, `testing`, `staging`, `production`). Vão em todo evento e transaction e na identidade da instância. `release` responde "quebrou desde quando?"; `environment` separa o ruído da homologação dos alertas de produção — cada regra de alerta lista em que ambientes dispara (por padrão só produção). Valores livres antigos (`prod`, `qa`) são normalizados pelo servidor.
390
111
 
391
112
  ## Claim
392
113
 
393
- O **claim** é o ato de um agente reivindicar uma run para si. Importa porque resolve a pergunta óbvia de qualquer sistema de fila: **o que impede dois agentes de pegarem a mesma run?**
394
-
395
- A resposta é que o claim é **server-side e atômico**. O SDK é um cliente HTTP puro: ele faz um `POST` de poll, e é o servidor que seleciona as runs pendentes e as promove a `claimed` numa única operação de banco com `FOR UPDATE SKIP LOCKED`. Dois agentes que fazem poll no mesmo milissegundo recebem conjuntos disjuntos — não há corrida a resolver no cliente, e nenhum lock vive no seu processo.
396
-
397
- O agente pede só o que consegue rodar (`max = concurrency − runs ativas`), então ele nunca reivindica trabalho para deixar parado.
398
-
399
- Detalhes do protocolo em [Protocolo & transporte](./protocol.md#onde-vive-o-claim-atômico).
114
+ O **claim** é o ato de um agente reivindicar uma execução pendente. É **server-side e atômico** (`FOR UPDATE SKIP LOCKED`): dois agentes que fazem poll no mesmo instante recebem conjuntos disjuntos — nenhum lock vive no seu processo. O agente pede só o que consegue rodar (tasks com slot livre), então nunca reivindica trabalho para deixar parado.
400
115
 
401
116
  ## Drain mode
402
117
 
403
- **Drain mode** é o comportamento do polling quando há trabalho acumulado: em vez de esperar o intervalo configurado, o agente faz o próximo poll **imediatamente**, e continua assim enquanto vierem runs. Quando um poll volta vazio, ele volta a esperar `pollIntervalMs`.
404
-
405
- O efeito é que a fila esvazia rápido sem que você precise baixar o intervalo. Um agente com `pollIntervalMs` de 5 segundos não demora 50 segundos para consumir 10 runs — ele drena tudo de uma vez e só então volta a dormir.
406
-
407
- Isso também explica por que o intervalo de poll não é a latência típica: ele é só o **teto da espera quando não há nada para fazer**.
408
-
409
- ## Fila, flush e close
410
-
411
- O SDK **não** envia um HTTP por evento. Cada `captureException`, transaction e heartbeat entra numa **fila em memória**; um timer junta os itens num **envelope** e faz um POST só. É isso que faz a telemetria custar quase nada no caminho crítico do seu robô — e é isso que cria o problema que `flush` e `close` resolvem: **quando o processo morre, a fila morre com ele.**
412
-
413
- A fila esvazia sozinha em três situações: passou o `flushIntervalMs`, acumulou `maxBatchSize` itens, ou você pediu.
414
-
415
- ### `flush(timeoutMs?)` — "manda o que tem agora"
416
-
417
- Empurra a fila e espera os envios em andamento terminarem. O SDK **continua vivo e usável** depois. Use antes de um ponto de risco: o fim de um lote longo, um `process.exit()` que não é seu, uma etapa que costuma travar a VM.
418
-
419
- Devolve `true` se tudo saiu dentro do tempo e `false` se estourou. `false` **não** significa que os dados foram perdidos — significa que o SDK parou de esperar; os envios em voo continuam tentando (com a [ladder de retry](./api-reference.md#tempos-e-intervalos)) enquanto o processo existir.
420
-
421
- ### `close(timeoutMs?)` — "acabou, encerra"
422
-
423
- Encerra tudo, **nesta ordem**: para o agente de tasks (aguardando as runs em execução) → para o heartbeat → faz o flush final e fecha o transport. É sempre a última linha do seu programa.
424
-
425
- > **Atenção:** `close()` é **terminal e sem volta**. Depois dele, todo `captureException`, transaction ou heartbeat é **descartado** com um aviso no console — não existe `reopen()`. Se o programa precisar voltar a emitir telemetria, chame `Hobots.init()` de novo.
426
-
427
- > **Atenção:** o `timeoutMs` do `close()` é aplicado **duas vezes, em sequência** — uma para esperar as runs de tasks, outra para o flush final. `close(15_000)` pode levar até ~30s no pior caso. Se você tem um SIGTERM com prazo curto (Kubernetes, systemd), dimensione por aí.
428
-
429
- O `flush` cobre a fila da **telemetria**. Os logs de uma run de task (`ctx.log`) são um canal separado, com o próprio ritmo — quem espera por eles é o `close()`/`stop()`, ao aguardar a run terminar.
430
-
431
- Defaults e tempos dos dois: [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
432
-
433
- ## beforeSend
434
-
435
- `beforeSend` é o último gancho antes de um [evento](#evento) entrar na [fila](#fila-flush-e-close). Você recebe o evento **pronto** — já com tags, contexts, extras e breadcrumbs mesclados — e devolve ele (editado ou não) ou `null` para descartar.
436
-
437
- ```js
438
- Hobots.init({
439
- clientSecret,
440
- instanceId: 'vm-cliente-01',
441
- beforeSend: event => {
442
- if (event.exception?.type === 'AbortError') return null; // não me interessa
443
- delete event.extra?.senha; // censura
444
- return event;
445
- },
446
- });
447
- ```
448
-
449
- Dois usos principais: **censurar** dados que não deveriam sair da máquina (a última rede de proteção contra vazar um token num extra) e **descartar** classes de erro que você já sabe que são ruído.
450
-
451
- Onde ele entra na ordem das coisas: a [amostragem](#amostragem-samplerate-e-tracessamplerate) acontece **antes** (evento sorteado fora nunca chega ao `beforeSend`), e o merge de scope e `hint` também — então o que você vê é o evento final, exatamente como seria enviado.
118
+ Enquanto o poll traz trabalho, o agente faz o próximo **imediatamente**; quando volta vazio, espera `pollIntervalMs`. O intervalo é o teto da espera quando não há nada para fazer, não a latência típica.
452
119
 
453
- > **Atenção:** `beforeSend` roda no caminho da captura, de forma síncrona. Mantenha a função rápida e sem I/O — e cuidado para não lançar exceção dentro dela.
120
+ ## close
454
121
 
455
- Vale só para eventos. Transactions e heartbeats não passam pelo `beforeSend`.
122
+ O SDK não envia um HTTP por evento: tudo entra numa fila e sai em envelopes. `hobots.close(timeoutMs?)` é a última linha do programa: para o polling, espera as execuções em voo (as que não terminam a tempo viram `AgentTerminated`), para o heartbeat — liberando o processo — e faz o flush final. Sem handler próprio de sinal, o SDK faz o essencial disso sozinho num `SIGINT`/`SIGTERM`.