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/dist/index.d.ts
CHANGED
|
@@ -52,11 +52,14 @@ interface Logger {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
declare class Client {
|
|
55
|
-
|
|
56
|
-
readonly config: ResolvedConfig;
|
|
55
|
+
#private;
|
|
57
56
|
readonly logger: Logger;
|
|
58
57
|
private readonly transport;
|
|
59
58
|
constructor(options: InitOptions);
|
|
59
|
+
get options(): InitOptions;
|
|
60
|
+
get config(): ResolvedConfig;
|
|
61
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
62
|
+
toJSON(): Record<string, unknown>;
|
|
60
63
|
captureException(error: unknown, hint: CaptureHint | undefined, scope: Scope): string;
|
|
61
64
|
captureMessage(message: string, level: Severity, hint: CaptureHint | undefined, scope: Scope): string;
|
|
62
65
|
captureEvent(event: EventPayload, hint: CaptureHint | undefined, scope: Scope): string;
|
|
@@ -159,6 +162,7 @@ interface RunLogger {
|
|
|
159
162
|
info(message: string): void;
|
|
160
163
|
warn(message: string): void;
|
|
161
164
|
error(message: string): void;
|
|
165
|
+
success(message: string): void;
|
|
162
166
|
}
|
|
163
167
|
interface RunContext {
|
|
164
168
|
runId: string;
|
|
@@ -286,6 +290,8 @@ declare function stop(timeoutMs?: number): Promise<boolean>;
|
|
|
286
290
|
*/
|
|
287
291
|
declare function close(timeoutMs?: number): Promise<boolean>;
|
|
288
292
|
|
|
293
|
+
declare function scrubSecrets(text: string): string;
|
|
294
|
+
|
|
289
295
|
/**
|
|
290
296
|
* Loop de polling do Manager: busca runs pendentes, executa os handlers registrados
|
|
291
297
|
* e reporta status/logs/result. Ao contrário do Monitor, o timer de polling NÃO é
|
|
@@ -350,6 +356,8 @@ declare class ManagerApi {
|
|
|
350
356
|
private headers;
|
|
351
357
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
352
358
|
poll(request: ManagerPollRequest, signal?: AbortSignal): Promise<ManagerPollResponse>;
|
|
359
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
360
|
+
private parsePollResponse;
|
|
353
361
|
/**
|
|
354
362
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
355
363
|
* 4xx é definitivo e vira ManagerApiError imediato (409 = run cancelada/transição inválida).
|
|
@@ -363,4 +371,4 @@ declare class ManagerApi {
|
|
|
363
371
|
updateRunOnce(runId: string, update: ManagerRunUpdateRequest, timeoutMs?: number): Promise<void>;
|
|
364
372
|
}
|
|
365
373
|
|
|
366
|
-
export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, ManagerAgent, ManagerApi, ManagerApiError, type ManagerOptions, ManagerPollRequest, ManagerPollResponse, ManagerRunUpdateRequest, type ManagerTaskHandler, type ResolvedConfig, type RunContext, Scope, Severity, Span, SpanData, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, resolveConfig, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
|
|
374
|
+
export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, ManagerAgent, ManagerApi, ManagerApiError, type ManagerOptions, ManagerPollRequest, ManagerPollResponse, ManagerRunUpdateRequest, type ManagerTaskHandler, type ResolvedConfig, type RunContext, Scope, Severity, Span, SpanData, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, resolveConfig, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
3
|
+
var SECRET_PATTERN = /^(hb_)?[a-f0-9]{64}$/i;
|
|
3
4
|
function envBaseUrl() {
|
|
4
5
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5
6
|
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
6
7
|
}
|
|
8
|
+
function isLoopbackHost(hostname2) {
|
|
9
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
10
|
+
}
|
|
11
|
+
var warnedInsecureBaseUrl = false;
|
|
12
|
+
function validateBaseUrl(raw, prefix) {
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
18
|
+
}
|
|
19
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
20
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
21
|
+
}
|
|
22
|
+
if (url.username || url.password) {
|
|
23
|
+
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`);
|
|
24
|
+
}
|
|
25
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
26
|
+
warnedInsecureBaseUrl = true;
|
|
27
|
+
console.warn(
|
|
28
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return raw.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
7
33
|
function resolveConfig(options, prefix = "[hobots]") {
|
|
8
34
|
const key = options.clientSecret?.trim();
|
|
9
35
|
if (!key) {
|
|
10
36
|
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
11
37
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
38
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${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`
|
|
41
|
+
);
|
|
17
42
|
}
|
|
43
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
18
44
|
return {
|
|
19
45
|
key,
|
|
20
46
|
baseUrl,
|
|
@@ -56,9 +82,28 @@ function parseStacktrace(stack) {
|
|
|
56
82
|
return frames;
|
|
57
83
|
}
|
|
58
84
|
|
|
85
|
+
// src/scrub.ts
|
|
86
|
+
var SECRET_PATTERNS = [
|
|
87
|
+
// client secret da Hobots
|
|
88
|
+
[/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
|
|
89
|
+
// tokens bearer em headers/mensagens
|
|
90
|
+
[/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
|
|
91
|
+
// chaves no formato sk-… (OpenAI e afins)
|
|
92
|
+
[/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
|
|
93
|
+
// credenciais embutidas em URL (https://user:pass@host)
|
|
94
|
+
[/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
|
|
95
|
+
];
|
|
96
|
+
function scrubSecrets(text) {
|
|
97
|
+
let out = text;
|
|
98
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
99
|
+
out = out.replace(pattern, replacement);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
59
104
|
// src/utils.ts
|
|
60
105
|
var SDK_NAME = "hobots";
|
|
61
|
-
var SDK_VERSION = true ? "1.
|
|
106
|
+
var SDK_VERSION = true ? "1.6.0" : "0.0.0-dev";
|
|
62
107
|
function isNode() {
|
|
63
108
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
64
109
|
}
|
|
@@ -67,6 +112,12 @@ function uuid4() {
|
|
|
67
112
|
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
68
113
|
return cryptoRef.randomUUID().replace(/-/g, "");
|
|
69
114
|
}
|
|
115
|
+
if (cryptoRef && typeof cryptoRef.getRandomValues === "function") {
|
|
116
|
+
const bytes = cryptoRef.getRandomValues(new Uint8Array(16));
|
|
117
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
118
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
119
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
120
|
+
}
|
|
70
121
|
let out = "";
|
|
71
122
|
for (let i = 0; i < 32; i++) {
|
|
72
123
|
out += Math.floor(Math.random() * 16).toString(16);
|
|
@@ -85,7 +136,42 @@ function sleep(ms) {
|
|
|
85
136
|
unrefTimer(timer);
|
|
86
137
|
});
|
|
87
138
|
}
|
|
88
|
-
|
|
139
|
+
function jitter(baseMs) {
|
|
140
|
+
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
141
|
+
}
|
|
142
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
143
|
+
function retryAfterMs(response) {
|
|
144
|
+
const header = response.headers.get("retry-after");
|
|
145
|
+
if (!header) return void 0;
|
|
146
|
+
const seconds = Number(header);
|
|
147
|
+
if (Number.isFinite(seconds)) {
|
|
148
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
|
|
149
|
+
}
|
|
150
|
+
const date = Date.parse(header);
|
|
151
|
+
if (Number.isNaN(date)) return void 0;
|
|
152
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
|
|
153
|
+
}
|
|
154
|
+
function withTimeoutSignal(timeoutMs, external) {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const onAbort = () => controller.abort(external?.reason);
|
|
157
|
+
if (external?.aborted) {
|
|
158
|
+
onAbort();
|
|
159
|
+
} else {
|
|
160
|
+
external?.addEventListener("abort", onAbort, { once: true });
|
|
161
|
+
}
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
unrefTimer(timer);
|
|
166
|
+
return {
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
cleanup: () => {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
external?.removeEventListener("abort", onAbort);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
var PREFIX = "[hobots]";
|
|
89
175
|
function makeLogger(enabled, prefix = PREFIX) {
|
|
90
176
|
return {
|
|
91
177
|
debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
|
|
@@ -98,7 +184,8 @@ var ANSI = {
|
|
|
98
184
|
gray: "\x1B[90m",
|
|
99
185
|
cyan: "\x1B[36m",
|
|
100
186
|
yellow: "\x1B[33m",
|
|
101
|
-
red: "\x1B[31m"
|
|
187
|
+
red: "\x1B[31m",
|
|
188
|
+
green: "\x1B[32m"
|
|
102
189
|
};
|
|
103
190
|
function colorEnabled() {
|
|
104
191
|
if (!isNode()) return false;
|
|
@@ -125,11 +212,11 @@ function eventFromException(error, level = "error") {
|
|
|
125
212
|
let frames = [];
|
|
126
213
|
if (error instanceof Error) {
|
|
127
214
|
type = error.name || "Error";
|
|
128
|
-
value = error.message;
|
|
215
|
+
value = scrubSecrets(error.message);
|
|
129
216
|
frames = parseStacktrace(error.stack);
|
|
130
217
|
} else {
|
|
131
218
|
type = "NonError";
|
|
132
|
-
value = safeString(error);
|
|
219
|
+
value = scrubSecrets(safeString(error));
|
|
133
220
|
}
|
|
134
221
|
return {
|
|
135
222
|
event_id: uuid4(),
|
|
@@ -147,12 +234,13 @@ function eventFromMessage(message, level = "info") {
|
|
|
147
234
|
event_id: uuid4(),
|
|
148
235
|
timestamp: nowISO(),
|
|
149
236
|
level,
|
|
150
|
-
message
|
|
237
|
+
message: scrubSecrets(message)
|
|
151
238
|
};
|
|
152
239
|
}
|
|
153
240
|
|
|
154
241
|
// src/transport.ts
|
|
155
242
|
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
243
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
156
244
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
157
245
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
158
246
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
@@ -219,11 +307,13 @@ var Transport = class {
|
|
|
219
307
|
}
|
|
220
308
|
async sendWithRetry(envelope) {
|
|
221
309
|
for (let attempt = 0; ; attempt++) {
|
|
310
|
+
let serverDelay;
|
|
222
311
|
try {
|
|
223
312
|
const response = await fetch(this.endpoint, {
|
|
224
313
|
method: "POST",
|
|
225
314
|
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
226
|
-
body: JSON.stringify(envelope)
|
|
315
|
+
body: JSON.stringify(envelope),
|
|
316
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
227
317
|
});
|
|
228
318
|
if (response.ok) {
|
|
229
319
|
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
@@ -233,13 +323,15 @@ var Transport = class {
|
|
|
233
323
|
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
234
324
|
return;
|
|
235
325
|
}
|
|
326
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
236
327
|
throw new Error(`HTTP ${response.status}`);
|
|
237
328
|
} catch (error) {
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
329
|
+
const base = RETRY_DELAYS_MS[attempt];
|
|
330
|
+
if (base === void 0) {
|
|
240
331
|
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
241
332
|
return;
|
|
242
333
|
}
|
|
334
|
+
const delay = serverDelay ?? jitter(base);
|
|
243
335
|
this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
|
|
244
336
|
await sleep(delay);
|
|
245
337
|
}
|
|
@@ -269,22 +361,38 @@ function runtimeContexts() {
|
|
|
269
361
|
return { runtime: { name: "browser" } };
|
|
270
362
|
}
|
|
271
363
|
var Client = class {
|
|
272
|
-
options;
|
|
273
|
-
config;
|
|
274
364
|
logger;
|
|
365
|
+
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
366
|
+
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
367
|
+
// que referencie o client), mas client.options/config seguem funcionando
|
|
368
|
+
#options;
|
|
369
|
+
#config;
|
|
275
370
|
transport;
|
|
276
371
|
constructor(options) {
|
|
277
|
-
this
|
|
372
|
+
this.#options = options;
|
|
278
373
|
this.logger = makeLogger(!!options.debug);
|
|
279
|
-
this
|
|
374
|
+
this.#config = resolveConfig(options);
|
|
280
375
|
this.transport = new Transport({
|
|
281
|
-
endpoint: this
|
|
282
|
-
key: this
|
|
376
|
+
endpoint: this.#config.ingestEndpoint,
|
|
377
|
+
key: this.#config.key,
|
|
283
378
|
logger: this.logger,
|
|
284
379
|
flushIntervalMs: options.flushIntervalMs,
|
|
285
380
|
maxBatchSize: options.maxBatchSize
|
|
286
381
|
});
|
|
287
382
|
}
|
|
383
|
+
get options() {
|
|
384
|
+
return this.#options;
|
|
385
|
+
}
|
|
386
|
+
get config() {
|
|
387
|
+
return this.#config;
|
|
388
|
+
}
|
|
389
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
390
|
+
toJSON() {
|
|
391
|
+
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
392
|
+
}
|
|
393
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
394
|
+
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
395
|
+
}
|
|
288
396
|
captureException(error, hint, scope2) {
|
|
289
397
|
return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
|
|
290
398
|
}
|
|
@@ -538,6 +646,15 @@ var Scope = class _Scope {
|
|
|
538
646
|
function spanId() {
|
|
539
647
|
return uuid4().slice(0, 16);
|
|
540
648
|
}
|
|
649
|
+
function printSpanLog(kind, phase, label, startTs, endTs) {
|
|
650
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
651
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? "green" : "cyan");
|
|
652
|
+
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
653
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
654
|
+
}
|
|
655
|
+
function spanLabel(op, description) {
|
|
656
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
657
|
+
}
|
|
541
658
|
var Span = class {
|
|
542
659
|
constructor(transaction, parentSpanId, op, description) {
|
|
543
660
|
this.transaction = transaction;
|
|
@@ -563,7 +680,9 @@ var Span = class {
|
|
|
563
680
|
return this.transaction.createSpan(this.spanId, op, description);
|
|
564
681
|
}
|
|
565
682
|
finish() {
|
|
566
|
-
if (
|
|
683
|
+
if (this.endTimestamp) return;
|
|
684
|
+
this.endTimestamp = nowISO();
|
|
685
|
+
printSpanLog("span", "finish", spanLabel(this.op, this.description), this.startTimestamp, this.endTimestamp);
|
|
567
686
|
}
|
|
568
687
|
toJSON() {
|
|
569
688
|
return {
|
|
@@ -585,6 +704,7 @@ var Transaction = class {
|
|
|
585
704
|
this.op = op;
|
|
586
705
|
this.sampled = sampled;
|
|
587
706
|
this.runId = runId;
|
|
707
|
+
printSpanLog("transaction", "start", op ? `${name} (${op})` : name);
|
|
588
708
|
}
|
|
589
709
|
eventId = uuid4();
|
|
590
710
|
spanId = spanId();
|
|
@@ -596,6 +716,7 @@ var Transaction = class {
|
|
|
596
716
|
createSpan(parentSpanId, op, description) {
|
|
597
717
|
const span = new Span(this, parentSpanId, op, description);
|
|
598
718
|
this.spans.push(span);
|
|
719
|
+
printSpanLog("span", "start", spanLabel(op, description));
|
|
599
720
|
return span;
|
|
600
721
|
}
|
|
601
722
|
/** Span direto da transaction. */
|
|
@@ -618,6 +739,14 @@ var Transaction = class {
|
|
|
618
739
|
if (this.finished) return;
|
|
619
740
|
this.finished = true;
|
|
620
741
|
for (const span of this.spans) span.finish();
|
|
742
|
+
const endTimestamp = nowISO();
|
|
743
|
+
printSpanLog(
|
|
744
|
+
"transaction",
|
|
745
|
+
"finish",
|
|
746
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
747
|
+
this.startTimestamp,
|
|
748
|
+
endTimestamp
|
|
749
|
+
);
|
|
621
750
|
if (!this.sampled || !this.client) return;
|
|
622
751
|
const payload = {
|
|
623
752
|
event_id: this.eventId,
|
|
@@ -625,7 +754,7 @@ var Transaction = class {
|
|
|
625
754
|
op: this.op,
|
|
626
755
|
status: this.status,
|
|
627
756
|
start_timestamp: this.startTimestamp,
|
|
628
|
-
timestamp:
|
|
757
|
+
timestamp: endTimestamp,
|
|
629
758
|
spans: this.spans.map((span) => span.toJSON()),
|
|
630
759
|
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
631
760
|
run_id: this.runId
|
|
@@ -682,6 +811,8 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
|
|
|
682
811
|
|
|
683
812
|
// src/manager/api.ts
|
|
684
813
|
var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
|
|
814
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
815
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
685
816
|
var ManagerApiError = class extends Error {
|
|
686
817
|
constructor(message, status) {
|
|
687
818
|
super(message);
|
|
@@ -706,16 +837,39 @@ var ManagerApi = class {
|
|
|
706
837
|
}
|
|
707
838
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
708
839
|
async poll(request, signal) {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
840
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
841
|
+
let response;
|
|
842
|
+
try {
|
|
843
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
844
|
+
method: "POST",
|
|
845
|
+
headers: this.headers(),
|
|
846
|
+
body: JSON.stringify(request),
|
|
847
|
+
signal: timeoutSignal
|
|
848
|
+
});
|
|
849
|
+
} finally {
|
|
850
|
+
cleanup();
|
|
851
|
+
}
|
|
715
852
|
if (!response.ok) {
|
|
716
853
|
throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
717
854
|
}
|
|
718
|
-
return await response.json();
|
|
855
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
856
|
+
}
|
|
857
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
858
|
+
parsePollResponse(body, max) {
|
|
859
|
+
const runs = body?.runs;
|
|
860
|
+
if (!Array.isArray(runs)) {
|
|
861
|
+
throw new ManagerApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
862
|
+
}
|
|
863
|
+
const valid = [];
|
|
864
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
865
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
866
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
867
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
868
|
+
}
|
|
869
|
+
if (valid.length < runs.length) {
|
|
870
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
871
|
+
}
|
|
872
|
+
return { runs: valid };
|
|
719
873
|
}
|
|
720
874
|
/**
|
|
721
875
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
@@ -724,25 +878,29 @@ var ManagerApi = class {
|
|
|
724
878
|
async updateRun(runId, update) {
|
|
725
879
|
for (let attempt = 0; ; attempt++) {
|
|
726
880
|
let retriable;
|
|
881
|
+
let serverDelay;
|
|
727
882
|
try {
|
|
728
883
|
const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
729
884
|
method: "PATCH",
|
|
730
885
|
headers: this.headers(),
|
|
731
|
-
body: JSON.stringify(update)
|
|
886
|
+
body: JSON.stringify(update),
|
|
887
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
732
888
|
});
|
|
733
889
|
if (response.ok) return;
|
|
734
890
|
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
735
891
|
throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
736
892
|
}
|
|
893
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
737
894
|
retriable = new Error(`HTTP ${response.status}`);
|
|
738
895
|
} catch (error) {
|
|
739
896
|
if (error instanceof ManagerApiError) throw error;
|
|
740
897
|
retriable = error;
|
|
741
898
|
}
|
|
742
|
-
const
|
|
743
|
-
if (
|
|
899
|
+
const base = RETRY_DELAYS_MS2[attempt];
|
|
900
|
+
if (base === void 0) {
|
|
744
901
|
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
745
902
|
}
|
|
903
|
+
const delay = serverDelay ?? jitter(base);
|
|
746
904
|
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
747
905
|
await sleep(delay);
|
|
748
906
|
}
|
|
@@ -767,11 +925,12 @@ var LOG_LEVEL_COLOR = {
|
|
|
767
925
|
debug: "gray",
|
|
768
926
|
info: "cyan",
|
|
769
927
|
warning: "yellow",
|
|
770
|
-
error: "red"
|
|
928
|
+
error: "red",
|
|
929
|
+
success: "green"
|
|
771
930
|
};
|
|
772
931
|
function printRunLog(task, level, message) {
|
|
773
932
|
const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
774
|
-
const method = level === "warning" ? "warn" : level;
|
|
933
|
+
const method = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
775
934
|
console[method](line, message);
|
|
776
935
|
}
|
|
777
936
|
function runCanceledError() {
|
|
@@ -816,7 +975,8 @@ var RunReporter = class {
|
|
|
816
975
|
debug: (message) => this.log("debug", message),
|
|
817
976
|
info: (message) => this.log("info", message),
|
|
818
977
|
warn: (message) => this.log("warning", message),
|
|
819
|
-
error: (message) => this.log("error", message)
|
|
978
|
+
error: (message) => this.log("error", message),
|
|
979
|
+
success: (message) => this.log("success", message)
|
|
820
980
|
};
|
|
821
981
|
const signal = this.abortController.signal;
|
|
822
982
|
this.context = {
|
|
@@ -864,13 +1024,14 @@ var RunReporter = class {
|
|
|
864
1024
|
}
|
|
865
1025
|
log(level, message) {
|
|
866
1026
|
if (this.dead) return;
|
|
867
|
-
|
|
1027
|
+
const scrubbed = scrubSecrets(message);
|
|
1028
|
+
printRunLog(this.context.task, level, scrubbed);
|
|
868
1029
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
869
1030
|
this.droppedCount++;
|
|
870
1031
|
return;
|
|
871
1032
|
}
|
|
872
1033
|
this.loggedCount++;
|
|
873
|
-
const truncated =
|
|
1034
|
+
const truncated = scrubbed.length > MAX_LOG_MESSAGE_CHARS ? `${scrubbed.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : scrubbed;
|
|
874
1035
|
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
875
1036
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
876
1037
|
this.flushLogs();
|
|
@@ -1054,8 +1215,8 @@ var ManagerAgent = class {
|
|
|
1054
1215
|
constructor(options, registry2) {
|
|
1055
1216
|
this.options = options;
|
|
1056
1217
|
this.registry = registry2;
|
|
1057
|
-
this.logger = makeLogger(options.debug ?? false, "[hobots
|
|
1058
|
-
const config = resolveConfig(options, "[hobots
|
|
1218
|
+
this.logger = makeLogger(options.debug ?? false, "[hobots]");
|
|
1219
|
+
const config = resolveConfig(options, "[hobots]");
|
|
1059
1220
|
this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
|
|
1060
1221
|
this.client = options.client;
|
|
1061
1222
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -1089,10 +1250,10 @@ var ManagerAgent = class {
|
|
|
1089
1250
|
start() {
|
|
1090
1251
|
if (this.state === "started") return;
|
|
1091
1252
|
if (this.state === "stopping") {
|
|
1092
|
-
throw new Error("[hobots
|
|
1253
|
+
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1093
1254
|
}
|
|
1094
1255
|
if (this.registry.size === 0) {
|
|
1095
|
-
throw new Error("[hobots
|
|
1256
|
+
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
1096
1257
|
}
|
|
1097
1258
|
this.state = "started";
|
|
1098
1259
|
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
@@ -1174,6 +1335,7 @@ var ManagerAgent = class {
|
|
|
1174
1335
|
this.consecutiveFailures = 0;
|
|
1175
1336
|
this.consecutiveAuthFailures = 0;
|
|
1176
1337
|
for (const run of response.runs) {
|
|
1338
|
+
if (this.activeRuns.size >= this.concurrency) break;
|
|
1177
1339
|
this.dispatch(run);
|
|
1178
1340
|
}
|
|
1179
1341
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
@@ -1194,7 +1356,7 @@ var ManagerAgent = class {
|
|
|
1194
1356
|
}
|
|
1195
1357
|
this.consecutiveFailures++;
|
|
1196
1358
|
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1197
|
-
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1359
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1198
1360
|
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1199
1361
|
return delay;
|
|
1200
1362
|
}
|
|
@@ -1206,7 +1368,7 @@ var ManagerAgent = class {
|
|
|
1206
1368
|
this.activeRuns.add(promise);
|
|
1207
1369
|
}
|
|
1208
1370
|
async executeRun(run) {
|
|
1209
|
-
const tx = new Transaction(this.client, `run:${run.task}`, "
|
|
1371
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
|
|
1210
1372
|
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
1211
1373
|
this.reporters.add(reporter);
|
|
1212
1374
|
try {
|
|
@@ -1272,11 +1434,11 @@ function toRunError(error) {
|
|
|
1272
1434
|
if (error instanceof Error) {
|
|
1273
1435
|
return {
|
|
1274
1436
|
type: error.name,
|
|
1275
|
-
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1276
|
-
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1437
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
1438
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
1277
1439
|
};
|
|
1278
1440
|
}
|
|
1279
|
-
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1441
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1280
1442
|
}
|
|
1281
1443
|
|
|
1282
1444
|
// src/core.ts
|
|
@@ -1420,6 +1582,7 @@ export {
|
|
|
1420
1582
|
init,
|
|
1421
1583
|
register,
|
|
1422
1584
|
resolveConfig,
|
|
1585
|
+
scrubSecrets,
|
|
1423
1586
|
setContext,
|
|
1424
1587
|
setExtra,
|
|
1425
1588
|
setTag,
|