hobots 1.5.1 → 1.7.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 +46 -74
- package/dist/index.cjs +267 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -123
- package/dist/index.d.ts +31 -123
- package/dist/index.js +267 -144
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +73 -4
- package/dist/protocol.d.ts +73 -4
- package/docs/README.md +8 -8
- package/docs/api-reference.md +40 -44
- package/docs/concepts.md +67 -52
- package/docs/getting-started.md +17 -17
- package/docs/monitor.md +8 -8
- package/docs/protocol.md +21 -25
- package/docs/{manager.md → tasks.md} +18 -18
- package/package.json +3 -7
- package/dist/manager/protocol.cjs +0 -19
- package/dist/manager/protocol.cjs.map +0 -1
- package/dist/manager/protocol.d.cts +0 -72
- package/dist/manager/protocol.d.ts +0 -72
- package/dist/manager/protocol.js +0 -1
- package/dist/manager/protocol.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
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,
|
|
21
47
|
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
22
|
-
|
|
48
|
+
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
23
49
|
};
|
|
24
50
|
}
|
|
25
51
|
|
|
@@ -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 ? "
|
|
106
|
+
var SDK_VERSION = true ? "2.0.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,69 @@ 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
|
+
function truncate(value, max) {
|
|
143
|
+
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
144
|
+
}
|
|
145
|
+
var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
146
|
+
async function fetchWithRetry(send, options) {
|
|
147
|
+
const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
|
|
148
|
+
for (let attempt = 0; ; attempt++) {
|
|
149
|
+
let retriable;
|
|
150
|
+
let serverDelay;
|
|
151
|
+
try {
|
|
152
|
+
const response = await send();
|
|
153
|
+
if (response.ok) return response;
|
|
154
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
|
|
155
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
156
|
+
retriable = new Error(`HTTP ${response.status}`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
retriable = error;
|
|
159
|
+
}
|
|
160
|
+
const base = delays[attempt];
|
|
161
|
+
if (base === void 0) {
|
|
162
|
+
throw retriable instanceof Error ? retriable : new Error(String(retriable));
|
|
163
|
+
}
|
|
164
|
+
const delay = serverDelay ?? jitter(base);
|
|
165
|
+
options.logger.warn(`${options.label}: falha (${String(retriable)}), retry em ${delay}ms`);
|
|
166
|
+
await sleep(delay);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
170
|
+
function retryAfterMs(response) {
|
|
171
|
+
const header = response.headers.get("retry-after");
|
|
172
|
+
if (!header) return void 0;
|
|
173
|
+
const seconds = Number(header);
|
|
174
|
+
if (Number.isFinite(seconds)) {
|
|
175
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
|
|
176
|
+
}
|
|
177
|
+
const date = Date.parse(header);
|
|
178
|
+
if (Number.isNaN(date)) return void 0;
|
|
179
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
|
|
180
|
+
}
|
|
181
|
+
function withTimeoutSignal(timeoutMs, external) {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const onAbort = () => controller.abort(external?.reason);
|
|
184
|
+
if (external?.aborted) {
|
|
185
|
+
onAbort();
|
|
186
|
+
} else {
|
|
187
|
+
external?.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
}
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
unrefTimer(timer);
|
|
193
|
+
return {
|
|
194
|
+
signal: controller.signal,
|
|
195
|
+
cleanup: () => {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
external?.removeEventListener("abort", onAbort);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
var PREFIX = "[hobots]";
|
|
89
202
|
function makeLogger(enabled, prefix = PREFIX) {
|
|
90
203
|
return {
|
|
91
204
|
debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
|
|
@@ -126,11 +239,11 @@ function eventFromException(error, level = "error") {
|
|
|
126
239
|
let frames = [];
|
|
127
240
|
if (error instanceof Error) {
|
|
128
241
|
type = error.name || "Error";
|
|
129
|
-
value = error.message;
|
|
242
|
+
value = scrubSecrets(error.message);
|
|
130
243
|
frames = parseStacktrace(error.stack);
|
|
131
244
|
} else {
|
|
132
245
|
type = "NonError";
|
|
133
|
-
value = safeString(error);
|
|
246
|
+
value = scrubSecrets(safeString(error));
|
|
134
247
|
}
|
|
135
248
|
return {
|
|
136
249
|
event_id: uuid4(),
|
|
@@ -148,12 +261,12 @@ function eventFromMessage(message, level = "info") {
|
|
|
148
261
|
event_id: uuid4(),
|
|
149
262
|
timestamp: nowISO(),
|
|
150
263
|
level,
|
|
151
|
-
message
|
|
264
|
+
message: scrubSecrets(message)
|
|
152
265
|
};
|
|
153
266
|
}
|
|
154
267
|
|
|
155
268
|
// src/transport.ts
|
|
156
|
-
var
|
|
269
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
157
270
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
158
271
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
159
272
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
@@ -219,32 +332,26 @@ var Transport = class {
|
|
|
219
332
|
return request;
|
|
220
333
|
}
|
|
221
334
|
async sendWithRetry(envelope) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
335
|
+
let response;
|
|
336
|
+
try {
|
|
337
|
+
response = await fetchWithRetry(
|
|
338
|
+
() => fetch(this.endpoint, {
|
|
225
339
|
method: "POST",
|
|
226
340
|
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
227
|
-
body: JSON.stringify(envelope)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
throw new Error(`HTTP ${response.status}`);
|
|
238
|
-
} catch (error) {
|
|
239
|
-
const delay = RETRY_DELAYS_MS[attempt];
|
|
240
|
-
if (delay === void 0) {
|
|
241
|
-
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
|
|
245
|
-
await sleep(delay);
|
|
246
|
-
}
|
|
341
|
+
body: JSON.stringify(envelope),
|
|
342
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
343
|
+
}),
|
|
344
|
+
{ logger: this.logger, label: "envio de envelope" }
|
|
345
|
+
);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
348
|
+
return;
|
|
247
349
|
}
|
|
350
|
+
if (response.ok) {
|
|
351
|
+
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
248
355
|
}
|
|
249
356
|
/** Envia o que estiver na fila e aguarda envios em andamento (até timeoutMs). */
|
|
250
357
|
async flush(timeoutMs = 5e3) {
|
|
@@ -270,22 +377,38 @@ function runtimeContexts() {
|
|
|
270
377
|
return { runtime: { name: "browser" } };
|
|
271
378
|
}
|
|
272
379
|
var Client = class {
|
|
273
|
-
options;
|
|
274
|
-
config;
|
|
275
380
|
logger;
|
|
381
|
+
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
382
|
+
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
383
|
+
// que referencie o client), mas client.options/config seguem funcionando
|
|
384
|
+
#options;
|
|
385
|
+
#config;
|
|
276
386
|
transport;
|
|
277
|
-
constructor(options) {
|
|
278
|
-
this
|
|
387
|
+
constructor(options, config = resolveConfig(options)) {
|
|
388
|
+
this.#options = options;
|
|
279
389
|
this.logger = makeLogger(!!options.debug);
|
|
280
|
-
this
|
|
390
|
+
this.#config = config;
|
|
281
391
|
this.transport = new Transport({
|
|
282
|
-
endpoint: this
|
|
283
|
-
key: this
|
|
392
|
+
endpoint: this.#config.ingestEndpoint,
|
|
393
|
+
key: this.#config.key,
|
|
284
394
|
logger: this.logger,
|
|
285
395
|
flushIntervalMs: options.flushIntervalMs,
|
|
286
396
|
maxBatchSize: options.maxBatchSize
|
|
287
397
|
});
|
|
288
398
|
}
|
|
399
|
+
get options() {
|
|
400
|
+
return this.#options;
|
|
401
|
+
}
|
|
402
|
+
get config() {
|
|
403
|
+
return this.#config;
|
|
404
|
+
}
|
|
405
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
406
|
+
toJSON() {
|
|
407
|
+
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
408
|
+
}
|
|
409
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
410
|
+
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
411
|
+
}
|
|
289
412
|
captureException(error, hint, scope2) {
|
|
290
413
|
return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
|
|
291
414
|
}
|
|
@@ -670,48 +793,31 @@ function sendCheckIn(client2, options) {
|
|
|
670
793
|
return checkInId;
|
|
671
794
|
}
|
|
672
795
|
async function withMonitorImpl(client2, captureError, monitorSlug, fn, options) {
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
status: "in_progress",
|
|
676
|
-
schedule: options?.schedule,
|
|
677
|
-
timezone: options?.timezone
|
|
678
|
-
});
|
|
796
|
+
const base = { monitorSlug, schedule: options?.schedule, timezone: options?.timezone };
|
|
797
|
+
const checkInId = sendCheckIn(client2, { ...base, status: "in_progress" });
|
|
679
798
|
const startedAt = Date.now();
|
|
680
799
|
try {
|
|
681
800
|
const result = await fn();
|
|
682
|
-
sendCheckIn(client2, {
|
|
683
|
-
monitorSlug,
|
|
684
|
-
status: "ok",
|
|
685
|
-
checkInId,
|
|
686
|
-
durationMs: Date.now() - startedAt,
|
|
687
|
-
schedule: options?.schedule,
|
|
688
|
-
timezone: options?.timezone
|
|
689
|
-
});
|
|
801
|
+
sendCheckIn(client2, { ...base, status: "ok", checkInId, durationMs: Date.now() - startedAt });
|
|
690
802
|
return result;
|
|
691
803
|
} catch (error) {
|
|
692
|
-
sendCheckIn(client2, {
|
|
693
|
-
monitorSlug,
|
|
694
|
-
status: "error",
|
|
695
|
-
checkInId,
|
|
696
|
-
durationMs: Date.now() - startedAt,
|
|
697
|
-
schedule: options?.schedule,
|
|
698
|
-
timezone: options?.timezone
|
|
699
|
-
});
|
|
804
|
+
sendCheckIn(client2, { ...base, status: "error", checkInId, durationMs: Date.now() - startedAt });
|
|
700
805
|
captureError(error);
|
|
701
806
|
throw error;
|
|
702
807
|
}
|
|
703
808
|
}
|
|
704
809
|
|
|
705
|
-
// src/
|
|
706
|
-
var
|
|
707
|
-
var
|
|
810
|
+
// src/tasks/api.ts
|
|
811
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
812
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
813
|
+
var TasksApiError = class extends Error {
|
|
708
814
|
constructor(message, status) {
|
|
709
815
|
super(message);
|
|
710
816
|
this.status = status;
|
|
711
|
-
this.name = "
|
|
817
|
+
this.name = "TasksApiError";
|
|
712
818
|
}
|
|
713
819
|
};
|
|
714
|
-
var
|
|
820
|
+
var TasksApi = class {
|
|
715
821
|
baseUrl;
|
|
716
822
|
key;
|
|
717
823
|
logger;
|
|
@@ -728,45 +834,61 @@ var ManagerApi = class {
|
|
|
728
834
|
}
|
|
729
835
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
730
836
|
async poll(request, signal) {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
837
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
838
|
+
let response;
|
|
839
|
+
try {
|
|
840
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
841
|
+
method: "POST",
|
|
842
|
+
headers: this.headers(),
|
|
843
|
+
body: JSON.stringify(request),
|
|
844
|
+
signal: timeoutSignal
|
|
845
|
+
});
|
|
846
|
+
} finally {
|
|
847
|
+
cleanup();
|
|
848
|
+
}
|
|
737
849
|
if (!response.ok) {
|
|
738
|
-
throw new
|
|
850
|
+
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
851
|
+
}
|
|
852
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
853
|
+
}
|
|
854
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
855
|
+
parsePollResponse(body, max) {
|
|
856
|
+
const runs = body?.runs;
|
|
857
|
+
if (!Array.isArray(runs)) {
|
|
858
|
+
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
859
|
+
}
|
|
860
|
+
const valid = [];
|
|
861
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
862
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
863
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
864
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
865
|
+
}
|
|
866
|
+
if (valid.length < runs.length) {
|
|
867
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
739
868
|
}
|
|
740
|
-
return
|
|
869
|
+
return { runs: valid };
|
|
741
870
|
}
|
|
742
871
|
/**
|
|
743
872
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
744
|
-
* 4xx é definitivo e vira
|
|
873
|
+
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
745
874
|
*/
|
|
746
875
|
async updateRun(runId, update) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
876
|
+
let response;
|
|
877
|
+
try {
|
|
878
|
+
response = await fetchWithRetry(
|
|
879
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
751
880
|
method: "PATCH",
|
|
752
881
|
headers: this.headers(),
|
|
753
|
-
body: JSON.stringify(update)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
const delay = RETRY_DELAYS_MS2[attempt];
|
|
765
|
-
if (delay === void 0) {
|
|
766
|
-
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
767
|
-
}
|
|
768
|
-
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
769
|
-
await sleep(delay);
|
|
882
|
+
body: JSON.stringify(update),
|
|
883
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
884
|
+
}),
|
|
885
|
+
{ logger: this.logger, label: `update da run ${runId}` }
|
|
886
|
+
);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
889
|
+
}
|
|
890
|
+
if (!response.ok) {
|
|
891
|
+
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
770
892
|
}
|
|
771
893
|
}
|
|
772
894
|
/**
|
|
@@ -784,7 +906,7 @@ var ManagerApi = class {
|
|
|
784
906
|
}
|
|
785
907
|
};
|
|
786
908
|
|
|
787
|
-
// src/
|
|
909
|
+
// src/tasks/context.ts
|
|
788
910
|
var LOG_LEVEL_COLOR = {
|
|
789
911
|
debug: "gray",
|
|
790
912
|
info: "cyan",
|
|
@@ -888,14 +1010,14 @@ var RunReporter = class {
|
|
|
888
1010
|
}
|
|
889
1011
|
log(level, message) {
|
|
890
1012
|
if (this.dead) return;
|
|
891
|
-
|
|
1013
|
+
const scrubbed = scrubSecrets(message);
|
|
1014
|
+
printRunLog(this.context.task, level, scrubbed);
|
|
892
1015
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
893
1016
|
this.droppedCount++;
|
|
894
1017
|
return;
|
|
895
1018
|
}
|
|
896
1019
|
this.loggedCount++;
|
|
897
|
-
|
|
898
|
-
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
1020
|
+
this.buffer.push({ ts: nowISO(), level, message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS) });
|
|
899
1021
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
900
1022
|
this.flushLogs();
|
|
901
1023
|
} else {
|
|
@@ -986,7 +1108,7 @@ var RunReporter = class {
|
|
|
986
1108
|
try {
|
|
987
1109
|
await this.api.updateRun(this.runId, payload);
|
|
988
1110
|
} catch (error) {
|
|
989
|
-
if (error instanceof
|
|
1111
|
+
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
990
1112
|
this.dead = true;
|
|
991
1113
|
this.stopCancelWatch();
|
|
992
1114
|
this.abort();
|
|
@@ -1002,7 +1124,7 @@ var RunReporter = class {
|
|
|
1002
1124
|
}
|
|
1003
1125
|
};
|
|
1004
1126
|
|
|
1005
|
-
// src/
|
|
1127
|
+
// src/tasks/run-context-store.ts
|
|
1006
1128
|
var storage;
|
|
1007
1129
|
var initPromise;
|
|
1008
1130
|
async function ensureStorage() {
|
|
@@ -1012,15 +1134,15 @@ async function ensureStorage() {
|
|
|
1012
1134
|
});
|
|
1013
1135
|
await initPromise;
|
|
1014
1136
|
}
|
|
1015
|
-
async function
|
|
1137
|
+
async function runWithRunContext(store, fn) {
|
|
1016
1138
|
await ensureStorage();
|
|
1017
|
-
return storage ? storage.run(
|
|
1139
|
+
return storage ? storage.run(store, fn) : fn();
|
|
1018
1140
|
}
|
|
1019
|
-
function
|
|
1020
|
-
return storage?.getStore()?.
|
|
1141
|
+
function currentRunTransaction() {
|
|
1142
|
+
return storage?.getStore()?.tx;
|
|
1021
1143
|
}
|
|
1022
1144
|
|
|
1023
|
-
// src/
|
|
1145
|
+
// src/tasks/shutdown.ts
|
|
1024
1146
|
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
1025
1147
|
function installShutdownHandlers(agent2) {
|
|
1026
1148
|
if (!isNode()) return () => {
|
|
@@ -1064,7 +1186,7 @@ function installShutdownHandlers(agent2) {
|
|
|
1064
1186
|
};
|
|
1065
1187
|
}
|
|
1066
1188
|
|
|
1067
|
-
// src/
|
|
1189
|
+
// src/tasks/agent.ts
|
|
1068
1190
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1069
1191
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
1070
1192
|
var MAX_CONCURRENCY = 5;
|
|
@@ -1074,13 +1196,13 @@ function hostname() {
|
|
|
1074
1196
|
if (!isNode()) return void 0;
|
|
1075
1197
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
1076
1198
|
}
|
|
1077
|
-
var
|
|
1199
|
+
var TaskAgent = class {
|
|
1078
1200
|
constructor(options, registry2) {
|
|
1079
1201
|
this.options = options;
|
|
1080
1202
|
this.registry = registry2;
|
|
1081
|
-
this.logger = makeLogger(options.debug ?? false
|
|
1082
|
-
const config =
|
|
1083
|
-
this.api = new
|
|
1203
|
+
this.logger = makeLogger(options.debug ?? false);
|
|
1204
|
+
const config = options.config;
|
|
1205
|
+
this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
|
|
1084
1206
|
this.client = options.client;
|
|
1085
1207
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
1086
1208
|
this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
|
|
@@ -1088,7 +1210,7 @@ var ManagerAgent = class {
|
|
|
1088
1210
|
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
1089
1211
|
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1090
1212
|
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
1091
|
-
this.logger.debug("agente configurado", { baseUrl: config.
|
|
1213
|
+
this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
|
|
1092
1214
|
}
|
|
1093
1215
|
logger;
|
|
1094
1216
|
api;
|
|
@@ -1113,10 +1235,10 @@ var ManagerAgent = class {
|
|
|
1113
1235
|
start() {
|
|
1114
1236
|
if (this.state === "started") return;
|
|
1115
1237
|
if (this.state === "stopping") {
|
|
1116
|
-
throw new Error("[hobots
|
|
1238
|
+
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1117
1239
|
}
|
|
1118
1240
|
if (this.registry.size === 0) {
|
|
1119
|
-
throw new Error("[hobots
|
|
1241
|
+
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
1120
1242
|
}
|
|
1121
1243
|
this.state = "started";
|
|
1122
1244
|
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
@@ -1198,6 +1320,7 @@ var ManagerAgent = class {
|
|
|
1198
1320
|
this.consecutiveFailures = 0;
|
|
1199
1321
|
this.consecutiveAuthFailures = 0;
|
|
1200
1322
|
for (const run of response.runs) {
|
|
1323
|
+
if (this.activeRuns.size >= this.concurrency) break;
|
|
1201
1324
|
this.dispatch(run);
|
|
1202
1325
|
}
|
|
1203
1326
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
@@ -1209,7 +1332,7 @@ var ManagerAgent = class {
|
|
|
1209
1332
|
}
|
|
1210
1333
|
}
|
|
1211
1334
|
delayAfterPollError(error) {
|
|
1212
|
-
if (error instanceof
|
|
1335
|
+
if (error instanceof TasksApiError && error.status === 401) {
|
|
1213
1336
|
this.consecutiveAuthFailures++;
|
|
1214
1337
|
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
1215
1338
|
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos");
|
|
@@ -1218,7 +1341,7 @@ var ManagerAgent = class {
|
|
|
1218
1341
|
}
|
|
1219
1342
|
this.consecutiveFailures++;
|
|
1220
1343
|
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1221
|
-
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1344
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1222
1345
|
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1223
1346
|
return delay;
|
|
1224
1347
|
}
|
|
@@ -1230,7 +1353,7 @@ var ManagerAgent = class {
|
|
|
1230
1353
|
this.activeRuns.add(promise);
|
|
1231
1354
|
}
|
|
1232
1355
|
async executeRun(run) {
|
|
1233
|
-
const tx = new Transaction(this.client, `run:${run.task}`, "
|
|
1356
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
|
|
1234
1357
|
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
1235
1358
|
this.reporters.add(reporter);
|
|
1236
1359
|
try {
|
|
@@ -1254,7 +1377,7 @@ var ManagerAgent = class {
|
|
|
1254
1377
|
}
|
|
1255
1378
|
this.logger.debug(`executando run ${run.id} (${run.task})`);
|
|
1256
1379
|
try {
|
|
1257
|
-
await
|
|
1380
|
+
await runWithRunContext({ runId: run.id, tx }, () => this.invokeWithTimeout(handler, run, reporter));
|
|
1258
1381
|
reporter.reportSuccess();
|
|
1259
1382
|
tx.setStatus("ok");
|
|
1260
1383
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
@@ -1289,18 +1412,15 @@ var ManagerAgent = class {
|
|
|
1289
1412
|
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
1290
1413
|
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
1291
1414
|
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
1292
|
-
function truncate(value, max) {
|
|
1293
|
-
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
1294
|
-
}
|
|
1295
1415
|
function toRunError(error) {
|
|
1296
1416
|
if (error instanceof Error) {
|
|
1297
1417
|
return {
|
|
1298
1418
|
type: error.name,
|
|
1299
|
-
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1300
|
-
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1419
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
1420
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
1301
1421
|
};
|
|
1302
1422
|
}
|
|
1303
|
-
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1423
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1304
1424
|
}
|
|
1305
1425
|
|
|
1306
1426
|
// src/core.ts
|
|
@@ -1310,7 +1430,7 @@ var scope = new Scope();
|
|
|
1310
1430
|
var heartbeat;
|
|
1311
1431
|
var agent;
|
|
1312
1432
|
var registry = /* @__PURE__ */ new Map();
|
|
1313
|
-
var
|
|
1433
|
+
var warnedTasksDisabled = false;
|
|
1314
1434
|
function init(options) {
|
|
1315
1435
|
if (client) {
|
|
1316
1436
|
client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
|
|
@@ -1321,8 +1441,9 @@ function init(options) {
|
|
|
1321
1441
|
void agent.stop(0);
|
|
1322
1442
|
agent = void 0;
|
|
1323
1443
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1444
|
+
warnedTasksDisabled = false;
|
|
1445
|
+
const config = resolveConfig(options);
|
|
1446
|
+
client = new Client(options, config);
|
|
1326
1447
|
if (options.captureUnhandled !== false) {
|
|
1327
1448
|
installGlobalHandlers(client, () => scope);
|
|
1328
1449
|
}
|
|
@@ -1331,10 +1452,10 @@ function init(options) {
|
|
|
1331
1452
|
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
1332
1453
|
heartbeat.start();
|
|
1333
1454
|
}
|
|
1334
|
-
if (options.
|
|
1335
|
-
const
|
|
1336
|
-
agent = new
|
|
1337
|
-
{ ...
|
|
1455
|
+
if (options.tasks) {
|
|
1456
|
+
const tasksOptions = options.tasks === true ? {} : options.tasks;
|
|
1457
|
+
agent = new TaskAgent(
|
|
1458
|
+
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client },
|
|
1338
1459
|
registry
|
|
1339
1460
|
);
|
|
1340
1461
|
}
|
|
@@ -1377,9 +1498,14 @@ function withScope(callback) {
|
|
|
1377
1498
|
}
|
|
1378
1499
|
}
|
|
1379
1500
|
function startTransaction(name, op) {
|
|
1501
|
+
const active = currentRunTransaction();
|
|
1502
|
+
if (active) {
|
|
1503
|
+
client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
|
|
1504
|
+
return active;
|
|
1505
|
+
}
|
|
1380
1506
|
const rate = client?.options.tracesSampleRate ?? 1;
|
|
1381
1507
|
const sampled = rate >= 1 || Math.random() < rate;
|
|
1382
|
-
return new Transaction(client, name, op, sampled
|
|
1508
|
+
return new Transaction(client, name, op, sampled);
|
|
1383
1509
|
}
|
|
1384
1510
|
function checkIn(options) {
|
|
1385
1511
|
return client ? sendCheckIn(client, options) : void 0;
|
|
@@ -1401,17 +1527,17 @@ function register(task, handler) {
|
|
|
1401
1527
|
if (registry.has(task)) {
|
|
1402
1528
|
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
1403
1529
|
}
|
|
1404
|
-
if (client && !agent && !
|
|
1405
|
-
|
|
1530
|
+
if (client && !agent && !warnedTasksDisabled) {
|
|
1531
|
+
warnedTasksDisabled = true;
|
|
1406
1532
|
client.logger.warn(
|
|
1407
|
-
`register('${task}') com
|
|
1533
|
+
`register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
|
|
1408
1534
|
);
|
|
1409
1535
|
}
|
|
1410
1536
|
registry.set(task, handler);
|
|
1411
1537
|
}
|
|
1412
1538
|
function start() {
|
|
1413
1539
|
if (!agent) {
|
|
1414
|
-
throw new Error("[hobots]
|
|
1540
|
+
throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
1415
1541
|
}
|
|
1416
1542
|
agent.start();
|
|
1417
1543
|
}
|
|
@@ -1428,9 +1554,6 @@ async function close(timeoutMs) {
|
|
|
1428
1554
|
export {
|
|
1429
1555
|
Client,
|
|
1430
1556
|
DEFAULT_BASE_URL,
|
|
1431
|
-
ManagerAgent,
|
|
1432
|
-
ManagerApi,
|
|
1433
|
-
ManagerApiError,
|
|
1434
1557
|
Scope,
|
|
1435
1558
|
Span,
|
|
1436
1559
|
Transaction,
|
|
@@ -1443,7 +1566,7 @@ export {
|
|
|
1443
1566
|
getClient,
|
|
1444
1567
|
init,
|
|
1445
1568
|
register,
|
|
1446
|
-
|
|
1569
|
+
scrubSecrets,
|
|
1447
1570
|
setContext,
|
|
1448
1571
|
setExtra,
|
|
1449
1572
|
setTag,
|