hobots 1.7.1 → 1.7.3

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/dist/index.d.cts CHANGED
@@ -1,10 +1,10 @@
1
- import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, CheckInPayload, HeartbeatPayload, SpanStatus, SpanData, CheckInStatus } from './protocol.cjs';
2
- export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
1
+ import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, CheckInPayload, HeartbeatPayload, RunLogLevel, SpanStatus, SpanData, CheckInStatus } from './protocol.cjs';
2
+ export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
3
3
 
4
4
  /** API oficial da Hobots — o SDK sempre fala com ela em produção. */
5
5
  declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
6
6
  interface ResolvedConfig {
7
- /** Client Secret do projeto (enviado como Authorization: Bearer, nunca na URL) */
7
+ /** Client Secret do agente (enviado como Authorization: Bearer, nunca na URL) */
8
8
  key: string;
9
9
  baseUrl: string;
10
10
  /** Ingestão de telemetria: <baseUrl>/v1/ingest */
@@ -64,6 +64,8 @@ declare class Client {
64
64
  close(timeoutMs?: number): Promise<boolean>;
65
65
  }
66
66
 
67
+ /** Recebe os eventos de lifecycle da transaction para espelhá-los nos logs da run. */
68
+ type LifecycleSink = (level: RunLogLevel, message: string) => void;
67
69
  declare class Span {
68
70
  private readonly transaction;
69
71
  readonly parentSpanId: string;
@@ -100,9 +102,19 @@ declare class Transaction {
100
102
  private readonly spans;
101
103
  private readonly tags;
102
104
  private finished;
105
+ private lifecycleSink?;
106
+ private lifecycleBacklog;
103
107
  constructor(client: Client | undefined, name: string, op?: string | undefined, sampled?: boolean,
104
108
  /** Correlaciona a uma execução de task — preenchido automaticamente em ctx.tx */
105
109
  runId?: string | undefined);
110
+ /**
111
+ * Ecoa no terminal o início/fim de um span ou transaction (sempre, independente de
112
+ * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
113
+ * assim que os eventos de performance também aparecem nos logs da run no app.
114
+ */
115
+ emit(kind: 'transaction' | 'span', phase: 'start' | 'finish', label: string, startTs?: string, endTs?: string): void;
116
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
117
+ setLifecycleSink(sink: LifecycleSink): void;
106
118
  createSpan(parentSpanId: string, op?: string, description?: string): Span;
107
119
  /** Span direto da transaction. */
108
120
  startChild(op?: string, description?: string): Span;
@@ -178,7 +190,7 @@ interface HeartbeatOptions {
178
190
  metadata?: Record<string, unknown>;
179
191
  }
180
192
  interface InitOptions {
181
- /** O Client Secret do projeto (hb_…) — a mesma credencial para telemetria e tasks. Copie em Configurações → Projetos. */
193
+ /** O Client Secret do agente (hb_…) — a mesma credencial para telemetria e tasks. Copie em Configurações → Agentes. */
182
194
  clientSecret: string;
183
195
  /** Default: 'production' */
184
196
  environment?: string;
@@ -272,4 +284,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
272
284
 
273
285
  declare function scrubSecrets(text: string): string;
274
286
 
275
- export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, Scope, Severity, Span, SpanData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
287
+ export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, RunLogLevel, Scope, Severity, Span, SpanData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, CheckInPayload, HeartbeatPayload, SpanStatus, SpanData, CheckInStatus } from './protocol.js';
2
- export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
1
+ import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, CheckInPayload, HeartbeatPayload, RunLogLevel, SpanStatus, SpanData, CheckInStatus } from './protocol.js';
2
+ export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
3
3
 
4
4
  /** API oficial da Hobots — o SDK sempre fala com ela em produção. */
5
5
  declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
6
6
  interface ResolvedConfig {
7
- /** Client Secret do projeto (enviado como Authorization: Bearer, nunca na URL) */
7
+ /** Client Secret do agente (enviado como Authorization: Bearer, nunca na URL) */
8
8
  key: string;
9
9
  baseUrl: string;
10
10
  /** Ingestão de telemetria: <baseUrl>/v1/ingest */
@@ -64,6 +64,8 @@ declare class Client {
64
64
  close(timeoutMs?: number): Promise<boolean>;
65
65
  }
66
66
 
67
+ /** Recebe os eventos de lifecycle da transaction para espelhá-los nos logs da run. */
68
+ type LifecycleSink = (level: RunLogLevel, message: string) => void;
67
69
  declare class Span {
68
70
  private readonly transaction;
69
71
  readonly parentSpanId: string;
@@ -100,9 +102,19 @@ declare class Transaction {
100
102
  private readonly spans;
101
103
  private readonly tags;
102
104
  private finished;
105
+ private lifecycleSink?;
106
+ private lifecycleBacklog;
103
107
  constructor(client: Client | undefined, name: string, op?: string | undefined, sampled?: boolean,
104
108
  /** Correlaciona a uma execução de task — preenchido automaticamente em ctx.tx */
105
109
  runId?: string | undefined);
110
+ /**
111
+ * Ecoa no terminal o início/fim de um span ou transaction (sempre, independente de
112
+ * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
113
+ * assim que os eventos de performance também aparecem nos logs da run no app.
114
+ */
115
+ emit(kind: 'transaction' | 'span', phase: 'start' | 'finish', label: string, startTs?: string, endTs?: string): void;
116
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
117
+ setLifecycleSink(sink: LifecycleSink): void;
106
118
  createSpan(parentSpanId: string, op?: string, description?: string): Span;
107
119
  /** Span direto da transaction. */
108
120
  startChild(op?: string, description?: string): Span;
@@ -178,7 +190,7 @@ interface HeartbeatOptions {
178
190
  metadata?: Record<string, unknown>;
179
191
  }
180
192
  interface InitOptions {
181
- /** O Client Secret do projeto (hb_…) — a mesma credencial para telemetria e tasks. Copie em Configurações → Projetos. */
193
+ /** O Client Secret do agente (hb_…) — a mesma credencial para telemetria e tasks. Copie em Configurações → Agentes. */
182
194
  clientSecret: string;
183
195
  /** Default: 'production' */
184
196
  environment?: string;
@@ -272,4 +284,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
272
284
 
273
285
  declare function scrubSecrets(text: string): string;
274
286
 
275
- export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, Scope, Severity, Span, SpanData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
287
+ export { Breadcrumb, type BreadcrumbInput, type CaptureHint, type CheckInOptions, CheckInPayload, CheckInStatus, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, RunLogLevel, Scope, Severity, Span, SpanData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, type WithMonitorOptions, addBreadcrumb, captureException, captureMessage, checkIn, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withMonitor, withScope };
package/dist/index.js CHANGED
@@ -33,11 +33,11 @@ function validateBaseUrl(raw, prefix) {
33
33
  function resolveConfig(options, prefix = "[hobots]") {
34
34
  const key = options.clientSecret?.trim();
35
35
  if (!key) {
36
- throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
36
+ throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`);
37
37
  }
38
38
  if (!SECRET_PATTERN.test(key)) {
39
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`
40
+ `${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
41
41
  );
42
42
  }
43
43
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -103,7 +103,7 @@ function scrubSecrets(text) {
103
103
 
104
104
  // src/utils.ts
105
105
  var SDK_NAME = "hobots";
106
- var SDK_VERSION = true ? "1.7.0" : "0.0.0-dev";
106
+ var SDK_VERSION = true ? "1.7.2" : "0.0.0-dev";
107
107
  function isNode() {
108
108
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
109
109
  }
@@ -662,12 +662,6 @@ var Scope = class _Scope {
662
662
  function spanId() {
663
663
  return uuid4().slice(0, 16);
664
664
  }
665
- function printSpanLog(kind, phase, label, startTs, endTs) {
666
- const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
667
- const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? "green" : "cyan");
668
- const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
669
- console.log(`${tag} ${label}${suffix}`);
670
- }
671
665
  function spanLabel(op, description) {
672
666
  return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
673
667
  }
@@ -698,7 +692,7 @@ var Span = class {
698
692
  finish() {
699
693
  if (this.endTimestamp) return;
700
694
  this.endTimestamp = nowISO();
701
- printSpanLog("span", "finish", spanLabel(this.op, this.description), this.startTimestamp, this.endTimestamp);
695
+ this.transaction.emit("span", "finish", spanLabel(this.op, this.description), this.startTimestamp, this.endTimestamp);
702
696
  }
703
697
  toJSON() {
704
698
  return {
@@ -720,7 +714,7 @@ var Transaction = class {
720
714
  this.op = op;
721
715
  this.sampled = sampled;
722
716
  this.runId = runId;
723
- printSpanLog("transaction", "start", op ? `${name} (${op})` : name);
717
+ this.emit("transaction", "start", op ? `${name} (${op})` : name);
724
718
  }
725
719
  eventId = uuid4();
726
720
  spanId = spanId();
@@ -729,10 +723,36 @@ var Transaction = class {
729
723
  spans = [];
730
724
  tags = {};
731
725
  finished = false;
726
+ lifecycleSink;
727
+ // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
728
+ // antes do RunReporter conectar) — replay no setLifecycleSink
729
+ lifecycleBacklog = [];
730
+ /**
731
+ * Ecoa no terminal o início/fim de um span ou transaction (sempre, independente de
732
+ * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
733
+ * assim que os eventos de performance também aparecem nos logs da run no app.
734
+ */
735
+ emit(kind, phase, label, startTs, endTs) {
736
+ const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
737
+ const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? "green" : "cyan");
738
+ const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
739
+ console.log(`${tag} ${label}${suffix}`);
740
+ const level = phase === "finish" ? "success" : "info";
741
+ const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
742
+ if (this.lifecycleSink) this.lifecycleSink(level, message);
743
+ else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
744
+ }
745
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
746
+ setLifecycleSink(sink) {
747
+ this.lifecycleSink = sink;
748
+ const backlog = this.lifecycleBacklog;
749
+ this.lifecycleBacklog = [];
750
+ for (const [level, message] of backlog) sink(level, message);
751
+ }
732
752
  createSpan(parentSpanId, op, description) {
733
753
  const span = new Span(this, parentSpanId, op, description);
734
754
  this.spans.push(span);
735
- printSpanLog("span", "start", spanLabel(op, description));
755
+ this.emit("span", "start", spanLabel(op, description));
736
756
  return span;
737
757
  }
738
758
  /** Span direto da transaction. */
@@ -756,7 +776,7 @@ var Transaction = class {
756
776
  this.finished = true;
757
777
  for (const span of this.spans) span.finish();
758
778
  const endTimestamp = nowISO();
759
- printSpanLog(
779
+ this.emit(
760
780
  "transaction",
761
781
  "finish",
762
782
  this.op ? `${this.name} (${this.op})` : this.name,
@@ -964,6 +984,7 @@ var RunReporter = class {
964
984
  error: (message) => this.log("error", message),
965
985
  success: (message) => this.log("success", message)
966
986
  };
987
+ tx.setLifecycleSink((level, message) => this.push(level, message));
967
988
  const signal = this.abortController.signal;
968
989
  this.context = {
969
990
  runId,
@@ -1008,10 +1029,25 @@ var RunReporter = class {
1008
1029
  this.clearFlushTimer();
1009
1030
  this.buffer = [];
1010
1031
  }
1032
+ /**
1033
+ * Drena o buffer pendente para o PATCH único do exit path (chame ANTES de kill,
1034
+ * que descarta o buffer). Capado em um chunk — o exit path não tem retry nem fila,
1035
+ * então preserva os logs mais recentes, os mais valiosos na morte do processo.
1036
+ */
1037
+ drainLogs() {
1038
+ this.clearFlushTimer();
1039
+ return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
1040
+ }
1011
1041
  log(level, message) {
1012
1042
  if (this.dead) return;
1013
1043
  const scrubbed = scrubSecrets(message);
1014
1044
  printRunLog(this.context.task, level, scrubbed);
1045
+ this.push(level, scrubbed, true);
1046
+ }
1047
+ /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1048
+ push(level, message, alreadyScrubbed = false) {
1049
+ if (this.dead) return;
1050
+ const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
1015
1051
  if (this.loggedCount >= MAX_LOGS_PER_RUN) {
1016
1052
  this.droppedCount++;
1017
1053
  return;
@@ -1100,10 +1136,16 @@ var RunReporter = class {
1100
1136
  return this.chain;
1101
1137
  }
1102
1138
  enqueue(update) {
1103
- if (this.dead) return;
1139
+ if (this.dead) {
1140
+ this.salvageLogs(update.logs);
1141
+ return;
1142
+ }
1104
1143
  this.pendingUpdates++;
1105
1144
  this.chain = this.chain.then(async () => {
1106
- if (this.dead) return;
1145
+ if (this.dead) {
1146
+ this.salvageLogs(update.logs);
1147
+ return;
1148
+ }
1107
1149
  const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
1108
1150
  try {
1109
1151
  await this.api.updateRun(this.runId, payload);
@@ -1112,16 +1154,35 @@ var RunReporter = class {
1112
1154
  this.dead = true;
1113
1155
  this.stopCancelWatch();
1114
1156
  this.abort();
1115
- this.buffer = [];
1116
- this.logger.warn(`run ${this.runId} cancelada/removida no servidor \u2014 reporte suprimido`);
1157
+ const pending = this.buffer.splice(0, this.buffer.length);
1158
+ this.salvageLogs(pending);
1159
+ this.logger.warn(
1160
+ `run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
1161
+ );
1117
1162
  return;
1118
1163
  }
1164
+ if (update.logs && update.logs.length > 0) {
1165
+ this.buffer.unshift(...update.logs);
1166
+ this.scheduleFlush();
1167
+ }
1119
1168
  this.logger.error(`update da run ${this.runId} perdido:`, error);
1120
1169
  }
1121
1170
  }).finally(() => {
1122
1171
  this.pendingUpdates--;
1123
1172
  });
1124
1173
  }
1174
+ /**
1175
+ * Best-effort de log tardio: envia logs para uma run já terminal, fora da fila.
1176
+ * O servidor anexa os logs mesmo respondendo 409; erros são engolidos — este é
1177
+ * o último recurso, não um canal confiável.
1178
+ */
1179
+ salvageLogs(logs) {
1180
+ if (!logs || logs.length === 0) return;
1181
+ for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
1182
+ void this.api.updateRunOnce(this.runId, { status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) }).catch(() => {
1183
+ });
1184
+ }
1185
+ }
1125
1186
  };
1126
1187
 
1127
1188
  // src/tasks/run-context-store.ts
@@ -1277,9 +1338,14 @@ var TaskAgent = class {
1277
1338
  const pending = [];
1278
1339
  for (const reporter of this.reporters) {
1279
1340
  if (reporter.isDead) continue;
1341
+ const logs = reporter.drainLogs();
1280
1342
  reporter.kill();
1281
1343
  pending.push(
1282
- this.api.updateRunOnce(reporter.context.runId, { status: "failed", error: { type: "AgentTerminated", message } }, timeoutMs).catch(() => {
1344
+ this.api.updateRunOnce(
1345
+ reporter.context.runId,
1346
+ { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
1347
+ timeoutMs
1348
+ ).catch(() => {
1283
1349
  })
1284
1350
  );
1285
1351
  }
@@ -1335,7 +1401,7 @@ var TaskAgent = class {
1335
1401
  if (error instanceof TasksApiError && error.status === 401) {
1336
1402
  this.consecutiveAuthFailures++;
1337
1403
  if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
1338
- this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos");
1404
+ this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
1339
1405
  }
1340
1406
  return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
1341
1407
  }
@@ -1378,15 +1444,16 @@ var TaskAgent = class {
1378
1444
  this.logger.debug(`executando run ${run.id} (${run.task})`);
1379
1445
  try {
1380
1446
  await runWithRunContext({ runId: run.id, tx }, () => this.invokeWithTimeout(handler, run, reporter));
1381
- reporter.reportSuccess();
1382
1447
  tx.setStatus("ok");
1448
+ tx.finish();
1449
+ reporter.reportSuccess();
1383
1450
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1384
1451
  } catch (error) {
1385
- reporter.reportFailure(toRunError(error));
1386
1452
  tx.setStatus("error");
1453
+ tx.finish();
1454
+ reporter.reportFailure(toRunError(error));
1387
1455
  this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
1388
1456
  }
1389
- tx.finish();
1390
1457
  await reporter.finalize();
1391
1458
  } finally {
1392
1459
  this.reporters.delete(reporter);