retrace-sdk 0.9.0 → 0.10.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/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { configure, getConfig, requireApiKey } from "./config.js";
2
- import { TraceRecorder } from "./recorder.js";
2
+ import { TraceRecorder, flushSharedTransport } from "./recorder.js";
3
3
  import { TraceStatus } from "./trace.js";
4
4
  let ambient = null;
5
5
  let exitHooked = false;
@@ -54,10 +54,20 @@ export function init(opts = {}) {
54
54
  }
55
55
  catch { /* best effort on shutdown */ }
56
56
  };
57
+ // On signal-triggered exits, process.exit() would otherwise kill the process before the
58
+ // final trace_ended is delivered over the network. End the trace, then await a transport
59
+ // drain (capped by a hard timeout so a hung network can't block shutdown) before exiting.
60
+ const finishAndExit = (status, code) => {
61
+ finish(status);
62
+ void Promise.race([
63
+ flushSharedTransport().catch(() => { }),
64
+ new Promise((r) => setTimeout(r, 3000)),
65
+ ]).then(() => process.exit(code));
66
+ };
57
67
  process.once("beforeExit", () => finish(TraceStatus.COMPLETED));
58
- process.once("SIGINT", () => { finish(TraceStatus.COMPLETED); process.exit(130); });
59
- process.once("SIGTERM", () => { finish(TraceStatus.COMPLETED); process.exit(143); });
60
- process.once("uncaughtException", (err) => { finish(TraceStatus.FAILED); console.error(err); process.exit(1); });
68
+ process.once("SIGINT", () => finishAndExit(TraceStatus.COMPLETED, 130));
69
+ process.once("SIGTERM", () => finishAndExit(TraceStatus.COMPLETED, 143));
70
+ process.once("uncaughtException", (err) => { console.error(err); finishAndExit(TraceStatus.FAILED, 1); });
61
71
  }
62
72
  return ambient;
63
73
  }
@@ -1,4 +1,6 @@
1
1
  import { SpanBuilder, SpanData, SpanType, TraceStatus } from "./trace.js";
2
+ /** Drain the shared transport's in-flight data to the network (awaited on graceful shutdown). */
3
+ export declare function flushSharedTransport(): Promise<void>;
2
4
  export interface RecordOptions {
3
5
  name?: string;
4
6
  input?: unknown;
package/dist/recorder.js CHANGED
@@ -17,6 +17,10 @@ function getSharedTransport() {
17
17
  }
18
18
  return sharedTransport;
19
19
  }
20
+ /** Drain the shared transport's in-flight data to the network (awaited on graceful shutdown). */
21
+ export async function flushSharedTransport() {
22
+ await sharedTransport?.flush();
23
+ }
20
24
  export class TraceRecorder {
21
25
  builder;
22
26
  transport;
package/dist/trace.js CHANGED
@@ -83,7 +83,12 @@ export class TraceBuilder {
83
83
  return this.data;
84
84
  }
85
85
  addSpan(span) {
86
- this.data.spans.push(span);
86
+ // Spans are streamed individually through the transport (and HTTPTransport keeps its own
87
+ // per-trace buffer for the batched POST), so this retained array is only an in-memory
88
+ // convenience and is never itself transmitted. Cap it so init()'s long-lived ambient
89
+ // trace can't accumulate spans for the life of the process (an unbounded memory leak).
90
+ if (this.data.spans.length < 1000)
91
+ this.data.spans.push(span);
87
92
  this.data.total_tokens += (span.input_tokens || 0) + (span.output_tokens || 0);
88
93
  this.data.total_cost += span.cost || 0;
89
94
  }
@@ -1,6 +1,8 @@
1
1
  export interface Transport {
2
2
  send(eventType: string, data: Record<string, unknown>): void;
3
3
  close(): void;
4
+ /** Drain in-flight data to the network (awaited on graceful shutdown). */
5
+ flush(): Promise<void>;
4
6
  }
5
7
  export declare class WSTransport implements Transport {
6
8
  private ws;
@@ -15,12 +17,15 @@ export declare class WSTransport implements Transport {
15
17
  private flushQueue;
16
18
  send(eventType: string, data: Record<string, unknown>): void;
17
19
  close(): void;
20
+ /** Wait for the socket's send buffer to drain so the final trace_ended actually leaves
21
+ * the process before exit. Best-effort with a hard timeout. */
22
+ flush(): Promise<void>;
18
23
  }
19
24
  export declare class HTTPTransport implements Transport {
20
25
  private traceData;
21
26
  private spans;
22
27
  send(eventType: string, data: Record<string, unknown>): void;
23
- flush(): void;
28
+ flush(): Promise<void>;
24
29
  private buildSpans;
25
30
  close(): void;
26
31
  }
package/dist/transport.js CHANGED
@@ -98,6 +98,14 @@ export class WSTransport {
98
98
  }
99
99
  this.connected = false;
100
100
  }
101
+ /** Wait for the socket's send buffer to drain so the final trace_ended actually leaves
102
+ * the process before exit. Best-effort with a hard timeout. */
103
+ async flush() {
104
+ const start = Date.now();
105
+ while (this.ws && this.ws.readyState === WebSocket.OPEN && this.ws.bufferedAmount > 0 && Date.now() - start < 2000) {
106
+ await new Promise((r) => setTimeout(r, 50));
107
+ }
108
+ }
101
109
  }
102
110
  export class HTTPTransport {
103
111
  traceData = null;
@@ -112,28 +120,34 @@ export class HTTPTransport {
112
120
  else if (eventType === "trace_ended") {
113
121
  if (this.traceData)
114
122
  Object.assign(this.traceData, data);
115
- this.flush();
123
+ void this.flush();
116
124
  }
117
125
  }
118
- flush() {
126
+ async flush() {
119
127
  if (!this.traceData)
120
128
  return;
121
129
  const cfg = getConfig();
122
130
  const url = `${cfg.baseUrl}/api/v1/traces`;
123
131
  const body = { ...this.traceData, spans: this.buildSpans() };
124
132
  const payload = JSON.stringify(body);
125
- // Retry up to 3 times with exponential backoff
126
- const attempt = (n, delay) => {
127
- fetch(url, {
128
- method: "POST",
129
- headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
130
- body: payload,
131
- }).catch(() => { if (n < 3)
132
- setTimeout(() => attempt(n + 1, delay * 2), delay); });
133
- };
134
- attempt(1, 1000);
133
+ // Clear first so a concurrent flush (e.g. trace_ended then shutdown drain) can't double-send.
135
134
  this.traceData = null;
136
135
  this.spans = [];
136
+ // Retry up to 3 times with exponential backoff; awaited so shutdown can drain it.
137
+ for (let n = 1; n <= 3; n++) {
138
+ try {
139
+ await fetch(url, {
140
+ method: "POST",
141
+ headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
142
+ body: payload,
143
+ });
144
+ return;
145
+ }
146
+ catch {
147
+ if (n < 3)
148
+ await new Promise((r) => setTimeout(r, 1000 * n));
149
+ }
150
+ }
137
151
  }
138
152
  buildSpans() {
139
153
  const merged = new Map();
@@ -150,7 +164,7 @@ export class HTTPTransport {
150
164
  return [...merged.values()];
151
165
  }
152
166
  close() {
153
- this.flush();
167
+ void this.flush();
154
168
  }
155
169
  }
156
170
  export function createTransport(mode = "auto") {
@@ -214,5 +228,24 @@ export function createTransport(mode = "auto") {
214
228
  http.close();
215
229
  }
216
230
  },
231
+ async flush() {
232
+ if (!decided) {
233
+ // Never connected over WS — force the HTTP fallback and drain the buffer so the
234
+ // final trace isn't lost on shutdown.
235
+ decided = true;
236
+ useWs = false;
237
+ clearTimeout(fallbackTimer);
238
+ ws.close();
239
+ for (const item of buffer.splice(0))
240
+ http.send(item.eventType, item.data);
241
+ await http.flush();
242
+ }
243
+ else if (useWs) {
244
+ await ws.flush();
245
+ }
246
+ else {
247
+ await http.flush();
248
+ }
249
+ },
217
250
  };
218
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The execution replay engine for AI agents. Record, replay, fork, and share agent executions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",