hobots 1.3.5 → 1.3.6
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 +35 -17
- package/dist/index.cjs +49 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +123 -110
- package/dist/index.d.ts +123 -110
- package/dist/index.js +49 -9
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +2 -0
- package/dist/protocol.d.ts +2 -0
- package/docs/README.md +4 -1
- package/docs/api-reference.md +87 -11
- package/docs/concepts.md +440 -0
- package/docs/getting-started.md +17 -2
- package/docs/manager.md +36 -7
- package/docs/monitor.md +37 -3
- package/docs/protocol.md +3 -1
- package/package.json +1 -1
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
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
|
+
- Esquecer o `finish()` de um span **não perde o dado**: o `finish()` da transaction fecha os spans abertos, marcando o fim naquele instante.
|
|
247
|
+
- 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.
|
|
248
|
+
|
|
249
|
+
## Transaction de run (ctx.tx)
|
|
250
|
+
|
|
251
|
+
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()`.
|
|
252
|
+
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
257
|
+
const login = ctx.tx.startChild('browser', 'login no portal');
|
|
258
|
+
await entrar();
|
|
259
|
+
login.finish();
|
|
260
|
+
|
|
261
|
+
for (const nota of params.notas) {
|
|
262
|
+
const span = ctx.tx.startChild('nfe', `nota ${nota.numero}`);
|
|
263
|
+
ctx.throwIfCanceled();
|
|
264
|
+
await processar(nota);
|
|
265
|
+
span.setData('valor', nota.valor).finish();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
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".
|
|
271
|
+
|
|
272
|
+
- **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.
|
|
273
|
+
- `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale para as transactions que **você** abre com `startTransaction()`.
|
|
274
|
+
- O nome é `run:<task>` e o `op` é `manager.run`, preenchidos pelo SDK.
|
|
275
|
+
|
|
276
|
+
## Amostragem (sampleRate e tracesSampleRate)
|
|
277
|
+
|
|
278
|
+
**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%.
|
|
279
|
+
|
|
280
|
+
São dois controles independentes, os dois de `0` a `1`:
|
|
281
|
+
|
|
282
|
+
| Opção | Afeta | `0.1` significa |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| `sampleRate` | [eventos](#evento) (`captureException`, `captureMessage`) | ~10% dos erros são enviados |
|
|
285
|
+
| `tracesSampleRate` | [transactions](#transaction) abertas por `startTransaction()` | ~10% das transactions são enviadas |
|
|
286
|
+
|
|
287
|
+
O sorteio é por item, no momento em que ele é criado — nunca "1 a cada 10", e sim 10% de chance cada.
|
|
288
|
+
|
|
289
|
+
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.
|
|
290
|
+
|
|
291
|
+
> **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.
|
|
292
|
+
|
|
293
|
+
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).
|
|
294
|
+
|
|
295
|
+
## Check-in
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
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.
|
|
300
|
+
|
|
301
|
+
O jeito recomendado é `withMonitor`, que faz o par sozinho e captura a exceção se houver:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
await Hobots.withMonitor('sync-notas', () => executarSync(), {
|
|
305
|
+
schedule: '0 */2 * * *',
|
|
306
|
+
timezone: 'America/Fortaleza',
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
Detalhes que importam:
|
|
313
|
+
|
|
314
|
+
- `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.
|
|
315
|
+
- 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.
|
|
316
|
+
|
|
317
|
+
## Heartbeat
|
|
318
|
+
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
É 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.
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
Hobots.init({
|
|
325
|
+
clientSecret,
|
|
326
|
+
instanceId: 'vm-cliente-01',
|
|
327
|
+
heartbeat: { intervalMs: 60_000, name: 'Robô NFe — VM 01' },
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`heartbeat: true` usa os defaults. A identidade é sempre o [`instanceId`](#instanceid) do `init()`; o `name` é só o rótulo amigável no dashboard.
|
|
332
|
+
|
|
333
|
+
Dois detalhes:
|
|
334
|
+
|
|
335
|
+
- O primeiro batimento sai **na hora** em que o `init()` roda — não depois do primeiro intervalo. O robô aparece online imediatamente.
|
|
336
|
+
- 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.
|
|
337
|
+
|
|
338
|
+
O timer do heartbeat é `unref`'d — ele **não** segura o processo vivo.
|
|
339
|
+
|
|
340
|
+
## instanceId
|
|
341
|
+
|
|
342
|
+
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.
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
346
|
+
## Release e environment
|
|
347
|
+
|
|
348
|
+
**`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.
|
|
349
|
+
|
|
350
|
+
Servem a duas perguntas concretas:
|
|
351
|
+
|
|
352
|
+
- *"quebrou desde quando?"* — com `release`, você compara o antes e o depois de um deploy e sabe qual versão introduziu o erro.
|
|
353
|
+
- *"isso é real ou é teste?"* — com `environment`, o ruído da homologação não polui os alertas de produção.
|
|
354
|
+
|
|
355
|
+
`environment` default é `production`. Se você roda homologação sem definir isso, os dois ambientes se misturam na mesma issue — vale configurar no primeiro dia.
|
|
356
|
+
|
|
357
|
+
## Run
|
|
358
|
+
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
É a unidade em volta da qual o [Manager](./manager.md) inteiro gira. O ciclo de vida no servidor:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
pending → claimed → running → succeeded | failed | canceled
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- **`pending`** — criada, esperando um agente.
|
|
368
|
+
- **`claimed`** — um agente reivindicou ([claim](#claim)), mas o handler ainda não confirmou o início.
|
|
369
|
+
- **`running`** — o handler está executando. Daqui para frente o agente reporta logs incrementais.
|
|
370
|
+
- **`succeeded` / `failed` / `canceled`** — terminal.
|
|
371
|
+
|
|
372
|
+
O agente só transiciona para `running`, `succeeded` e `failed`. `pending` e `claimed` são do servidor; `canceled` vem de alguém cancelando no app.
|
|
373
|
+
|
|
374
|
+
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).
|
|
375
|
+
|
|
376
|
+
## Claim
|
|
377
|
+
|
|
378
|
+
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?**
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
O agente pede só o que consegue rodar (`max = concurrency − runs ativas`), então ele nunca reivindica trabalho para deixar parado.
|
|
383
|
+
|
|
384
|
+
Detalhes do protocolo em [Protocolo & transporte](./protocol.md#onde-vive-o-claim-atômico).
|
|
385
|
+
|
|
386
|
+
## Drain mode
|
|
387
|
+
|
|
388
|
+
**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`.
|
|
389
|
+
|
|
390
|
+
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.
|
|
391
|
+
|
|
392
|
+
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**.
|
|
393
|
+
|
|
394
|
+
## Fila, flush e close
|
|
395
|
+
|
|
396
|
+
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.**
|
|
397
|
+
|
|
398
|
+
A fila esvazia sozinha em três situações: passou o `flushIntervalMs`, acumulou `maxBatchSize` itens, ou você pediu.
|
|
399
|
+
|
|
400
|
+
### `flush(timeoutMs?)` — "manda o que tem agora"
|
|
401
|
+
|
|
402
|
+
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.
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
### `close(timeoutMs?)` — "acabou, encerra"
|
|
407
|
+
|
|
408
|
+
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.
|
|
409
|
+
|
|
410
|
+
> **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.
|
|
411
|
+
|
|
412
|
+
> **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í.
|
|
413
|
+
|
|
414
|
+
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.
|
|
415
|
+
|
|
416
|
+
Defaults e tempos dos dois: [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
|
|
417
|
+
|
|
418
|
+
## beforeSend
|
|
419
|
+
|
|
420
|
+
`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.
|
|
421
|
+
|
|
422
|
+
```js
|
|
423
|
+
Hobots.init({
|
|
424
|
+
clientSecret,
|
|
425
|
+
instanceId: 'vm-cliente-01',
|
|
426
|
+
beforeSend: (event) => {
|
|
427
|
+
if (event.exception?.type === 'AbortError') return null; // não me interessa
|
|
428
|
+
delete event.extra?.senha; // censura
|
|
429
|
+
return event;
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
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.
|
|
435
|
+
|
|
436
|
+
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.
|
|
437
|
+
|
|
438
|
+
> **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.
|
|
439
|
+
|
|
440
|
+
Vale só para eventos. Transactions, check-ins e heartbeats não passam pelo `beforeSend`.
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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).
|
package/docs/manager.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Capability de **execução remota sob demanda**, habilitada com `manager: true` (ou opções) no `init()` único. O agente faz polling no app, executa os handlers registrados quando aparece trabalho e reporta status e logs incrementais. Usa o mesmo Client Secret, o mesmo `debug` e o `instanceId` como identidade — nenhum handler global extra é instalado.
|
|
4
4
|
|
|
5
|
+
O vocabulário desta página ([run](./concepts.md#run), [claim](./concepts.md#claim), [drain mode](./concepts.md#drain-mode), [ctx.tx](./concepts.md#transaction-de-run-ctxtx)) está explicado em [Conceitos](./concepts.md).
|
|
6
|
+
|
|
5
7
|
## Ciclo de vida
|
|
6
8
|
|
|
7
9
|
```js
|
|
@@ -26,10 +28,10 @@ process.on('SIGINT', () => {
|
|
|
26
28
|
});
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
- `init({ manager })` — cria o agente. Chamar `init()` de novo para o agente anterior e o substitui (os handlers registrados são preservados).
|
|
31
|
+
- `init({ manager })` — cria o agente. Chamar `init()` de novo para o agente anterior e o substitui (os handlers registrados são preservados). Se havia runs em execução, elas são interrompidas **sem espera** e reportadas como `AgentTerminated` na hora — `init()` não é um jeito gracioso de reconfigurar um agente ocupado; para isso use `stop()` antes.
|
|
30
32
|
- `register(task, handler)` — `task` é o `task_slug` configurado no formulário do app. Registre **antes** de `start()`. Pode ser chamado antes do `init()`; se o `init()` não habilitou o Manager, o SDK avisa no console.
|
|
31
33
|
- `start()` — começa o polling. Lança se o `init()` não configurou `manager` ou se nenhuma task foi registrada.
|
|
32
|
-
- `stop(timeoutMs?)` — para **só o Manager**: encerra o polling e aguarda as runs ativas (default
|
|
34
|
+
- `stop(timeoutMs?)` — para **só o Manager**: encerra o polling e aguarda as runs ativas ([default](./api-reference.md#tempos-e-intervalos)); a telemetria continua. Retorna `false` se estourou o timeout.
|
|
33
35
|
- `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final.
|
|
34
36
|
|
|
35
37
|
O `instanceId` (obrigatório) é a identidade do agente no painel — o poll o envia como `agent.instance_id`, a **mesma identidade** do heartbeat do Monitor. Com o `init()` único, os dois canais convergem em **um agente** automaticamente.
|
|
@@ -46,6 +48,7 @@ interface RunContext {
|
|
|
46
48
|
runId: string;
|
|
47
49
|
task: string;
|
|
48
50
|
log(level: 'debug' | 'info' | 'warning' | 'error', message: string): void;
|
|
51
|
+
tx: Transaction;
|
|
49
52
|
signal: AbortSignal;
|
|
50
53
|
throwIfCanceled(): void;
|
|
51
54
|
sleep(ms: number): Promise<void>;
|
|
@@ -53,7 +56,8 @@ interface RunContext {
|
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
- **`params`** — os dados do formulário configurado no app (o JSON validado).
|
|
56
|
-
- **`ctx.log.<level>(message)`** — logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na run em tempo quase real.
|
|
59
|
+
- **`ctx.log.<level>(message)`** — logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na run em tempo quase real. Cada linha também é **impressa no terminal local** como `[hobots:<task>] INFO mensagem`, colorida por nível — útil para acompanhar o robô rodando na VM. A cor é desligada automaticamente fora de um TTY e quando a env `NO_COLOR` está definida; o eco local é independente do envio ao servidor.
|
|
60
|
+
- **`ctx.tx`** — a [transaction de performance](./concepts.md#transaction-de-run-ctxtx) desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild()` para medir as etapas (veja abaixo).
|
|
57
61
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**. Cancelamento é cooperativo (veja abaixo).
|
|
58
62
|
- **`ctx.throwIfCanceled()`** — lança `RunCanceled` se o `signal` já disparou. O ponto de parada de 1 linha para chamar entre as etapas do trabalho.
|
|
59
63
|
- **`ctx.sleep(ms)`** — sleep que respeita o `signal`: rejeita com `RunCanceled` no cancelamento em vez de esperar até o fim. Use no lugar do seu `sleep` em esperas longas.
|
|
@@ -64,9 +68,34 @@ Você pode tipar os params: `Hobots.register<{ lote: number }>(...)`.
|
|
|
64
68
|
|
|
65
69
|
> **Dados sensíveis:** o que você escreve em `ctx.log()` e as mensagens/stack de erros lançados **são enviados ao servidor** e ficam visíveis na execução no app. Não inclua senhas, tokens, documentos, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
|
|
66
70
|
|
|
71
|
+
## Performance da run (`ctx.tx`)
|
|
72
|
+
|
|
73
|
+
O SDK já abre uma [transaction](./concepts.md#transaction-de-run-ctxtx) para cada run e entrega em `ctx.tx`. Ela é correlacionada à execução, recebe o status conforme o desfecho (`ok`, `error` ou `cancelled`) e é **finalizada automaticamente** quando o handler termina. Você não cria e não finaliza — só marca as etapas:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
77
|
+
const login = ctx.tx.startChild('browser', 'login no portal');
|
|
78
|
+
await entrar();
|
|
79
|
+
login.finish();
|
|
80
|
+
|
|
81
|
+
for (const nota of params.notas) {
|
|
82
|
+
const span = ctx.tx.startChild('nfe', `nota ${nota.numero}`);
|
|
83
|
+
ctx.throwIfCanceled();
|
|
84
|
+
await processar(nota);
|
|
85
|
+
span.setData('valor', nota.valor).finish();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Cada `startChild` vira uma barra no waterfall da aba de performance daquela execução no app — é como você responde "onde foram os 4 minutos". Sem nenhum span, a transaction ainda mede o tempo total da run; o waterfall só fica com uma barra.
|
|
91
|
+
|
|
92
|
+
- **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.
|
|
93
|
+
- `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale só para as transactions que você abre com `startTransaction()`.
|
|
94
|
+
- Span que você esquecer aberto é fechado no `finish()` da transaction, então o dado não se perde.
|
|
95
|
+
|
|
67
96
|
## Cancelamento cooperativo
|
|
68
97
|
|
|
69
|
-
Uma run cancelada no app **não mata** o seu handler — o SDK não interrompe código em execução. Ele apenas dispara `ctx.signal`. Enquanto o handler roda, o SDK mantém um **heartbeat de cancelamento** (a cada `cancelCheckIntervalMs`,
|
|
98
|
+
Uma run cancelada no app **não mata** o seu handler — o SDK não interrompe código em execução. Ele apenas dispara `ctx.signal`. Enquanto o handler roda, o SDK mantém um **heartbeat de cancelamento** (a cada `cancelCheckIntervalMs`, veja [Tempos e intervalos](./api-reference.md#tempos-e-intervalos)): a run cancelada no app é detectada em segundos, mesmo que o handler nunca chame `ctx.log()`. Chame **`ctx.throwIfCanceled()`** em loops longos e pontos de parada (e troque esperas por `ctx.sleep()`):
|
|
70
99
|
|
|
71
100
|
```js
|
|
72
101
|
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
@@ -84,7 +113,7 @@ Quando o cancelamento veio do app, a run já está `cancelada` no servidor — o
|
|
|
84
113
|
Se o processo do agente **morre no meio de uma run**, a solicitação vira **falha** no app com erro `AgentTerminated` ("execução encerrada inesperadamente") — sem re-execução automática. Dois caminhos garantem isso:
|
|
85
114
|
|
|
86
115
|
- **Morte capturável** (SIGINT/SIGTERM sem handler próprio do programa, erro não tratado): o SDK reporta a falha na hora (~2s, um PATCH best-effort com timeout curto) e então deixa o processo morrer com a semântica default do sinal. Os handlers são instalados no `start()` e **nunca interferem** quando o programa tem handler próprio para o sinal (o shutdown gracioso via `close()` continua sendo o caminho normal). Desative com `manager: { installExitHandlers: false }`.
|
|
87
|
-
- **Morte dura** (kill -9, queda de energia, rede fora): nada roda no cliente. O heartbeat de cancelamento é também o **sinal de vida da run** — o servidor marca a run como falha ao notar
|
|
116
|
+
- **Morte dura** (kill -9, queda de energia, rede fora): nada roda no cliente. O heartbeat de cancelamento é também o **sinal de vida da run** — o servidor marca a run como falha ao notar silêncio prolongado (o [sweep](./api-reference.md#tempos-e-intervalos) server-side).
|
|
88
117
|
|
|
89
118
|
`close(timeoutMs)` que estoura o timeout com runs ainda em execução também reporta essas runs como `AgentTerminated` — o processo vai morrer em seguida e o app não deve mostrar a run como "executando" para sempre.
|
|
90
119
|
|
|
@@ -94,7 +123,7 @@ Se o agente morrer logo **após capturar** uma run (antes de reportar `running`)
|
|
|
94
123
|
|
|
95
124
|
## Concorrência
|
|
96
125
|
|
|
97
|
-
`concurrency` (default 1, máx 5) controla quantas runs o agente executa em paralelo. Com 1, as runs são sequenciais. O agente só reivindica o que consegue rodar (`max = concurrency - runs ativas`) e entra em **drain mode**: enquanto vêm runs, re-pesquisa imediatamente; quando não vem nada, espera `pollIntervalMs`.
|
|
126
|
+
`concurrency` (default 1, máx 5) controla quantas runs o agente executa em paralelo. Com 1, as runs são sequenciais. O agente só reivindica o que consegue rodar (`max = concurrency - runs ativas`) e entra em **[drain mode](./concepts.md#drain-mode)**: enquanto vêm runs, re-pesquisa imediatamente; quando não vem nada, espera `pollIntervalMs`. Ou seja, o intervalo de poll é o teto da espera **quando não há trabalho** — não a latência típica para começar uma run.
|
|
98
127
|
|
|
99
128
|
## Timeout por run
|
|
100
129
|
|
|
@@ -104,4 +133,4 @@ Todas as opções em [Referência de API → ManagerOptions](./api-reference.md#
|
|
|
104
133
|
|
|
105
134
|
## Como o claim funciona (resumo)
|
|
106
135
|
|
|
107
|
-
O SDK é um cliente HTTP: ele faz `POST /v1/manager/poll` e o **servidor** reivindica as runs pendentes de forma atômica com `FOR UPDATE SKIP LOCKED` — dois agentes nunca pegam a mesma run.
|
|
136
|
+
O SDK é um cliente HTTP: ele faz `POST /v1/manager/poll` e o **servidor** reivindica as runs pendentes de forma atômica com `FOR UPDATE SKIP LOCKED` — dois agentes nunca pegam a mesma run. Conceito em [Claim](./concepts.md#claim), detalhes em [Protocolo & transporte](./protocol.md).
|