retrace-sdk 0.10.0 → 0.11.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/config.d.ts CHANGED
@@ -7,6 +7,10 @@ export interface Config {
7
7
  sampleRate: number;
8
8
  /** Optional seed for deterministic sampling. When set, the same trace name always produces the same sample decision. */
9
9
  sampleSeed: string | undefined;
10
+ /** Transport mode. "auto" (default) tries WebSocket then falls back to HTTP; "http" is
11
+ * request/response only (recommended for short-lived scripts and serverless — it never
12
+ * holds an open socket and always surfaces upload errors); "ws" forces WebSocket. */
13
+ transport: "auto" | "ws" | "http";
10
14
  }
11
15
  export declare function configure(opts: Partial<Config>): Config;
12
16
  export declare function requireApiKey(): string;
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ const config = {
6
6
  enabled: !["false", "0"].includes((process.env.RETRACE_ENABLED || "true").toLowerCase()),
7
7
  sampleRate: parseFloat(process.env.RETRACE_SAMPLE_RATE || "1"),
8
8
  sampleSeed: process.env.RETRACE_SAMPLE_SEED || undefined,
9
+ transport: (["auto", "ws", "http"].includes(process.env.RETRACE_TRANSPORT || "") ? process.env.RETRACE_TRANSPORT : "auto"),
9
10
  };
10
11
  config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
11
12
  export function configure(opts) {
package/dist/recorder.js CHANGED
@@ -9,10 +9,11 @@ import { installAnthropicInterceptor } from "./interceptors/anthropic.js";
9
9
  let sharedTransport = null;
10
10
  function getSharedTransport() {
11
11
  if (!sharedTransport) {
12
- sharedTransport = createTransport();
13
- // Flush pending data before process exits
12
+ sharedTransport = createTransport(getConfig().transport);
13
+ // Flush pending data before the process exits. Flushing (not just close) ensures the
14
+ // final trace is actually uploaded — close() alone can race the process exiting.
14
15
  if (typeof process !== "undefined") {
15
- process.on("beforeExit", () => { sharedTransport?.close(); });
16
+ process.on("beforeExit", () => { void flushSharedTransport(); });
16
17
  }
17
18
  }
18
19
  return sharedTransport;
@@ -10,6 +10,7 @@ export declare class WSTransport implements Transport {
10
10
  private closed;
11
11
  private backoff;
12
12
  private queue;
13
+ private reconnectTimer;
13
14
  onError?: (type: string, message: string) => void;
14
15
  get isConnected(): boolean;
15
16
  connect(): void;
package/dist/transport.js CHANGED
@@ -6,6 +6,7 @@ export class WSTransport {
6
6
  closed = false;
7
7
  backoff = 1000;
8
8
  queue = [];
9
+ reconnectTimer = null;
9
10
  onError;
10
11
  get isConnected() { return this.connected; }
11
12
  connect() {
@@ -15,6 +16,10 @@ export class WSTransport {
15
16
  const url = `${cfg.wsUrl}/ws/v1/stream`;
16
17
  this.ws = new WebSocket(url);
17
18
  this.ws.on("open", () => {
19
+ // Unref the underlying socket so a short-lived script (the common SDK usage) can exit
20
+ // once its work is done instead of hanging on an open WebSocket. Graceful shutdown
21
+ // still drains via flush()/beforeExit.
22
+ this.ws?._socket?.unref?.();
18
23
  this.ws.send(JSON.stringify({ type: "auth", api_key: cfg.apiKey }));
19
24
  });
20
25
  this.ws.on("message", (raw) => {
@@ -60,7 +65,9 @@ export class WSTransport {
60
65
  this.connected = false;
61
66
  this.ws = null;
62
67
  if (!this.closed) {
63
- setTimeout(() => this.reconnect(), this.backoff * (0.5 + Math.random() * 0.5));
68
+ this.reconnectTimer = setTimeout(() => this.reconnect(), this.backoff * (0.5 + Math.random() * 0.5));
69
+ // Don't let the reconnect timer keep the event loop (and the process) alive.
70
+ this.reconnectTimer?.unref?.();
64
71
  this.backoff = Math.min(this.backoff * 2, 30000);
65
72
  }
66
73
  });
@@ -92,6 +99,10 @@ export class WSTransport {
92
99
  }
93
100
  close() {
94
101
  this.closed = true;
102
+ if (this.reconnectTimer) {
103
+ clearTimeout(this.reconnectTimer);
104
+ this.reconnectTimer = null;
105
+ }
95
106
  if (this.ws) {
96
107
  this.ws.close();
97
108
  this.ws = null;
@@ -136,17 +147,30 @@ export class HTTPTransport {
136
147
  // Retry up to 3 times with exponential backoff; awaited so shutdown can drain it.
137
148
  for (let n = 1; n <= 3; n++) {
138
149
  try {
139
- await fetch(url, {
150
+ const res = await fetch(url, {
140
151
  method: "POST",
141
152
  headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
142
153
  body: payload,
143
154
  });
144
- return;
155
+ if (res.ok)
156
+ return;
157
+ const txt = await res.text().catch(() => "");
158
+ // 4xx (except 429) is a client/payload error that won't succeed on retry — surface
159
+ // it loudly and stop, rather than silently dropping the trace.
160
+ if (res.status < 500 && res.status !== 429) {
161
+ console.error(`[retrace] trace upload rejected (HTTP ${res.status}): ${txt.slice(0, 300)}`);
162
+ return;
163
+ }
164
+ // 5xx / 429 — transient; retry.
165
+ if (n === 3)
166
+ console.error(`[retrace] trace upload failed after ${n} attempts (HTTP ${res.status}): ${txt.slice(0, 200)}`);
145
167
  }
146
- catch {
147
- if (n < 3)
148
- await new Promise((r) => setTimeout(r, 1000 * n));
168
+ catch (err) {
169
+ if (n === 3)
170
+ console.error(`[retrace] trace upload network error after ${n} attempts: ${err?.message ?? err}`);
149
171
  }
172
+ if (n < 3)
173
+ await new Promise((r) => setTimeout(r, 1000 * n));
150
174
  }
151
175
  }
152
176
  buildSpans() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",