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/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
|
|
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
|
-
|
|
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
|
|
12
|
-
|
|
|
13
|
-
| [
|
|
14
|
-
| [
|
|
15
|
-
| [
|
|
16
|
-
| [
|
|
17
|
-
| [
|
|
18
|
-
| [
|
|
19
|
-
| [
|
|
20
|
-
| [
|
|
21
|
-
| [
|
|
22
|
-
| [
|
|
23
|
-
| [
|
|
24
|
-
| [
|
|
25
|
-
| [
|
|
26
|
-
| [
|
|
27
|
-
| [
|
|
28
|
-
| [
|
|
29
|
-
| [
|
|
30
|
-
| [
|
|
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**
|
|
44
|
-
| **Instância** — o par `(
|
|
45
|
-
| **
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
## Modo sob demanda (`register`)
|
|
71
47
|
|
|
72
|
-
|
|
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
|
-
|
|
50
|
+
## Modo sempre ligado (`start`)
|
|
75
51
|
|
|
76
|
-
|
|
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
|
-
|
|
54
|
+
## Execução (run)
|
|
79
55
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
## Item
|
|
180
65
|
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
## Log da execução
|
|
197
73
|
|
|
198
|
-
|
|
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
|
-
|
|
76
|
+
## Anexo
|
|
201
77
|
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
|
|
206
|
-
```
|
|
80
|
+
## Cancelamento cooperativo
|
|
207
81
|
|
|
208
|
-
|
|
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
|
-
|
|
84
|
+
## Retry
|
|
211
85
|
|
|
212
|
-
|
|
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
|
-
|
|
88
|
+
## Evento e issue
|
|
216
89
|
|
|
217
|
-
|
|
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 **
|
|
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**
|
|
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** é
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
## close
|
|
454
121
|
|
|
455
|
-
|
|
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`.
|