hobots 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -32
- package/dist/index.cjs +209 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +209 -46
- package/dist/index.js.map +1 -1
- package/dist/manager/protocol.cjs.map +1 -1
- package/dist/manager/protocol.d.cts +3 -3
- package/dist/manager/protocol.d.ts +3 -3
- package/docs/api-reference.md +0 -2
- package/docs/concepts.md +40 -40
- package/docs/getting-started.md +1 -1
- package/docs/protocol.md +2 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Cada projeto tem **um único Client Secret** — a mesma credencial autentica o
|
|
|
65
65
|
hb_<64 hex>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
A credencial é **secreta** e viaja sempre no header `Authorization: Bearer` — nunca na URL. Copie o Client Secret no app, em **Configurações → Projetos** — os snippets de integração já vêm prontos. O SDK fala com a API oficial `https://api2.hobots.app` automaticamente; em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL` (ex.: `HOBOTS_API_URL=http://localhost:3001`).
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -354,37 +354,7 @@ Para parar de aceitar execuções **sem** derrubar a telemetria, use `stop(timeo
|
|
|
354
354
|
| `hobots` | Tudo: `init`, Monitor (captura, transactions, crons, heartbeat), Manager (`register`/`start`/`stop`), `close`, classes e tipos. |
|
|
355
355
|
| `hobots/protocol` | Tipos do wire format (Monitor e Manager). |
|
|
356
356
|
|
|
357
|
-
Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
|
|
358
|
-
|
|
359
|
-
---
|
|
360
|
-
|
|
361
|
-
## Migração da API antiga (`hobots/manager`)
|
|
362
|
-
|
|
363
|
-
O Manager foi unificado no import raiz — `hobots/manager` **foi removido**:
|
|
364
|
-
|
|
365
|
-
```diff
|
|
366
|
-
-import * as Monitor from 'hobots';
|
|
367
|
-
-import * as Manager from 'hobots/manager';
|
|
368
|
-
+import * as Hobots from 'hobots';
|
|
369
|
-
|
|
370
|
-
-Monitor.init({ dsn, serverName: 'vm-01', heartbeat: true });
|
|
371
|
-
-Manager.init({ dsn, agentName: 'vm-01' });
|
|
372
|
-
+Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true, manager: true });
|
|
373
|
-
|
|
374
|
-
-Manager.register('minha-task', handler);
|
|
375
|
-
-Manager.start();
|
|
376
|
-
+Hobots.register('minha-task', handler);
|
|
377
|
-
+Hobots.start();
|
|
378
|
-
|
|
379
|
-
-await Manager.stop(15_000);
|
|
380
|
-
-await Monitor.close(3_000);
|
|
381
|
-
+await Hobots.close(15_000);
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
- A `dsn` (mecanismo antigo, `https://<secretKey>@host/<slug>`) não existe mais — passe `clientSecret` (em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL`). Copie o Client Secret em **Configurações → Projetos**.
|
|
385
|
-
- `agentName` e `serverName` não existem mais — a identidade do agente é o `instanceId` (obrigatório, sem fallback).
|
|
386
|
-
- As opções do Manager (`pollIntervalMs`, `concurrency`, `taskTimeoutMs`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `installExitHandlers`) vão no campo `manager` do `init()`; `clientSecret` e `debug` não se repetem.
|
|
387
|
-
- Tipos (`ManagerTaskHandler`, `RunContext`, `ManagerOptions`) e classes (`ManagerAgent`, `ManagerApi`) agora vêm de `hobots`; o wire format vem de `hobots/protocol`.
|
|
357
|
+
Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
|
|
388
358
|
|
|
389
359
|
---
|
|
390
360
|
|
package/dist/index.cjs
CHANGED
|
@@ -48,6 +48,7 @@ __export(src_exports, {
|
|
|
48
48
|
init: () => init,
|
|
49
49
|
register: () => register,
|
|
50
50
|
resolveConfig: () => resolveConfig,
|
|
51
|
+
scrubSecrets: () => scrubSecrets,
|
|
51
52
|
setContext: () => setContext,
|
|
52
53
|
setExtra: () => setExtra,
|
|
53
54
|
setTag: () => setTag,
|
|
@@ -63,21 +64,47 @@ module.exports = __toCommonJS(src_exports);
|
|
|
63
64
|
|
|
64
65
|
// src/config.ts
|
|
65
66
|
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
67
|
+
var SECRET_PATTERN = /^(hb_)?[a-f0-9]{64}$/i;
|
|
66
68
|
function envBaseUrl() {
|
|
67
69
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
68
70
|
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
69
71
|
}
|
|
72
|
+
function isLoopbackHost(hostname2) {
|
|
73
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
74
|
+
}
|
|
75
|
+
var warnedInsecureBaseUrl = false;
|
|
76
|
+
function validateBaseUrl(raw, prefix) {
|
|
77
|
+
let url;
|
|
78
|
+
try {
|
|
79
|
+
url = new URL(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
82
|
+
}
|
|
83
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
84
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
85
|
+
}
|
|
86
|
+
if (url.username || url.password) {
|
|
87
|
+
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
88
|
+
}
|
|
89
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
90
|
+
warnedInsecureBaseUrl = true;
|
|
91
|
+
console.warn(
|
|
92
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return raw.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
70
97
|
function resolveConfig(options, prefix = "[hobots]") {
|
|
71
98
|
const key = options.clientSecret?.trim();
|
|
72
99
|
if (!key) {
|
|
73
100
|
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
74
101
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
102
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`
|
|
105
|
+
);
|
|
80
106
|
}
|
|
107
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
81
108
|
return {
|
|
82
109
|
key,
|
|
83
110
|
baseUrl,
|
|
@@ -119,9 +146,28 @@ function parseStacktrace(stack) {
|
|
|
119
146
|
return frames;
|
|
120
147
|
}
|
|
121
148
|
|
|
149
|
+
// src/scrub.ts
|
|
150
|
+
var SECRET_PATTERNS = [
|
|
151
|
+
// client secret da Hobots
|
|
152
|
+
[/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
|
|
153
|
+
// tokens bearer em headers/mensagens
|
|
154
|
+
[/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
|
|
155
|
+
// chaves no formato sk-… (OpenAI e afins)
|
|
156
|
+
[/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
|
|
157
|
+
// credenciais embutidas em URL (https://user:pass@host)
|
|
158
|
+
[/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
|
|
159
|
+
];
|
|
160
|
+
function scrubSecrets(text) {
|
|
161
|
+
let out = text;
|
|
162
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
163
|
+
out = out.replace(pattern, replacement);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
// src/utils.ts
|
|
123
169
|
var SDK_NAME = "hobots";
|
|
124
|
-
var SDK_VERSION = true ? "1.
|
|
170
|
+
var SDK_VERSION = true ? "1.6.0" : "0.0.0-dev";
|
|
125
171
|
function isNode() {
|
|
126
172
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
127
173
|
}
|
|
@@ -130,6 +176,12 @@ function uuid4() {
|
|
|
130
176
|
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
131
177
|
return cryptoRef.randomUUID().replace(/-/g, "");
|
|
132
178
|
}
|
|
179
|
+
if (cryptoRef && typeof cryptoRef.getRandomValues === "function") {
|
|
180
|
+
const bytes = cryptoRef.getRandomValues(new Uint8Array(16));
|
|
181
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
182
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
183
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
184
|
+
}
|
|
133
185
|
let out = "";
|
|
134
186
|
for (let i = 0; i < 32; i++) {
|
|
135
187
|
out += Math.floor(Math.random() * 16).toString(16);
|
|
@@ -148,7 +200,42 @@ function sleep(ms) {
|
|
|
148
200
|
unrefTimer(timer);
|
|
149
201
|
});
|
|
150
202
|
}
|
|
151
|
-
|
|
203
|
+
function jitter(baseMs) {
|
|
204
|
+
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
205
|
+
}
|
|
206
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
207
|
+
function retryAfterMs(response) {
|
|
208
|
+
const header = response.headers.get("retry-after");
|
|
209
|
+
if (!header) return void 0;
|
|
210
|
+
const seconds = Number(header);
|
|
211
|
+
if (Number.isFinite(seconds)) {
|
|
212
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
|
|
213
|
+
}
|
|
214
|
+
const date = Date.parse(header);
|
|
215
|
+
if (Number.isNaN(date)) return void 0;
|
|
216
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
|
|
217
|
+
}
|
|
218
|
+
function withTimeoutSignal(timeoutMs, external) {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const onAbort = () => controller.abort(external?.reason);
|
|
221
|
+
if (external?.aborted) {
|
|
222
|
+
onAbort();
|
|
223
|
+
} else {
|
|
224
|
+
external?.addEventListener("abort", onAbort, { once: true });
|
|
225
|
+
}
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
|
|
228
|
+
}, timeoutMs);
|
|
229
|
+
unrefTimer(timer);
|
|
230
|
+
return {
|
|
231
|
+
signal: controller.signal,
|
|
232
|
+
cleanup: () => {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
external?.removeEventListener("abort", onAbort);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var PREFIX = "[hobots]";
|
|
152
239
|
function makeLogger(enabled, prefix = PREFIX) {
|
|
153
240
|
return {
|
|
154
241
|
debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
|
|
@@ -161,7 +248,8 @@ var ANSI = {
|
|
|
161
248
|
gray: "\x1B[90m",
|
|
162
249
|
cyan: "\x1B[36m",
|
|
163
250
|
yellow: "\x1B[33m",
|
|
164
|
-
red: "\x1B[31m"
|
|
251
|
+
red: "\x1B[31m",
|
|
252
|
+
green: "\x1B[32m"
|
|
165
253
|
};
|
|
166
254
|
function colorEnabled() {
|
|
167
255
|
if (!isNode()) return false;
|
|
@@ -188,11 +276,11 @@ function eventFromException(error, level = "error") {
|
|
|
188
276
|
let frames = [];
|
|
189
277
|
if (error instanceof Error) {
|
|
190
278
|
type = error.name || "Error";
|
|
191
|
-
value = error.message;
|
|
279
|
+
value = scrubSecrets(error.message);
|
|
192
280
|
frames = parseStacktrace(error.stack);
|
|
193
281
|
} else {
|
|
194
282
|
type = "NonError";
|
|
195
|
-
value = safeString(error);
|
|
283
|
+
value = scrubSecrets(safeString(error));
|
|
196
284
|
}
|
|
197
285
|
return {
|
|
198
286
|
event_id: uuid4(),
|
|
@@ -210,12 +298,13 @@ function eventFromMessage(message, level = "info") {
|
|
|
210
298
|
event_id: uuid4(),
|
|
211
299
|
timestamp: nowISO(),
|
|
212
300
|
level,
|
|
213
|
-
message
|
|
301
|
+
message: scrubSecrets(message)
|
|
214
302
|
};
|
|
215
303
|
}
|
|
216
304
|
|
|
217
305
|
// src/transport.ts
|
|
218
306
|
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
307
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
219
308
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
220
309
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
221
310
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
@@ -282,11 +371,13 @@ var Transport = class {
|
|
|
282
371
|
}
|
|
283
372
|
async sendWithRetry(envelope) {
|
|
284
373
|
for (let attempt = 0; ; attempt++) {
|
|
374
|
+
let serverDelay;
|
|
285
375
|
try {
|
|
286
376
|
const response = await fetch(this.endpoint, {
|
|
287
377
|
method: "POST",
|
|
288
378
|
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
289
|
-
body: JSON.stringify(envelope)
|
|
379
|
+
body: JSON.stringify(envelope),
|
|
380
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
290
381
|
});
|
|
291
382
|
if (response.ok) {
|
|
292
383
|
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
@@ -296,13 +387,15 @@ var Transport = class {
|
|
|
296
387
|
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
297
388
|
return;
|
|
298
389
|
}
|
|
390
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
299
391
|
throw new Error(`HTTP ${response.status}`);
|
|
300
392
|
} catch (error) {
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
393
|
+
const base = RETRY_DELAYS_MS[attempt];
|
|
394
|
+
if (base === void 0) {
|
|
303
395
|
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
304
396
|
return;
|
|
305
397
|
}
|
|
398
|
+
const delay = serverDelay ?? jitter(base);
|
|
306
399
|
this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
|
|
307
400
|
await sleep(delay);
|
|
308
401
|
}
|
|
@@ -332,22 +425,38 @@ function runtimeContexts() {
|
|
|
332
425
|
return { runtime: { name: "browser" } };
|
|
333
426
|
}
|
|
334
427
|
var Client = class {
|
|
335
|
-
options;
|
|
336
|
-
config;
|
|
337
428
|
logger;
|
|
429
|
+
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
430
|
+
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
431
|
+
// que referencie o client), mas client.options/config seguem funcionando
|
|
432
|
+
#options;
|
|
433
|
+
#config;
|
|
338
434
|
transport;
|
|
339
435
|
constructor(options) {
|
|
340
|
-
this
|
|
436
|
+
this.#options = options;
|
|
341
437
|
this.logger = makeLogger(!!options.debug);
|
|
342
|
-
this
|
|
438
|
+
this.#config = resolveConfig(options);
|
|
343
439
|
this.transport = new Transport({
|
|
344
|
-
endpoint: this
|
|
345
|
-
key: this
|
|
440
|
+
endpoint: this.#config.ingestEndpoint,
|
|
441
|
+
key: this.#config.key,
|
|
346
442
|
logger: this.logger,
|
|
347
443
|
flushIntervalMs: options.flushIntervalMs,
|
|
348
444
|
maxBatchSize: options.maxBatchSize
|
|
349
445
|
});
|
|
350
446
|
}
|
|
447
|
+
get options() {
|
|
448
|
+
return this.#options;
|
|
449
|
+
}
|
|
450
|
+
get config() {
|
|
451
|
+
return this.#config;
|
|
452
|
+
}
|
|
453
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
454
|
+
toJSON() {
|
|
455
|
+
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
456
|
+
}
|
|
457
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
458
|
+
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
459
|
+
}
|
|
351
460
|
captureException(error, hint, scope2) {
|
|
352
461
|
return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
|
|
353
462
|
}
|
|
@@ -601,6 +710,15 @@ var Scope = class _Scope {
|
|
|
601
710
|
function spanId() {
|
|
602
711
|
return uuid4().slice(0, 16);
|
|
603
712
|
}
|
|
713
|
+
function printSpanLog(kind, phase, label, startTs, endTs) {
|
|
714
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
715
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? "green" : "cyan");
|
|
716
|
+
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
717
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
718
|
+
}
|
|
719
|
+
function spanLabel(op, description) {
|
|
720
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
721
|
+
}
|
|
604
722
|
var Span = class {
|
|
605
723
|
constructor(transaction, parentSpanId, op, description) {
|
|
606
724
|
this.transaction = transaction;
|
|
@@ -626,7 +744,9 @@ var Span = class {
|
|
|
626
744
|
return this.transaction.createSpan(this.spanId, op, description);
|
|
627
745
|
}
|
|
628
746
|
finish() {
|
|
629
|
-
if (
|
|
747
|
+
if (this.endTimestamp) return;
|
|
748
|
+
this.endTimestamp = nowISO();
|
|
749
|
+
printSpanLog("span", "finish", spanLabel(this.op, this.description), this.startTimestamp, this.endTimestamp);
|
|
630
750
|
}
|
|
631
751
|
toJSON() {
|
|
632
752
|
return {
|
|
@@ -648,6 +768,7 @@ var Transaction = class {
|
|
|
648
768
|
this.op = op;
|
|
649
769
|
this.sampled = sampled;
|
|
650
770
|
this.runId = runId;
|
|
771
|
+
printSpanLog("transaction", "start", op ? `${name} (${op})` : name);
|
|
651
772
|
}
|
|
652
773
|
eventId = uuid4();
|
|
653
774
|
spanId = spanId();
|
|
@@ -659,6 +780,7 @@ var Transaction = class {
|
|
|
659
780
|
createSpan(parentSpanId, op, description) {
|
|
660
781
|
const span = new Span(this, parentSpanId, op, description);
|
|
661
782
|
this.spans.push(span);
|
|
783
|
+
printSpanLog("span", "start", spanLabel(op, description));
|
|
662
784
|
return span;
|
|
663
785
|
}
|
|
664
786
|
/** Span direto da transaction. */
|
|
@@ -681,6 +803,14 @@ var Transaction = class {
|
|
|
681
803
|
if (this.finished) return;
|
|
682
804
|
this.finished = true;
|
|
683
805
|
for (const span of this.spans) span.finish();
|
|
806
|
+
const endTimestamp = nowISO();
|
|
807
|
+
printSpanLog(
|
|
808
|
+
"transaction",
|
|
809
|
+
"finish",
|
|
810
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
811
|
+
this.startTimestamp,
|
|
812
|
+
endTimestamp
|
|
813
|
+
);
|
|
684
814
|
if (!this.sampled || !this.client) return;
|
|
685
815
|
const payload = {
|
|
686
816
|
event_id: this.eventId,
|
|
@@ -688,7 +818,7 @@ var Transaction = class {
|
|
|
688
818
|
op: this.op,
|
|
689
819
|
status: this.status,
|
|
690
820
|
start_timestamp: this.startTimestamp,
|
|
691
|
-
timestamp:
|
|
821
|
+
timestamp: endTimestamp,
|
|
692
822
|
spans: this.spans.map((span) => span.toJSON()),
|
|
693
823
|
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
694
824
|
run_id: this.runId
|
|
@@ -745,6 +875,8 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
|
|
|
745
875
|
|
|
746
876
|
// src/manager/api.ts
|
|
747
877
|
var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
|
|
878
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
879
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
748
880
|
var ManagerApiError = class extends Error {
|
|
749
881
|
constructor(message, status) {
|
|
750
882
|
super(message);
|
|
@@ -769,16 +901,39 @@ var ManagerApi = class {
|
|
|
769
901
|
}
|
|
770
902
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
771
903
|
async poll(request, signal) {
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
904
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
905
|
+
let response;
|
|
906
|
+
try {
|
|
907
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: this.headers(),
|
|
910
|
+
body: JSON.stringify(request),
|
|
911
|
+
signal: timeoutSignal
|
|
912
|
+
});
|
|
913
|
+
} finally {
|
|
914
|
+
cleanup();
|
|
915
|
+
}
|
|
778
916
|
if (!response.ok) {
|
|
779
917
|
throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
780
918
|
}
|
|
781
|
-
return await response.json();
|
|
919
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
920
|
+
}
|
|
921
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
922
|
+
parsePollResponse(body, max) {
|
|
923
|
+
const runs = body?.runs;
|
|
924
|
+
if (!Array.isArray(runs)) {
|
|
925
|
+
throw new ManagerApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
926
|
+
}
|
|
927
|
+
const valid = [];
|
|
928
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
929
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
930
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
931
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
932
|
+
}
|
|
933
|
+
if (valid.length < runs.length) {
|
|
934
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
935
|
+
}
|
|
936
|
+
return { runs: valid };
|
|
782
937
|
}
|
|
783
938
|
/**
|
|
784
939
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
@@ -787,25 +942,29 @@ var ManagerApi = class {
|
|
|
787
942
|
async updateRun(runId, update) {
|
|
788
943
|
for (let attempt = 0; ; attempt++) {
|
|
789
944
|
let retriable;
|
|
945
|
+
let serverDelay;
|
|
790
946
|
try {
|
|
791
947
|
const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
792
948
|
method: "PATCH",
|
|
793
949
|
headers: this.headers(),
|
|
794
|
-
body: JSON.stringify(update)
|
|
950
|
+
body: JSON.stringify(update),
|
|
951
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
795
952
|
});
|
|
796
953
|
if (response.ok) return;
|
|
797
954
|
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
798
955
|
throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
799
956
|
}
|
|
957
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
800
958
|
retriable = new Error(`HTTP ${response.status}`);
|
|
801
959
|
} catch (error) {
|
|
802
960
|
if (error instanceof ManagerApiError) throw error;
|
|
803
961
|
retriable = error;
|
|
804
962
|
}
|
|
805
|
-
const
|
|
806
|
-
if (
|
|
963
|
+
const base = RETRY_DELAYS_MS2[attempt];
|
|
964
|
+
if (base === void 0) {
|
|
807
965
|
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
808
966
|
}
|
|
967
|
+
const delay = serverDelay ?? jitter(base);
|
|
809
968
|
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
810
969
|
await sleep(delay);
|
|
811
970
|
}
|
|
@@ -830,11 +989,12 @@ var LOG_LEVEL_COLOR = {
|
|
|
830
989
|
debug: "gray",
|
|
831
990
|
info: "cyan",
|
|
832
991
|
warning: "yellow",
|
|
833
|
-
error: "red"
|
|
992
|
+
error: "red",
|
|
993
|
+
success: "green"
|
|
834
994
|
};
|
|
835
995
|
function printRunLog(task, level, message) {
|
|
836
996
|
const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
837
|
-
const method = level === "warning" ? "warn" : level;
|
|
997
|
+
const method = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
838
998
|
console[method](line, message);
|
|
839
999
|
}
|
|
840
1000
|
function runCanceledError() {
|
|
@@ -879,7 +1039,8 @@ var RunReporter = class {
|
|
|
879
1039
|
debug: (message) => this.log("debug", message),
|
|
880
1040
|
info: (message) => this.log("info", message),
|
|
881
1041
|
warn: (message) => this.log("warning", message),
|
|
882
|
-
error: (message) => this.log("error", message)
|
|
1042
|
+
error: (message) => this.log("error", message),
|
|
1043
|
+
success: (message) => this.log("success", message)
|
|
883
1044
|
};
|
|
884
1045
|
const signal = this.abortController.signal;
|
|
885
1046
|
this.context = {
|
|
@@ -927,13 +1088,14 @@ var RunReporter = class {
|
|
|
927
1088
|
}
|
|
928
1089
|
log(level, message) {
|
|
929
1090
|
if (this.dead) return;
|
|
930
|
-
|
|
1091
|
+
const scrubbed = scrubSecrets(message);
|
|
1092
|
+
printRunLog(this.context.task, level, scrubbed);
|
|
931
1093
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
932
1094
|
this.droppedCount++;
|
|
933
1095
|
return;
|
|
934
1096
|
}
|
|
935
1097
|
this.loggedCount++;
|
|
936
|
-
const truncated =
|
|
1098
|
+
const truncated = scrubbed.length > MAX_LOG_MESSAGE_CHARS ? `${scrubbed.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : scrubbed;
|
|
937
1099
|
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
938
1100
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
939
1101
|
this.flushLogs();
|
|
@@ -1117,8 +1279,8 @@ var ManagerAgent = class {
|
|
|
1117
1279
|
constructor(options, registry2) {
|
|
1118
1280
|
this.options = options;
|
|
1119
1281
|
this.registry = registry2;
|
|
1120
|
-
this.logger = makeLogger(options.debug ?? false, "[hobots
|
|
1121
|
-
const config = resolveConfig(options, "[hobots
|
|
1282
|
+
this.logger = makeLogger(options.debug ?? false, "[hobots]");
|
|
1283
|
+
const config = resolveConfig(options, "[hobots]");
|
|
1122
1284
|
this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
|
|
1123
1285
|
this.client = options.client;
|
|
1124
1286
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -1152,10 +1314,10 @@ var ManagerAgent = class {
|
|
|
1152
1314
|
start() {
|
|
1153
1315
|
if (this.state === "started") return;
|
|
1154
1316
|
if (this.state === "stopping") {
|
|
1155
|
-
throw new Error("[hobots
|
|
1317
|
+
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1156
1318
|
}
|
|
1157
1319
|
if (this.registry.size === 0) {
|
|
1158
|
-
throw new Error("[hobots
|
|
1320
|
+
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
1159
1321
|
}
|
|
1160
1322
|
this.state = "started";
|
|
1161
1323
|
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
@@ -1237,6 +1399,7 @@ var ManagerAgent = class {
|
|
|
1237
1399
|
this.consecutiveFailures = 0;
|
|
1238
1400
|
this.consecutiveAuthFailures = 0;
|
|
1239
1401
|
for (const run of response.runs) {
|
|
1402
|
+
if (this.activeRuns.size >= this.concurrency) break;
|
|
1240
1403
|
this.dispatch(run);
|
|
1241
1404
|
}
|
|
1242
1405
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
@@ -1257,7 +1420,7 @@ var ManagerAgent = class {
|
|
|
1257
1420
|
}
|
|
1258
1421
|
this.consecutiveFailures++;
|
|
1259
1422
|
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1260
|
-
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1423
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1261
1424
|
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1262
1425
|
return delay;
|
|
1263
1426
|
}
|
|
@@ -1269,7 +1432,7 @@ var ManagerAgent = class {
|
|
|
1269
1432
|
this.activeRuns.add(promise);
|
|
1270
1433
|
}
|
|
1271
1434
|
async executeRun(run) {
|
|
1272
|
-
const tx = new Transaction(this.client, `run:${run.task}`, "
|
|
1435
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
|
|
1273
1436
|
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
1274
1437
|
this.reporters.add(reporter);
|
|
1275
1438
|
try {
|
|
@@ -1335,11 +1498,11 @@ function toRunError(error) {
|
|
|
1335
1498
|
if (error instanceof Error) {
|
|
1336
1499
|
return {
|
|
1337
1500
|
type: error.name,
|
|
1338
|
-
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1339
|
-
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1501
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
1502
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
1340
1503
|
};
|
|
1341
1504
|
}
|
|
1342
|
-
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1505
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1343
1506
|
}
|
|
1344
1507
|
|
|
1345
1508
|
// src/core.ts
|