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.cjs
CHANGED
|
@@ -32,9 +32,6 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
Client: () => Client,
|
|
34
34
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
35
|
-
ManagerAgent: () => ManagerAgent,
|
|
36
|
-
ManagerApi: () => ManagerApi,
|
|
37
|
-
ManagerApiError: () => ManagerApiError,
|
|
38
35
|
Scope: () => Scope,
|
|
39
36
|
Span: () => Span,
|
|
40
37
|
Transaction: () => Transaction,
|
|
@@ -47,7 +44,7 @@ __export(src_exports, {
|
|
|
47
44
|
getClient: () => getClient,
|
|
48
45
|
init: () => init,
|
|
49
46
|
register: () => register,
|
|
50
|
-
|
|
47
|
+
scrubSecrets: () => scrubSecrets,
|
|
51
48
|
setContext: () => setContext,
|
|
52
49
|
setExtra: () => setExtra,
|
|
53
50
|
setTag: () => setTag,
|
|
@@ -63,26 +60,52 @@ module.exports = __toCommonJS(src_exports);
|
|
|
63
60
|
|
|
64
61
|
// src/config.ts
|
|
65
62
|
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
63
|
+
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
66
64
|
function envBaseUrl() {
|
|
67
65
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
68
66
|
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
69
67
|
}
|
|
68
|
+
function isLoopbackHost(hostname2) {
|
|
69
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
70
|
+
}
|
|
71
|
+
var warnedInsecureBaseUrl = false;
|
|
72
|
+
function validateBaseUrl(raw, prefix) {
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
78
|
+
}
|
|
79
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
80
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
81
|
+
}
|
|
82
|
+
if (url.username || url.password) {
|
|
83
|
+
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`);
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
86
|
+
warnedInsecureBaseUrl = true;
|
|
87
|
+
console.warn(
|
|
88
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return raw.replace(/\/+$/, "");
|
|
92
|
+
}
|
|
70
93
|
function resolveConfig(options, prefix = "[hobots]") {
|
|
71
94
|
const key = options.clientSecret?.trim();
|
|
72
95
|
if (!key) {
|
|
73
96
|
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
74
97
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
98
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${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`
|
|
101
|
+
);
|
|
80
102
|
}
|
|
103
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
81
104
|
return {
|
|
82
105
|
key,
|
|
83
106
|
baseUrl,
|
|
84
107
|
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
85
|
-
|
|
108
|
+
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
86
109
|
};
|
|
87
110
|
}
|
|
88
111
|
|
|
@@ -119,9 +142,28 @@ function parseStacktrace(stack) {
|
|
|
119
142
|
return frames;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
// src/scrub.ts
|
|
146
|
+
var SECRET_PATTERNS = [
|
|
147
|
+
// client secret da Hobots
|
|
148
|
+
[/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
|
|
149
|
+
// tokens bearer em headers/mensagens
|
|
150
|
+
[/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
|
|
151
|
+
// chaves no formato sk-… (OpenAI e afins)
|
|
152
|
+
[/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
|
|
153
|
+
// credenciais embutidas em URL (https://user:pass@host)
|
|
154
|
+
[/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
|
|
155
|
+
];
|
|
156
|
+
function scrubSecrets(text) {
|
|
157
|
+
let out = text;
|
|
158
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
159
|
+
out = out.replace(pattern, replacement);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
// src/utils.ts
|
|
123
165
|
var SDK_NAME = "hobots";
|
|
124
|
-
var SDK_VERSION = true ? "
|
|
166
|
+
var SDK_VERSION = true ? "2.0.0" : "0.0.0-dev";
|
|
125
167
|
function isNode() {
|
|
126
168
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
127
169
|
}
|
|
@@ -130,6 +172,12 @@ function uuid4() {
|
|
|
130
172
|
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
131
173
|
return cryptoRef.randomUUID().replace(/-/g, "");
|
|
132
174
|
}
|
|
175
|
+
if (cryptoRef && typeof cryptoRef.getRandomValues === "function") {
|
|
176
|
+
const bytes = cryptoRef.getRandomValues(new Uint8Array(16));
|
|
177
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
178
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
179
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
180
|
+
}
|
|
133
181
|
let out = "";
|
|
134
182
|
for (let i = 0; i < 32; i++) {
|
|
135
183
|
out += Math.floor(Math.random() * 16).toString(16);
|
|
@@ -148,7 +196,69 @@ function sleep(ms) {
|
|
|
148
196
|
unrefTimer(timer);
|
|
149
197
|
});
|
|
150
198
|
}
|
|
151
|
-
|
|
199
|
+
function jitter(baseMs) {
|
|
200
|
+
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
201
|
+
}
|
|
202
|
+
function truncate(value, max) {
|
|
203
|
+
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
204
|
+
}
|
|
205
|
+
var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
206
|
+
async function fetchWithRetry(send, options) {
|
|
207
|
+
const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
|
|
208
|
+
for (let attempt = 0; ; attempt++) {
|
|
209
|
+
let retriable;
|
|
210
|
+
let serverDelay;
|
|
211
|
+
try {
|
|
212
|
+
const response = await send();
|
|
213
|
+
if (response.ok) return response;
|
|
214
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
|
|
215
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
216
|
+
retriable = new Error(`HTTP ${response.status}`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
retriable = error;
|
|
219
|
+
}
|
|
220
|
+
const base = delays[attempt];
|
|
221
|
+
if (base === void 0) {
|
|
222
|
+
throw retriable instanceof Error ? retriable : new Error(String(retriable));
|
|
223
|
+
}
|
|
224
|
+
const delay = serverDelay ?? jitter(base);
|
|
225
|
+
options.logger.warn(`${options.label}: falha (${String(retriable)}), retry em ${delay}ms`);
|
|
226
|
+
await sleep(delay);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
230
|
+
function retryAfterMs(response) {
|
|
231
|
+
const header = response.headers.get("retry-after");
|
|
232
|
+
if (!header) return void 0;
|
|
233
|
+
const seconds = Number(header);
|
|
234
|
+
if (Number.isFinite(seconds)) {
|
|
235
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
|
|
236
|
+
}
|
|
237
|
+
const date = Date.parse(header);
|
|
238
|
+
if (Number.isNaN(date)) return void 0;
|
|
239
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
|
|
240
|
+
}
|
|
241
|
+
function withTimeoutSignal(timeoutMs, external) {
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
const onAbort = () => controller.abort(external?.reason);
|
|
244
|
+
if (external?.aborted) {
|
|
245
|
+
onAbort();
|
|
246
|
+
} else {
|
|
247
|
+
external?.addEventListener("abort", onAbort, { once: true });
|
|
248
|
+
}
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
|
|
251
|
+
}, timeoutMs);
|
|
252
|
+
unrefTimer(timer);
|
|
253
|
+
return {
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
cleanup: () => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
external?.removeEventListener("abort", onAbort);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var PREFIX = "[hobots]";
|
|
152
262
|
function makeLogger(enabled, prefix = PREFIX) {
|
|
153
263
|
return {
|
|
154
264
|
debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
|
|
@@ -189,11 +299,11 @@ function eventFromException(error, level = "error") {
|
|
|
189
299
|
let frames = [];
|
|
190
300
|
if (error instanceof Error) {
|
|
191
301
|
type = error.name || "Error";
|
|
192
|
-
value = error.message;
|
|
302
|
+
value = scrubSecrets(error.message);
|
|
193
303
|
frames = parseStacktrace(error.stack);
|
|
194
304
|
} else {
|
|
195
305
|
type = "NonError";
|
|
196
|
-
value = safeString(error);
|
|
306
|
+
value = scrubSecrets(safeString(error));
|
|
197
307
|
}
|
|
198
308
|
return {
|
|
199
309
|
event_id: uuid4(),
|
|
@@ -211,12 +321,12 @@ function eventFromMessage(message, level = "info") {
|
|
|
211
321
|
event_id: uuid4(),
|
|
212
322
|
timestamp: nowISO(),
|
|
213
323
|
level,
|
|
214
|
-
message
|
|
324
|
+
message: scrubSecrets(message)
|
|
215
325
|
};
|
|
216
326
|
}
|
|
217
327
|
|
|
218
328
|
// src/transport.ts
|
|
219
|
-
var
|
|
329
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
220
330
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
221
331
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
222
332
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
@@ -282,32 +392,26 @@ var Transport = class {
|
|
|
282
392
|
return request;
|
|
283
393
|
}
|
|
284
394
|
async sendWithRetry(envelope) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
395
|
+
let response;
|
|
396
|
+
try {
|
|
397
|
+
response = await fetchWithRetry(
|
|
398
|
+
() => fetch(this.endpoint, {
|
|
288
399
|
method: "POST",
|
|
289
400
|
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
290
|
-
body: JSON.stringify(envelope)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
throw new Error(`HTTP ${response.status}`);
|
|
301
|
-
} catch (error) {
|
|
302
|
-
const delay = RETRY_DELAYS_MS[attempt];
|
|
303
|
-
if (delay === void 0) {
|
|
304
|
-
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
|
|
308
|
-
await sleep(delay);
|
|
309
|
-
}
|
|
401
|
+
body: JSON.stringify(envelope),
|
|
402
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
403
|
+
}),
|
|
404
|
+
{ logger: this.logger, label: "envio de envelope" }
|
|
405
|
+
);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
408
|
+
return;
|
|
310
409
|
}
|
|
410
|
+
if (response.ok) {
|
|
411
|
+
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
311
415
|
}
|
|
312
416
|
/** Envia o que estiver na fila e aguarda envios em andamento (até timeoutMs). */
|
|
313
417
|
async flush(timeoutMs = 5e3) {
|
|
@@ -333,22 +437,38 @@ function runtimeContexts() {
|
|
|
333
437
|
return { runtime: { name: "browser" } };
|
|
334
438
|
}
|
|
335
439
|
var Client = class {
|
|
336
|
-
options;
|
|
337
|
-
config;
|
|
338
440
|
logger;
|
|
441
|
+
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
442
|
+
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
443
|
+
// que referencie o client), mas client.options/config seguem funcionando
|
|
444
|
+
#options;
|
|
445
|
+
#config;
|
|
339
446
|
transport;
|
|
340
|
-
constructor(options) {
|
|
341
|
-
this
|
|
447
|
+
constructor(options, config = resolveConfig(options)) {
|
|
448
|
+
this.#options = options;
|
|
342
449
|
this.logger = makeLogger(!!options.debug);
|
|
343
|
-
this
|
|
450
|
+
this.#config = config;
|
|
344
451
|
this.transport = new Transport({
|
|
345
|
-
endpoint: this
|
|
346
|
-
key: this
|
|
452
|
+
endpoint: this.#config.ingestEndpoint,
|
|
453
|
+
key: this.#config.key,
|
|
347
454
|
logger: this.logger,
|
|
348
455
|
flushIntervalMs: options.flushIntervalMs,
|
|
349
456
|
maxBatchSize: options.maxBatchSize
|
|
350
457
|
});
|
|
351
458
|
}
|
|
459
|
+
get options() {
|
|
460
|
+
return this.#options;
|
|
461
|
+
}
|
|
462
|
+
get config() {
|
|
463
|
+
return this.#config;
|
|
464
|
+
}
|
|
465
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
466
|
+
toJSON() {
|
|
467
|
+
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
468
|
+
}
|
|
469
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
470
|
+
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
471
|
+
}
|
|
352
472
|
captureException(error, hint, scope2) {
|
|
353
473
|
return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
|
|
354
474
|
}
|
|
@@ -733,48 +853,31 @@ function sendCheckIn(client2, options) {
|
|
|
733
853
|
return checkInId;
|
|
734
854
|
}
|
|
735
855
|
async function withMonitorImpl(client2, captureError, monitorSlug, fn, options) {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
status: "in_progress",
|
|
739
|
-
schedule: options?.schedule,
|
|
740
|
-
timezone: options?.timezone
|
|
741
|
-
});
|
|
856
|
+
const base = { monitorSlug, schedule: options?.schedule, timezone: options?.timezone };
|
|
857
|
+
const checkInId = sendCheckIn(client2, { ...base, status: "in_progress" });
|
|
742
858
|
const startedAt = Date.now();
|
|
743
859
|
try {
|
|
744
860
|
const result = await fn();
|
|
745
|
-
sendCheckIn(client2, {
|
|
746
|
-
monitorSlug,
|
|
747
|
-
status: "ok",
|
|
748
|
-
checkInId,
|
|
749
|
-
durationMs: Date.now() - startedAt,
|
|
750
|
-
schedule: options?.schedule,
|
|
751
|
-
timezone: options?.timezone
|
|
752
|
-
});
|
|
861
|
+
sendCheckIn(client2, { ...base, status: "ok", checkInId, durationMs: Date.now() - startedAt });
|
|
753
862
|
return result;
|
|
754
863
|
} catch (error) {
|
|
755
|
-
sendCheckIn(client2, {
|
|
756
|
-
monitorSlug,
|
|
757
|
-
status: "error",
|
|
758
|
-
checkInId,
|
|
759
|
-
durationMs: Date.now() - startedAt,
|
|
760
|
-
schedule: options?.schedule,
|
|
761
|
-
timezone: options?.timezone
|
|
762
|
-
});
|
|
864
|
+
sendCheckIn(client2, { ...base, status: "error", checkInId, durationMs: Date.now() - startedAt });
|
|
763
865
|
captureError(error);
|
|
764
866
|
throw error;
|
|
765
867
|
}
|
|
766
868
|
}
|
|
767
869
|
|
|
768
|
-
// src/
|
|
769
|
-
var
|
|
770
|
-
var
|
|
870
|
+
// src/tasks/api.ts
|
|
871
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
872
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
873
|
+
var TasksApiError = class extends Error {
|
|
771
874
|
constructor(message, status) {
|
|
772
875
|
super(message);
|
|
773
876
|
this.status = status;
|
|
774
|
-
this.name = "
|
|
877
|
+
this.name = "TasksApiError";
|
|
775
878
|
}
|
|
776
879
|
};
|
|
777
|
-
var
|
|
880
|
+
var TasksApi = class {
|
|
778
881
|
baseUrl;
|
|
779
882
|
key;
|
|
780
883
|
logger;
|
|
@@ -791,45 +894,61 @@ var ManagerApi = class {
|
|
|
791
894
|
}
|
|
792
895
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
793
896
|
async poll(request, signal) {
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
897
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
898
|
+
let response;
|
|
899
|
+
try {
|
|
900
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: this.headers(),
|
|
903
|
+
body: JSON.stringify(request),
|
|
904
|
+
signal: timeoutSignal
|
|
905
|
+
});
|
|
906
|
+
} finally {
|
|
907
|
+
cleanup();
|
|
908
|
+
}
|
|
800
909
|
if (!response.ok) {
|
|
801
|
-
throw new
|
|
910
|
+
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
911
|
+
}
|
|
912
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
913
|
+
}
|
|
914
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
915
|
+
parsePollResponse(body, max) {
|
|
916
|
+
const runs = body?.runs;
|
|
917
|
+
if (!Array.isArray(runs)) {
|
|
918
|
+
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
919
|
+
}
|
|
920
|
+
const valid = [];
|
|
921
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
922
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
923
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
924
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
925
|
+
}
|
|
926
|
+
if (valid.length < runs.length) {
|
|
927
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
802
928
|
}
|
|
803
|
-
return
|
|
929
|
+
return { runs: valid };
|
|
804
930
|
}
|
|
805
931
|
/**
|
|
806
932
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
807
|
-
* 4xx é definitivo e vira
|
|
933
|
+
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
808
934
|
*/
|
|
809
935
|
async updateRun(runId, update) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
936
|
+
let response;
|
|
937
|
+
try {
|
|
938
|
+
response = await fetchWithRetry(
|
|
939
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
814
940
|
method: "PATCH",
|
|
815
941
|
headers: this.headers(),
|
|
816
|
-
body: JSON.stringify(update)
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
const delay = RETRY_DELAYS_MS2[attempt];
|
|
828
|
-
if (delay === void 0) {
|
|
829
|
-
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
830
|
-
}
|
|
831
|
-
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
832
|
-
await sleep(delay);
|
|
942
|
+
body: JSON.stringify(update),
|
|
943
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
944
|
+
}),
|
|
945
|
+
{ logger: this.logger, label: `update da run ${runId}` }
|
|
946
|
+
);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
949
|
+
}
|
|
950
|
+
if (!response.ok) {
|
|
951
|
+
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
833
952
|
}
|
|
834
953
|
}
|
|
835
954
|
/**
|
|
@@ -847,7 +966,7 @@ var ManagerApi = class {
|
|
|
847
966
|
}
|
|
848
967
|
};
|
|
849
968
|
|
|
850
|
-
// src/
|
|
969
|
+
// src/tasks/context.ts
|
|
851
970
|
var LOG_LEVEL_COLOR = {
|
|
852
971
|
debug: "gray",
|
|
853
972
|
info: "cyan",
|
|
@@ -951,14 +1070,14 @@ var RunReporter = class {
|
|
|
951
1070
|
}
|
|
952
1071
|
log(level, message) {
|
|
953
1072
|
if (this.dead) return;
|
|
954
|
-
|
|
1073
|
+
const scrubbed = scrubSecrets(message);
|
|
1074
|
+
printRunLog(this.context.task, level, scrubbed);
|
|
955
1075
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
956
1076
|
this.droppedCount++;
|
|
957
1077
|
return;
|
|
958
1078
|
}
|
|
959
1079
|
this.loggedCount++;
|
|
960
|
-
|
|
961
|
-
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
1080
|
+
this.buffer.push({ ts: nowISO(), level, message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS) });
|
|
962
1081
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
963
1082
|
this.flushLogs();
|
|
964
1083
|
} else {
|
|
@@ -1049,7 +1168,7 @@ var RunReporter = class {
|
|
|
1049
1168
|
try {
|
|
1050
1169
|
await this.api.updateRun(this.runId, payload);
|
|
1051
1170
|
} catch (error) {
|
|
1052
|
-
if (error instanceof
|
|
1171
|
+
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
1053
1172
|
this.dead = true;
|
|
1054
1173
|
this.stopCancelWatch();
|
|
1055
1174
|
this.abort();
|
|
@@ -1065,7 +1184,7 @@ var RunReporter = class {
|
|
|
1065
1184
|
}
|
|
1066
1185
|
};
|
|
1067
1186
|
|
|
1068
|
-
// src/
|
|
1187
|
+
// src/tasks/run-context-store.ts
|
|
1069
1188
|
var storage;
|
|
1070
1189
|
var initPromise;
|
|
1071
1190
|
async function ensureStorage() {
|
|
@@ -1075,15 +1194,15 @@ async function ensureStorage() {
|
|
|
1075
1194
|
});
|
|
1076
1195
|
await initPromise;
|
|
1077
1196
|
}
|
|
1078
|
-
async function
|
|
1197
|
+
async function runWithRunContext(store, fn) {
|
|
1079
1198
|
await ensureStorage();
|
|
1080
|
-
return storage ? storage.run(
|
|
1199
|
+
return storage ? storage.run(store, fn) : fn();
|
|
1081
1200
|
}
|
|
1082
|
-
function
|
|
1083
|
-
return storage?.getStore()?.
|
|
1201
|
+
function currentRunTransaction() {
|
|
1202
|
+
return storage?.getStore()?.tx;
|
|
1084
1203
|
}
|
|
1085
1204
|
|
|
1086
|
-
// src/
|
|
1205
|
+
// src/tasks/shutdown.ts
|
|
1087
1206
|
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
1088
1207
|
function installShutdownHandlers(agent2) {
|
|
1089
1208
|
if (!isNode()) return () => {
|
|
@@ -1127,7 +1246,7 @@ function installShutdownHandlers(agent2) {
|
|
|
1127
1246
|
};
|
|
1128
1247
|
}
|
|
1129
1248
|
|
|
1130
|
-
// src/
|
|
1249
|
+
// src/tasks/agent.ts
|
|
1131
1250
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1132
1251
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
1133
1252
|
var MAX_CONCURRENCY = 5;
|
|
@@ -1137,13 +1256,13 @@ function hostname() {
|
|
|
1137
1256
|
if (!isNode()) return void 0;
|
|
1138
1257
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
1139
1258
|
}
|
|
1140
|
-
var
|
|
1259
|
+
var TaskAgent = class {
|
|
1141
1260
|
constructor(options, registry2) {
|
|
1142
1261
|
this.options = options;
|
|
1143
1262
|
this.registry = registry2;
|
|
1144
|
-
this.logger = makeLogger(options.debug ?? false
|
|
1145
|
-
const config =
|
|
1146
|
-
this.api = new
|
|
1263
|
+
this.logger = makeLogger(options.debug ?? false);
|
|
1264
|
+
const config = options.config;
|
|
1265
|
+
this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
|
|
1147
1266
|
this.client = options.client;
|
|
1148
1267
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
1149
1268
|
this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
|
|
@@ -1151,7 +1270,7 @@ var ManagerAgent = class {
|
|
|
1151
1270
|
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
1152
1271
|
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1153
1272
|
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
1154
|
-
this.logger.debug("agente configurado", { baseUrl: config.
|
|
1273
|
+
this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
|
|
1155
1274
|
}
|
|
1156
1275
|
logger;
|
|
1157
1276
|
api;
|
|
@@ -1176,10 +1295,10 @@ var ManagerAgent = class {
|
|
|
1176
1295
|
start() {
|
|
1177
1296
|
if (this.state === "started") return;
|
|
1178
1297
|
if (this.state === "stopping") {
|
|
1179
|
-
throw new Error("[hobots
|
|
1298
|
+
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1180
1299
|
}
|
|
1181
1300
|
if (this.registry.size === 0) {
|
|
1182
|
-
throw new Error("[hobots
|
|
1301
|
+
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
1183
1302
|
}
|
|
1184
1303
|
this.state = "started";
|
|
1185
1304
|
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
@@ -1261,6 +1380,7 @@ var ManagerAgent = class {
|
|
|
1261
1380
|
this.consecutiveFailures = 0;
|
|
1262
1381
|
this.consecutiveAuthFailures = 0;
|
|
1263
1382
|
for (const run of response.runs) {
|
|
1383
|
+
if (this.activeRuns.size >= this.concurrency) break;
|
|
1264
1384
|
this.dispatch(run);
|
|
1265
1385
|
}
|
|
1266
1386
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
@@ -1272,7 +1392,7 @@ var ManagerAgent = class {
|
|
|
1272
1392
|
}
|
|
1273
1393
|
}
|
|
1274
1394
|
delayAfterPollError(error) {
|
|
1275
|
-
if (error instanceof
|
|
1395
|
+
if (error instanceof TasksApiError && error.status === 401) {
|
|
1276
1396
|
this.consecutiveAuthFailures++;
|
|
1277
1397
|
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
1278
1398
|
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos");
|
|
@@ -1281,7 +1401,7 @@ var ManagerAgent = class {
|
|
|
1281
1401
|
}
|
|
1282
1402
|
this.consecutiveFailures++;
|
|
1283
1403
|
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1284
|
-
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1404
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1285
1405
|
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1286
1406
|
return delay;
|
|
1287
1407
|
}
|
|
@@ -1293,7 +1413,7 @@ var ManagerAgent = class {
|
|
|
1293
1413
|
this.activeRuns.add(promise);
|
|
1294
1414
|
}
|
|
1295
1415
|
async executeRun(run) {
|
|
1296
|
-
const tx = new Transaction(this.client, `run:${run.task}`, "
|
|
1416
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
|
|
1297
1417
|
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
1298
1418
|
this.reporters.add(reporter);
|
|
1299
1419
|
try {
|
|
@@ -1317,7 +1437,7 @@ var ManagerAgent = class {
|
|
|
1317
1437
|
}
|
|
1318
1438
|
this.logger.debug(`executando run ${run.id} (${run.task})`);
|
|
1319
1439
|
try {
|
|
1320
|
-
await
|
|
1440
|
+
await runWithRunContext({ runId: run.id, tx }, () => this.invokeWithTimeout(handler, run, reporter));
|
|
1321
1441
|
reporter.reportSuccess();
|
|
1322
1442
|
tx.setStatus("ok");
|
|
1323
1443
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
@@ -1352,18 +1472,15 @@ var ManagerAgent = class {
|
|
|
1352
1472
|
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
1353
1473
|
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
1354
1474
|
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
1355
|
-
function truncate(value, max) {
|
|
1356
|
-
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
1357
|
-
}
|
|
1358
1475
|
function toRunError(error) {
|
|
1359
1476
|
if (error instanceof Error) {
|
|
1360
1477
|
return {
|
|
1361
1478
|
type: error.name,
|
|
1362
|
-
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1363
|
-
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1479
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
1480
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
1364
1481
|
};
|
|
1365
1482
|
}
|
|
1366
|
-
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1483
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1367
1484
|
}
|
|
1368
1485
|
|
|
1369
1486
|
// src/core.ts
|
|
@@ -1373,7 +1490,7 @@ var scope = new Scope();
|
|
|
1373
1490
|
var heartbeat;
|
|
1374
1491
|
var agent;
|
|
1375
1492
|
var registry = /* @__PURE__ */ new Map();
|
|
1376
|
-
var
|
|
1493
|
+
var warnedTasksDisabled = false;
|
|
1377
1494
|
function init(options) {
|
|
1378
1495
|
if (client) {
|
|
1379
1496
|
client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
|
|
@@ -1384,8 +1501,9 @@ function init(options) {
|
|
|
1384
1501
|
void agent.stop(0);
|
|
1385
1502
|
agent = void 0;
|
|
1386
1503
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1504
|
+
warnedTasksDisabled = false;
|
|
1505
|
+
const config = resolveConfig(options);
|
|
1506
|
+
client = new Client(options, config);
|
|
1389
1507
|
if (options.captureUnhandled !== false) {
|
|
1390
1508
|
installGlobalHandlers(client, () => scope);
|
|
1391
1509
|
}
|
|
@@ -1394,10 +1512,10 @@ function init(options) {
|
|
|
1394
1512
|
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
1395
1513
|
heartbeat.start();
|
|
1396
1514
|
}
|
|
1397
|
-
if (options.
|
|
1398
|
-
const
|
|
1399
|
-
agent = new
|
|
1400
|
-
{ ...
|
|
1515
|
+
if (options.tasks) {
|
|
1516
|
+
const tasksOptions = options.tasks === true ? {} : options.tasks;
|
|
1517
|
+
agent = new TaskAgent(
|
|
1518
|
+
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client },
|
|
1401
1519
|
registry
|
|
1402
1520
|
);
|
|
1403
1521
|
}
|
|
@@ -1440,9 +1558,14 @@ function withScope(callback) {
|
|
|
1440
1558
|
}
|
|
1441
1559
|
}
|
|
1442
1560
|
function startTransaction(name, op) {
|
|
1561
|
+
const active = currentRunTransaction();
|
|
1562
|
+
if (active) {
|
|
1563
|
+
client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
|
|
1564
|
+
return active;
|
|
1565
|
+
}
|
|
1443
1566
|
const rate = client?.options.tracesSampleRate ?? 1;
|
|
1444
1567
|
const sampled = rate >= 1 || Math.random() < rate;
|
|
1445
|
-
return new Transaction(client, name, op, sampled
|
|
1568
|
+
return new Transaction(client, name, op, sampled);
|
|
1446
1569
|
}
|
|
1447
1570
|
function checkIn(options) {
|
|
1448
1571
|
return client ? sendCheckIn(client, options) : void 0;
|
|
@@ -1464,17 +1587,17 @@ function register(task, handler) {
|
|
|
1464
1587
|
if (registry.has(task)) {
|
|
1465
1588
|
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
1466
1589
|
}
|
|
1467
|
-
if (client && !agent && !
|
|
1468
|
-
|
|
1590
|
+
if (client && !agent && !warnedTasksDisabled) {
|
|
1591
|
+
warnedTasksDisabled = true;
|
|
1469
1592
|
client.logger.warn(
|
|
1470
|
-
`register('${task}') com
|
|
1593
|
+
`register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
|
|
1471
1594
|
);
|
|
1472
1595
|
}
|
|
1473
1596
|
registry.set(task, handler);
|
|
1474
1597
|
}
|
|
1475
1598
|
function start() {
|
|
1476
1599
|
if (!agent) {
|
|
1477
|
-
throw new Error("[hobots]
|
|
1600
|
+
throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
1478
1601
|
}
|
|
1479
1602
|
agent.start();
|
|
1480
1603
|
}
|