hobots 1.3.5 → 1.5.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.
@@ -17,8 +17,8 @@
17
17
  | --- | --- | --- |
18
18
  | `init` | `init(options: InitOptions): void` | — |
19
19
  | `getClient` | `getClient(): Client \| undefined` | cliente atual |
20
- | `captureException` | `captureException(error: unknown, hint?: CaptureHint)` | `string \| undefined` (event_id) |
21
- | `captureMessage` | `captureMessage(message: string, level?: Severity, hint?: CaptureHint)` | `string \| undefined` |
20
+ | `captureException` | `captureException(error: unknown, hint?: CaptureHint)` | `string \| undefined` (event_id — **não** é confirmação de envio) |
21
+ | `captureMessage` | `captureMessage(message: string, level?: Severity, hint?: CaptureHint)` | `string \| undefined` (idem) |
22
22
  | `addBreadcrumb` | `addBreadcrumb(crumb: BreadcrumbInput): void` | — |
23
23
  | `setTag` / `setTags` | `setTag(key, value)` · `setTags(tags)` | — |
24
24
  | `setUser` | `setUser(user: EventUser \| null): void` | — |
@@ -28,8 +28,29 @@
28
28
  | `startTransaction` | `startTransaction(name: string, op?: string): Transaction` | transaction |
29
29
  | `checkIn` | `checkIn(options: CheckInOptions): string \| undefined` | check_in_id |
30
30
  | `withMonitor` | `withMonitor<T>(slug: string, fn: () => T \| Promise<T>, options?: WithMonitorOptions): Promise<T>` | retorno de `fn` |
31
- | `flush` | `flush(timeoutMs?: number): Promise<boolean>` | enviou tudo? |
32
- | `close` | `close(timeoutMs?: number): Promise<boolean>` | para o Manager (aguarda runs) + heartbeat + flush final do transport |
31
+ | `flush` | `flush(timeoutMs = 5_000): Promise<boolean>` | enviou tudo dentro do prazo? |
32
+ | `close` | `close(timeoutMs = 5_000): Promise<boolean>` | para o Manager (aguarda runs) + heartbeat + flush final do transport |
33
+
34
+ ### O que cada função faz
35
+
36
+ Os conceitos por trás dessas chamadas estão em [Conceitos](./concepts.md); aqui está o que cada uma **faz** e o que **devolve**.
37
+
38
+ - **`init(options)`** — cria o cliente, instala os handlers globais de erro (a menos que `captureUnhandled: false`), inicia o heartbeat se configurado e cria o agente do Manager se `manager` foi passado. Chamar de novo **substitui** o cliente e o agente (os handlers registrados com `register` são preservados; o [scope](./concepts.md#scope) **não** é limpo). Chame uma vez, o mais cedo possível.
39
+ - **`getClient()`** — devolve o cliente atual ou `undefined` se o `init()` nunca rodou. Útil para checar se o SDK está ativo.
40
+ - **`captureException(error, hint?)`** — monta um [evento](./concepts.md#evento) a partir da exceção (tipo, mensagem, stack trace), mescla o scope e enfileira. O `level` é `error` a menos que você passe outro no `hint`.
41
+ - **`captureMessage(message, level = 'info', hint?)`** — mesma coisa, sem exceção: um evento só com texto. Bom para marcos (`'success'`) e avisos que não são erro.
42
+ > **Atenção:** as duas devolvem um `event_id` **mesmo quando o evento foi descartado** por [amostragem](./concepts.md#amostragem-samplerate-e-tracessamplerate) ou por [`beforeSend`](./concepts.md#beforesend). O retorno não prova que algo foi enviado — só é `undefined` quando o `init()` não foi chamado.
43
+ - **`addBreadcrumb(crumb)`** — adiciona uma migalha à trilha do [scope](./concepts.md#scope) global. Todos os campos são opcionais menos o `timestamp`, que o SDK preenche. A lista **não** é limpa ao enviar um evento; ela só descarta as mais antigas ao estourar `maxBreadcrumbs`. Conceito: [Breadcrumb](./concepts.md#breadcrumb).
44
+ - **`setTag(key, value)` / `setTags(tags)`** — grava [tags](./concepts.md#tag) no scope global; valem para todos os eventos seguintes. Só `string` → `string`. Precedência: `init.tags` → scope → evento → `hint`.
45
+ - **`setUser(user | null)`** — define quem está operando (`id`, `username`, `email`). `null` remove.
46
+ - **`setContext(name, ctx | null)`** — define um [context](./concepts.md#context) nomeado. Passar `null` **apaga o grupo inteiro**. Os grupos `runtime` e `os` são injetados pelo SDK em todo evento.
47
+ - **`setExtra(key, value)`** — anexa um dado livre, sem estrutura e sem índice. Conceito e critério de escolha em [Extra](./concepts.md#extra).
48
+ - **`withScope(cb)`** — roda o callback com um **clone** do scope e restaura o original no fim; devolve o que o callback devolver. É **síncrono** — um `await` dentro do callback não isola de código concorrente. Conceito: [withScope](./concepts.md#withscope).
49
+ - **`startTransaction(name, op?)`** — abre uma [transaction](./concepts.md#transaction) de performance. A decisão de [amostragem](./concepts.md#amostragem-samplerate-e-tracessamplerate) acontece **aqui**, não no `finish()`. Nada é enviado até o `finish()`. Dentro de um handler do Manager, use [`ctx.tx`](./concepts.md#transaction-de-run-ctxtx) em vez desta.
50
+ - **`checkIn(options)`** — envia um [check-in](./concepts.md#check-in) avulso e devolve o `check_in_id`. Reaproveite o id do `in_progress` para fechar a execução com `ok`/`error`.
51
+ - **`withMonitor(slug, fn, options?)`** — envolve `fn` no par de check-ins (`in_progress` → `ok`/`error`) medindo a duração, e devolve o que `fn` devolver. Em caso de erro, fecha como `error`, captura o evento com a tag `monitor: <slug>` e **relança** a exceção. Passe `schedule` para o servidor criar/atualizar o monitor sozinho e detectar execução perdida.
52
+ - **`flush(timeoutMs = 5_000)`** → `Promise<boolean>` — empurra a fila do transport e aguarda os envios em andamento. `true` = tudo saiu no prazo; `false` = o SDK parou de esperar (os envios em voo seguem tentando). O SDK **continua utilizável** depois. Conceito: [Fila, flush e close](./concepts.md#fila-flush-e-close).
53
+ - **`close(timeoutMs = 5_000)`** → `Promise<boolean>` — encerra tudo na ordem agente → heartbeat → flush final. O `timeoutMs` é gasto **duas vezes** (espera das runs, depois flush). Depois do `close()` o transport está fechado e todo item novo é descartado com aviso no console; não há `reopen()`. Conceito: [Fila, flush e close](./concepts.md#fila-flush-e-close).
33
54
 
34
55
  Classes exportadas: `Client`, `Scope`, `Transaction`, `Span`. Função `resolveConfig({ clientSecret }): ResolvedConfig` — resolve a credencial nos endpoints (constante `DEFAULT_BASE_URL = 'https://api2.hobots.app'`, em `src/config.ts`; em desenvolvimento a env `HOBOTS_API_URL` sobrescreve a URL base):
35
56
 
@@ -42,11 +63,14 @@ interface ResolvedConfig {
42
63
  }
43
64
  ```
44
65
 
45
- ### Métodos de `Transaction` / `Span`
66
+ ### Métodos de `Transaction` / `Span` / `Scope`
46
67
 
47
- - `Transaction`: `startChild(op?, description?) → Span`, `setTag(k, v)`, `setStatus(s)`, `setName(n)`, `finish()`.
48
- - `Span`: `startChild(op?, description?) → Span`, `setStatus(s)`, `setData(k, v)`, `finish()`, `toJSON()`.
49
- - `Scope`: `setTag/setTags/setUser/setContext/setExtra/setExtras/setLevel/setFingerprint/addBreadcrumb/clearBreadcrumbs/clone` (todos encadeáveis).
68
+ Todos encadeáveis, exceto `finish()` e `toJSON()`.
69
+
70
+ - **`Transaction`** — `startChild(op?, description?) → Span` abre um [span](./concepts.md#span) direto; `setTag(k, v)` marca a transaction (tags de scope **não** são herdadas); `setStatus(s)` (default `'ok'`); `setName(n)` renomeia antes do envio; `finish()` fecha os spans abertos, monta o payload e envia — é **idempotente** e não envia nada se a transaction não foi amostrada.
71
+ - **`Span`** — `startChild(op?, description?) → Span` aninha um filho (profundidade arbitrária); `setStatus(s)` (default `'ok'`); `setData(k, v)` anexa dados só àquele span; `finish()` marca o fim — **opcional**, porque o `finish()` da transaction fecha o que ficou aberto; `toJSON()` serializa no formato do wire.
72
+ - **`Scope`** — `setTag`/`setTags`/`setUser`/`setContext`/`setExtra`/`setExtras`/`setLevel`/`setFingerprint`/`addBreadcrumb`/`clearBreadcrumbs`/`clone`. Alcançável via [`withScope`](./concepts.md#withscope). `setContext(name, null)` apaga o grupo; `setUser(null)` remove o usuário.
73
+ > **Atenção:** `setLevel` **não tem efeito** em `captureException` (que já grava `error`) nem em `captureMessage` (que já grava `info`) — os dois definem o level do evento antes de o scope ser consultado. Use o `hint`.
50
74
 
51
75
  ---
52
76
 
@@ -56,9 +80,15 @@ Habilitado com `manager: true` (ou `ManagerOptions`) no `init()` único. Todas e
56
80
 
57
81
  | Função | Assinatura |
58
82
  | --- | --- |
59
- | `register` | `register<P, R>(task: string, handler: ManagerTaskHandler<P, R>): void` |
83
+ | `register` | `register<P>(task: string, handler: ManagerTaskHandler<P>): void` |
60
84
  | `start` | `start(): void` — lança se o `init()` não configurou `manager` ou sem tasks registradas |
61
- | `stop` | `stop(timeoutMs?: number): Promise<boolean>` — para só o Manager; a telemetria continua |
85
+ | `stop` | `stop(timeoutMs = 30_000): Promise<boolean>` — para só o Manager; a telemetria continua |
86
+
87
+ ### O que cada função faz
88
+
89
+ - **`register(task, handler)`** — associa um handler ao `task_slug` configurado no formulário do app. Registre **antes** de `start()`; pode ser chamado antes do `init()`. Registrar o mesmo `task` duas vezes **sobrescreve** o handler (com aviso no console). Se o `init()` não habilitou o Manager, o SDK avisa e não faz nada. Tipagem dos params: `register<{ lote: number }>(…)`.
90
+ - **`start()`** — começa o polling. **Mantém o processo vivo** até `stop()`/`close()` — o timer não é `unref`'d, ao contrário dos do Monitor. Lança se o `init()` não configurou `manager` ou se nenhuma task foi registrada. Também instala os handlers de saída (SIGINT/SIGTERM/erro fatal) que reportam as runs ativas como `AgentTerminated`, a menos que `installExitHandlers: false`.
91
+ - **`stop(timeoutMs = 30_000)`** → `Promise<boolean>` — encerra o polling e aguarda as [runs](./concepts.md#run) ativas terminarem. Para **só o Manager**: a telemetria (heartbeat, captura de erros) continua funcionando. Devolve `false` se estourou o timeout — as runs que ainda estavam rodando são reportadas como `AgentTerminated`. Para encerrar tudo, use [`close()`](./concepts.md#fila-flush-e-close).
62
92
 
63
93
  Classes/erros exportados: `ManagerAgent`, `ManagerApi`, `ManagerApiError`.
64
94
 
@@ -88,6 +118,8 @@ No poll, o agente se identifica com `ManagerAgentInfo` (`hobots/protocol`): o ca
88
118
  | `flushIntervalMs` | `number` | `2000` | intervalo do flush automático do transport |
89
119
  | `maxBatchSize` | `number` | `10` | itens por envelope antes do flush imediato |
90
120
 
121
+ O teto da fila do transport (200 itens; ao encher, o **mais antigo** é descartado com aviso) é interno — **não** é opção do `init()`. Conceitos por trás dessas opções: [amostragem](./concepts.md#amostragem-samplerate-e-tracessamplerate), [breadcrumbs](./concepts.md#breadcrumb), [instanceId](./concepts.md#instanceid), [release e environment](./concepts.md#release-e-environment), [beforeSend](./concepts.md#beforesend).
122
+
91
123
  ### HeartbeatOptions
92
124
 
93
125
  | Opção | Tipo | Default | Descrição |
@@ -105,7 +137,7 @@ No poll, o agente se identifica com `ManagerAgentInfo` (`hobots/protocol`): o ca
105
137
  | `extra` | `Record<string,unknown>` | dados extras |
106
138
  | `fingerprint` | `string[]` | agrupamento manual |
107
139
 
108
- `Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success'`. O nível `'success'` marca itens concluídos com êxito — contam como eventos, mas não geram issue nem alertas.
140
+ `Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success'`. O que cada nível significa (e por que `'success'` existe) em [Level](./concepts.md#level-severity).
109
141
 
110
142
  ### CheckInOptions
111
143
 
@@ -149,8 +181,52 @@ interface RunContext {
149
181
  runId: string;
150
182
  task: string;
151
183
  log(level: 'debug' | 'info' | 'warning' | 'error', message: string): void;
184
+ tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
152
185
  signal: AbortSignal;
153
186
  throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
154
187
  sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
155
188
  }
156
189
  ```
190
+
191
+ - **`runId` / `task`** — identificam a execução; úteis para correlacionar com os seus próprios logs.
192
+ - **`log(level, message)`** — enfileira uma linha de log enviada ao app em PATCHes incrementais, e **também imprime no terminal local**. Conceito: [Run](./concepts.md#run).
193
+ - **`tx`** — a [transaction de performance da run](./concepts.md#transaction-de-run-ctxtx). Use `ctx.tx.startChild(op, description)` para medir as etapas; **não** chame `finish()` nela nem abra uma transaction própria com `startTransaction()`.
194
+ - **`signal`** — `AbortSignal` que dispara em cancelamento no app, timeout ou shutdown.
195
+ - **`throwIfCanceled()`** — o ponto de parada de uma linha: lança `RunCanceled` se o `signal` já disparou.
196
+ - **`sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim). Use no lugar do seu `sleep` em esperas longas.
197
+
198
+ ---
199
+
200
+ ## Tempos e intervalos
201
+
202
+ Todos os prazos do SDK num lugar só. A coluna **Onde configura** diz se o valor é seu ou fixo — o que está com `—` é comportamento do SDK ou do servidor e não se muda pelo `init()`.
203
+
204
+ ### Monitor — envio de telemetria
205
+
206
+ | Tempo | Default | Onde configura | O que acontece |
207
+ | --- | --- | --- | --- |
208
+ | flush automático | `2000` ms | `init({ flushIntervalMs })` | de quanto em quanto o transport esvazia a fila; menor = mais requests, telemetria mais fresca |
209
+ | lote por envelope | `10` itens | `init({ maxBatchSize })` | ao atingir, envia **na hora** sem esperar o intervalo |
210
+ | teto da fila | `200` itens | — (fixo) | fila cheia descarta o item **mais antigo**, com aviso no console |
211
+ | `flush(timeoutMs)` | `5000` ms | argumento | quanto espera os envios em voo antes de devolver `false` |
212
+ | `close(timeoutMs)` | `5000` ms | argumento | idem, no flush final (veja a nota 1 abaixo) |
213
+ | ladder de retry | `1s, 4s, 16s` | — (fixo) | 3 tentativas por envelope; depois ele é descartado. 4xx (exceto 429) não tem retry |
214
+ | heartbeat | `30000` ms | `init({ heartbeat: { intervalMs } })` | bate **imediatamente** no `init()` e depois a cada intervalo; o valor vai no payload e define quanto silêncio o servidor tolera antes de marcar offline |
215
+
216
+ ### Manager — polling e runs
217
+
218
+ | Tempo | Default | Onde configura | O que acontece |
219
+ | --- | --- | --- | --- |
220
+ | poll sem trabalho | `5000` ms (mín. `1000`) | `manager: { pollIntervalMs }` | é só o **teto da espera quando não há nada**: com trabalho na fila o próximo poll é imediato ([drain mode](./concepts.md#drain-mode)) |
221
+ | flush de logs da run | `2000` ms | `manager: { logFlushIntervalMs }` | ou na hora, ao acumular 20 linhas |
222
+ | heartbeat de cancelamento | `5000` ms (clamp `1000`..`15000`) | `manager: { cancelCheckIntervalMs }` | detecta cancelamento feito no app **e** é o sinal de vida da run (veja a nota 2) |
223
+ | timeout por run | sem timeout | `manager: { taskTimeoutMs }` | no estouro dispara `ctx.signal`, libera o slot de concorrência e reporta `TaskTimeout` — **não mata** o handler |
224
+ | `stop(timeoutMs)` | `30000` ms | argumento | espera as runs ativas; no estouro elas são reportadas como `AgentTerminated` |
225
+ | retry do poll | `1s, 4s, 16s` | — (fixo) | erros de rede/5xx |
226
+ | retry com chave rejeitada (401) | `30000` ms | — (fixo) | segue tentando devagar, com log throttled |
227
+ | PATCH do caminho de saída | `~2500` ms | — (fixo) | melhor esforço, sem retry, ao morrer com run ativa |
228
+ | sweep do servidor | `~60000` ms | — (servidor) | run `running` sem reporte por esse tempo vira `failed`/`AgentTerminated`, sem re-entrega |
229
+
230
+ **Nota 1 — `close(timeoutMs)` gasta o tempo duas vezes.** Ele espera as runs do Manager (`timeoutMs`) e **depois** faz o flush final (`timeoutMs`), em sequência. Pior caso ≈ `2 × timeoutMs`; `close()` sem argumento pode levar até ~35s (30s aguardando runs + 5s de flush). Se o seu SIGTERM tem prazo curto (Kubernetes, systemd), dimensione por aí.
231
+
232
+ **Nota 2 — o teto de 15s do `cancelCheckIntervalMs` existe por causa do sweep de 60s.** Esse heartbeat é o que mantém o `last_report_at` da run atualizado; o clamp garante que o intervalo nunca chegue perto do prazo em que o servidor declara a run morta.
@@ -0,0 +1,441 @@
1
+ # Conceitos
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, span, check-in, 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**.
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ó.
8
+
9
+ ## Mapa rápido
10
+
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
+ | [Level](#level-severity) | A gravidade do 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
+ | [Span](#span) | 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 do Manager |
25
+ | [Amostragem](#amostragem-samplerate-e-tracessamplerate) | Enviar só uma fração do que acontece | Volume alto demais para enviar tudo |
26
+ | [Check-in](#check-in) | Par de avisos "comecei" / "terminei" de um job agendado | Cron que precisa ser vigiado |
27
+ | [Heartbeat](#heartbeat) | Batimento "estou vivo" | Saber que o robô está de pé, mesmo parado |
28
+ | [instanceId](#instanceid) | A identidade da instância do robô | Sempre — é obrigatório no `init()` |
29
+ | [Release e environment](#release-e-environment) | Qual versão, em qual ambiente | Separar produção de homologação; achar a versão que quebrou |
30
+ | [Run](#run) | Uma execução solicitada no app | O Manager inteiro gira em volta disso |
31
+ | [Claim](#claim) | O ato de um agente pegar uma run para si | Entender por que dois agentes nunca colidem |
32
+ | [Drain mode](#drain-mode) | O agente acelera o poll enquanto tem trabalho | Entender o ritmo do polling |
33
+ | [Fila, flush e close](#fila-flush-e-close) | Nada é enviado na hora — vai para uma fila | Encerrar o processo sem perder telemetria |
34
+ | [beforeSend](#beforesend) | O último gancho antes do envio | Censurar ou descartar eventos |
35
+
36
+ > **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.
37
+
38
+ ---
39
+
40
+ ## Evento
41
+
42
+ 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 do Monitor.
43
+
44
+ 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.
45
+
46
+ No servidor, eventos parecidos são agrupados numa **issue** — é 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).
47
+
48
+ ```js
49
+ Hobots.captureException(error);
50
+ Hobots.captureMessage('Lote processado com sucesso', 'info');
51
+ ```
52
+
53
+ As duas devolvem o `event_id` (um uuid), útil para você logar junto e depois achar o evento no app.
54
+
55
+ > **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.
56
+
57
+ ## Level (Severity)
58
+
59
+ O **level** é a gravidade do evento. São seis: `fatal`, `error`, `warning`, `info`, `debug` e `success`.
60
+
61
+ Serve para você separar o que precisa acordar alguém do que é só registro histórico. `error` e `fatal` são problemas; `warning` é algo que ainda funciona mas merece olhar; `info` e `debug` são narração.
62
+
63
+ 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.
64
+
65
+ A precedência é: o `level` do `hint` que você passou → o level que a função de captura já definiu → `error`. Na prática:
66
+
67
+ - `captureException(error)` sem `hint` é sempre `error`.
68
+ - `captureMessage(msg)` sem level explícito é sempre `info`.
69
+ - Para qualquer outra coisa, passe o level: `captureException(error, { level: 'warning' })`.
70
+
71
+ > **Atenção:** `scope.setLevel()` (dentro de um [`withScope`](#withscope)) **não afeta** `captureException` nem `captureMessage` — as duas já definem o level do evento antes de o scope ser consultado. Para mudar o level, use o `hint` (`{ level: … }`) ou o segundo argumento do `captureMessage`.
72
+
73
+ ## Fingerprint
74
+
75
+ O **fingerprint** é a chave de agrupamento: eventos com o mesmo fingerprint caem na **mesma issue**.
76
+
77
+ 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:
78
+
79
+ - **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.
80
+ - **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]`.
81
+
82
+ ```js
83
+ Hobots.captureException(error, { fingerprint: ['sefaz-timeout'] });
84
+ ```
85
+
86
+ O fingerprint é um array de strings — o servidor concatena os pedaços. Precedência: o do `hint` → o do evento → o do [scope](#scope).
87
+
88
+ > **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.
89
+
90
+ ## Scope
91
+
92
+ 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), level, [fingerprint](#fingerprint) e a lista de [breadcrumbs](#breadcrumb).
93
+
94
+ Você não manipula o scope direto — você usa os `set*`, que escrevem nele:
95
+
96
+ ```js
97
+ Hobots.setTag('regiao', 'nordeste');
98
+ Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
99
+ Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
100
+ Hobots.setExtra('ultimoLote', 128);
101
+ ```
102
+
103
+ 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.
104
+
105
+ Dois pontos importantes:
106
+
107
+ - **Existe um scope global, e só um.** Tudo que você escreve nele fica até você sobrescrever. Para contexto temporário, use [`withScope`](#withscope).
108
+ - **`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á.
109
+
110
+ ## withScope
111
+
112
+ `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.
113
+
114
+ ```js
115
+ Hobots.withScope((scope) => {
116
+ scope.setTag('lote', '128').setFingerprint(['reprocessamento']);
117
+ Hobots.captureMessage('Reprocessando lote 128');
118
+ });
119
+ // aqui a tag 'lote' e o fingerprint não existem mais
120
+ ```
121
+
122
+ 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.
123
+
124
+ > **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' } })`.
125
+
126
+ ## Tag
127
+
128
+ 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".
129
+
130
+ ```js
131
+ Hobots.setTag('regiao', 'nordeste');
132
+ Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
133
+ ```
134
+
135
+ 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).
136
+
137
+ Tags podem vir de quatro lugares, e em caso de conflito a última vence:
138
+
139
+ `init({ tags })` → [scope](#scope) → o próprio evento → o `hint` da chamada
140
+
141
+ 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.
142
+
143
+ > **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()`. [Check-ins](#check-in) e [heartbeats](#heartbeat) não levam tag nenhuma.
144
+
145
+ ## Context
146
+
147
+ 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.
148
+
149
+ ```js
150
+ Hobots.setContext('vm', { cpu: 4, ram: '8GB', disco: '40GB' });
151
+ Hobots.setContext('portal', { nome: 'SEFAZ-CE', versao: '2.4', homologacao: false });
152
+ ```
153
+
154
+ 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.
155
+
156
+ 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.
157
+
158
+ Para **remover** um grupo, passe `null`:
159
+
160
+ ```js
161
+ Hobots.setContext('portal', null); // apaga o grupo inteiro
162
+ ```
163
+
164
+ ## Extra
165
+
166
+ **Extra** é o depósito livre: chave → qualquer valor, sem grupo e sem tipo. Vai junto do evento e você lê no app.
167
+
168
+ ```js
169
+ Hobots.setExtra('ultimoLote', 128);
170
+ Hobots.setExtra('tentativas', [1, 2, 3]);
171
+ ```
172
+
173
+ É deliberadamente o menos estruturado dos três, e por isso o critério de escolha é simples:
174
+
175
+ | Você vai… | Use |
176
+ | --- | --- |
177
+ | filtrar, agrupar, contar por isso | [tag](#tag) |
178
+ | ler isso como um conjunto coerente | [context](#context) |
179
+ | nenhum dos dois — é só um dado a mais | **extra** |
180
+
181
+ 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.
182
+
183
+ > **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.
184
+
185
+ ## Breadcrumb
186
+
187
+ 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.
188
+
189
+ 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".
190
+
191
+ ```js
192
+ Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
193
+ ```
194
+
195
+ Todos os campos são opcionais: `category` agrupa a origem, `message` é o texto lido por humano, `level` colore a linha na timeline, `data` carrega um objeto livre, `type` classifica a migalha. O `timestamp` é preenchido pelo SDK.
196
+
197
+ Dois pontos que costumam surpreender:
198
+
199
+ - **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.
200
+ - **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()`.
201
+
202
+ > **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`.
203
+
204
+ Tamanho do buffer: [`maxBreadcrumbs`](./api-reference.md#initoptions).
205
+
206
+ ## Transaction
207
+
208
+ 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 [spans](#span), para "onde foram esses minutos?".
209
+
210
+ Enquanto o [evento](#evento) responde *o que quebrou*, a transaction responde *quanto tempo levou*. São os dois eixos do Monitor.
211
+
212
+ ```js
213
+ const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
214
+ await processarLote();
215
+ tx.setStatus('ok'); // ou 'error'
216
+ tx.finish();
217
+ ```
218
+
219
+ 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`).
220
+
221
+ Três coisas que valem saber:
222
+
223
+ - **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.
224
+ - **`finish()` é idempotente e fecha o resto.** Chamar duas vezes não envia duas; e ele **finaliza automaticamente** todos os spans que ficaram abertos.
225
+ - **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.
226
+
227
+ O `status` default é `'ok'`; encadeáveis: `setStatus`, `setName`, `setTag`.
228
+
229
+ Dentro de um handler do Manager você **não** cria transaction — já existe uma em [`ctx.tx`](#transaction-de-run-ctxtx).
230
+
231
+ ## Span
232
+
233
+ Um **span** é uma etapa medida **dentro** de uma [transaction](#transaction). A transaction diz que a rotina levou 4 minutos; os spans dizem que 3min40 foram esperando o portal.
234
+
235
+ ```js
236
+ const span = tx.startChild('http.client', 'GET /api/notas');
237
+ await api.buscarNotas();
238
+ span.setData('notas', 42).finish();
239
+ ```
240
+
241
+ `startChild(op?, description?)` existe **tanto na transaction quanto no span**, então você aninha quanto quiser — um span de "processar nota" com filhos de "baixar XML" e "gravar no banco". No app isso vira o **waterfall**: barras encaixadas, cada uma com sua duração.
242
+
243
+ O `op` é a categoria (útil para agrupar "todas as queries") e a `description` é o detalhe humano ("`INSERT INTO notas`"). `setData(chave, valor)` anexa dados àquele span específico — contagens, tamanhos, ids.
244
+
245
+ - Métodos encadeáveis: `setStatus`, `setData`, `finish`.
246
+ - **Status do span:** `'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 `span.setStatus('warning')` para etapas que passaram com ressalva (ex.: retry que deu certo, resultado parcial).
247
+ - Esquecer o `finish()` de um span **não perde o dado**: o `finish()` da transaction fecha os spans abertos, marcando o fim naquele instante.
248
+ - Os spans são guardados numa lista plana ligada por `parent_span_id` — o aninhamento que você vê no app é reconstruído a partir dessas referências, não da ordem das chamadas.
249
+
250
+ ## Transaction de run (ctx.tx)
251
+
252
+ Dentro de um handler do Manager 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()`.
253
+
254
+ O que sobra para você é a parte interessante: marcar as **etapas** do trabalho com [spans](#span). Cada `startChild` vira uma barra no waterfall da aba de performance daquela execução no app.
255
+
256
+ ```js
257
+ Hobots.register('processar-nfe', async (params, ctx) => {
258
+ const login = ctx.tx.startChild('browser', 'login no portal');
259
+ await entrar();
260
+ login.finish();
261
+
262
+ for (const nota of params.notas) {
263
+ const span = ctx.tx.startChild('nfe', `nota ${nota.numero}`);
264
+ ctx.throwIfCanceled();
265
+ await processar(nota);
266
+ span.setData('valor', nota.valor).finish();
267
+ }
268
+ });
269
+ ```
270
+
271
+ Sem nenhum `startChild` a transaction ainda existe e ainda mede o tempo total da run — o waterfall só fica com uma barra. Cada span que você abre é uma resposta a mais para "onde foram os 4 minutos".
272
+
273
+ - **Não chame `Hobots.startTransaction()` dentro de um handler.** Você criaria uma segunda transaction, **sem** correlação com a run, e ela não apareceria na execução.
274
+ - `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale para as transactions que **você** abre com `startTransaction()`.
275
+ - O nome é `run:<task>` e o `op` é `manager.run`, preenchidos pelo SDK.
276
+
277
+ ## Amostragem (sampleRate e tracesSampleRate)
278
+
279
+ **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%.
280
+
281
+ São dois controles independentes, os dois de `0` a `1`:
282
+
283
+ | Opção | Afeta | `0.1` significa |
284
+ | --- | --- | --- |
285
+ | `sampleRate` | [eventos](#evento) (`captureException`, `captureMessage`) | ~10% dos erros são enviados |
286
+ | `tracesSampleRate` | [transactions](#transaction) abertas por `startTransaction()` | ~10% das transactions são enviadas |
287
+
288
+ O sorteio é por item, no momento em que ele é criado — nunca "1 a cada 10", e sim 10% de chance cada.
289
+
290
+ Quem **nunca** é amostrado: [check-ins](#check-in), [heartbeats](#heartbeat) e a [transaction de run](#transaction-de-run-ctxtx) (`ctx.tx`). São sinais de vida e de agendamento — amostrar transformaria "o robô está offline" num palpite.
291
+
292
+ > **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.
293
+
294
+ 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).
295
+
296
+ ## Check-in
297
+
298
+ Um **check-in** é um aviso de que um job agendado começou ou terminou. Dois check-ins com o **mesmo id** formam um par: `in_progress` na entrada, `ok` ou `error` na saída — e a diferença entre eles é a duração medida.
299
+
300
+ Isso responde a uma pergunta que erro e performance não respondem: **"o cron rodou?"**. Um robô que nunca executa nunca lança exceção — o silêncio é o sintoma, e check-in é como o silêncio fica visível.
301
+
302
+ O jeito recomendado é `withMonitor`, que faz o par sozinho e captura a exceção se houver:
303
+
304
+ ```js
305
+ await Hobots.withMonitor('sync-notas', () => executarSync(), {
306
+ schedule: '0 */2 * * *',
307
+ timezone: 'America/Fortaleza',
308
+ });
309
+ ```
310
+
311
+ O **`schedule`** (expressão cron) é o campo que muda o jogo: com ele, o servidor **cria ou atualiza o monitor automaticamente** (upsert — você não precisa cadastrar nada no app) e passa a saber quando a próxima execução era esperada. É isso que habilita a detecção de **execução perdida**: se as 4h da manhã passaram e nenhum `in_progress` chegou, o app avisa.
312
+
313
+ Detalhes que importam:
314
+
315
+ - `withMonitor` **relança** a exceção depois de fechar o check-in como `error` e capturar o evento (com a tag `monitor: <slug>`) — o seu `try/catch` em volta continua funcionando igual.
316
+ - Precisa de controle manual (o par não está no mesmo lugar do código)? Use `checkIn` direto e reaproveite o `checkInId` devolvido pelo `in_progress` para fechar a execução.
317
+
318
+ ## Heartbeat
319
+
320
+ 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.
321
+
322
+ É o complemento do [check-in](#check-in): check-in vigia *execuções agendadas*, heartbeat vigia *o processo*. Um robô que fica de plantão esperando trabalho não tem cron para monitorar — mas você ainda quer saber quando a VM dele caiu.
323
+
324
+ ```js
325
+ Hobots.init({
326
+ clientSecret,
327
+ instanceId: 'vm-cliente-01',
328
+ heartbeat: { intervalMs: 60_000, name: 'Robô NFe — VM 01' },
329
+ });
330
+ ```
331
+
332
+ `heartbeat: true` usa os defaults. A identidade é sempre o [`instanceId`](#instanceid) do `init()`; o `name` é só o rótulo amigável no dashboard.
333
+
334
+ Dois detalhes:
335
+
336
+ - O primeiro batimento sai **na hora** em que o `init()` roda — não depois do primeiro intervalo. O robô aparece online imediatamente.
337
+ - 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.
338
+
339
+ O timer do heartbeat é `unref`'d — ele **não** segura o processo vivo.
340
+
341
+ ## instanceId
342
+
343
+ 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.
344
+
345
+ O que o torna especial é que ele é a identidade dos **dois** canais ao mesmo tempo: o [heartbeat](#heartbeat) do Monitor e o agente do [Manager](./manager.md) enviam o mesmo `instanceId`. O servidor correlaciona os dois, e o robô aparece como **um agente só** no painel — sem nenhuma configuração extra.
346
+
347
+ ## Release e environment
348
+
349
+ **`release`** é a versão do seu programa (`robo-nfe@1.4.2`) e **`environment`** é o ambiente lógico (`production`, `homologacao`). Os dois são anexados a **todo** item enviado: eventos, transactions e check-ins.
350
+
351
+ Servem a duas perguntas concretas:
352
+
353
+ - *"quebrou desde quando?"* — com `release`, você compara o antes e o depois de um deploy e sabe qual versão introduziu o erro.
354
+ - *"isso é real ou é teste?"* — com `environment`, o ruído da homologação não polui os alertas de produção.
355
+
356
+ `environment` default é `production`. Se você roda homologação sem definir isso, os dois ambientes se misturam na mesma issue — vale configurar no primeiro dia.
357
+
358
+ ## Run
359
+
360
+ 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.
361
+
362
+ É a unidade em volta da qual o [Manager](./manager.md) inteiro gira. O ciclo de vida no servidor:
363
+
364
+ ```
365
+ pending → claimed → running → succeeded | failed | canceled
366
+ ```
367
+
368
+ - **`pending`** — criada, esperando um agente.
369
+ - **`claimed`** — um agente reivindicou ([claim](#claim)), mas o handler ainda não confirmou o início.
370
+ - **`running`** — o handler está executando. Daqui para frente o agente reporta logs incrementais.
371
+ - **`succeeded` / `failed` / `canceled`** — terminal.
372
+
373
+ O agente só transiciona para `running`, `succeeded` e `failed`. `pending` e `claimed` são do servidor; `canceled` vem de alguém cancelando no app.
374
+
375
+ 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#managertaskhandler--runcontext) com logs, sinal de cancelamento e a [transaction](#transaction-de-run-ctxtx).
376
+
377
+ ## Claim
378
+
379
+ 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?**
380
+
381
+ 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.
382
+
383
+ O agente pede só o que consegue rodar (`max = concurrency − runs ativas`), então ele nunca reivindica trabalho para deixar parado.
384
+
385
+ Detalhes do protocolo em [Protocolo & transporte](./protocol.md#onde-vive-o-claim-atômico).
386
+
387
+ ## Drain mode
388
+
389
+ **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`.
390
+
391
+ 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.
392
+
393
+ 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**.
394
+
395
+ ## Fila, flush e close
396
+
397
+ O SDK **não** envia um HTTP por evento. Cada `captureException`, transaction, check-in 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.**
398
+
399
+ A fila esvazia sozinha em três situações: passou o `flushIntervalMs`, acumulou `maxBatchSize` itens, ou você pediu.
400
+
401
+ ### `flush(timeoutMs?)` — "manda o que tem agora"
402
+
403
+ 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.
404
+
405
+ 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.
406
+
407
+ ### `close(timeoutMs?)` — "acabou, encerra"
408
+
409
+ Encerra tudo, **nesta ordem**: para o agente do Manager (aguardando as runs em execução) → para o heartbeat → faz o flush final e fecha o transport. É sempre a última linha do seu programa.
410
+
411
+ > **Atenção:** `close()` é **terminal e sem volta**. Depois dele, todo `captureException`, transaction, check-in 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.
412
+
413
+ > **Atenção:** o `timeoutMs` do `close()` é aplicado **duas vezes, em sequência** — uma para esperar as runs do Manager, 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í.
414
+
415
+ O `flush` cobre a fila do **Monitor**. Os logs de uma run do Manager (`ctx.log`) são um canal separado, com o próprio ritmo — quem espera por eles é o `close()`/`stop()`, ao aguardar a run terminar.
416
+
417
+ Defaults e tempos dos dois: [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
418
+
419
+ ## beforeSend
420
+
421
+ `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.
422
+
423
+ ```js
424
+ Hobots.init({
425
+ clientSecret,
426
+ instanceId: 'vm-cliente-01',
427
+ beforeSend: (event) => {
428
+ if (event.exception?.type === 'AbortError') return null; // não me interessa
429
+ delete event.extra?.senha; // censura
430
+ return event;
431
+ },
432
+ });
433
+ ```
434
+
435
+ 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.
436
+
437
+ 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.
438
+
439
+ > **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.
440
+
441
+ Vale só para eventos. Transactions, check-ins e heartbeats não passam pelo `beforeSend`.
@@ -8,6 +8,19 @@ pnpm add hobots
8
8
 
9
9
  Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global). O Monitor também funciona no browser para captura de erros.
10
10
 
11
+ ## Conceitos em 2 minutos
12
+
13
+ Os termos que aparecem nesta página, em uma frase cada. Explicação completa de todos em [Conceitos](./concepts.md).
14
+
15
+ | Conceito | Em uma frase |
16
+ | --- | --- |
17
+ | [Evento](./concepts.md#evento) | A foto de um erro ou de uma mensagem que você registrou |
18
+ | [Tag](./concepts.md#tag) | Rótulo curto e indexável, para filtrar depois (`cliente`, `regiao`) |
19
+ | [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — o servidor acusa offline quando ele para |
20
+ | [instanceId](./concepts.md#instanceid) | A identidade da instância do robô; a mesma nos dois canais |
21
+ | [Run](./concepts.md#run) | Uma execução solicitada no app, que um agente pega e executa |
22
+ | [Fila, flush e close](./concepts.md#fila-flush-e-close) | Nada é enviado na hora — vai para uma fila que precisa ser esvaziada antes do processo morrer |
23
+
11
24
  ## O Client Secret
12
25
 
13
26
  Cada projeto tem **um único Client Secret** — a mesma credencial autentica o Monitor **e** o Manager:
@@ -75,7 +88,7 @@ Hobots.init({
75
88
 
76
89
  Hobots.register('processar-nfe', async (params, ctx) => {
77
90
  ctx.log.info(`processando lote ${params.lote}…`);
78
- return { processadas: 12 };
91
+ ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
79
92
  });
80
93
  Hobots.start(); // mantém o processo vivo até stop()/close()
81
94
  ```
@@ -102,4 +115,6 @@ process.on('SIGINT', () => {
102
115
  });
103
116
  ```
104
117
 
105
- Para parar de aceitar execuções **sem** derrubar a telemetria, use `Hobots.stop(timeoutMs?)`.
118
+ > **Atenção:** o `timeoutMs` do `close()` é gasto **duas vezes, em sequência** — uma esperando as runs do Manager, outra no flush final. O `close(15_000)` acima pode levar até ~30s no pior caso. Se o seu SIGTERM tem prazo curto (Kubernetes, systemd), dimensione por aí. Os defaults, quando você omite o argumento, estão em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
119
+
120
+ Para parar de aceitar execuções **sem** derrubar a telemetria, use `Hobots.stop(timeoutMs?)`. Conceito das duas funções em [Fila, flush e close](./concepts.md#fila-flush-e-close).