retrace-sdk 0.16.7 → 0.16.8
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 +2 -2
- package/dist/interceptors/anthropic.js +1 -0
- package/dist/interceptors/openai.js +1 -0
- package/dist/transport.d.ts +6 -3
- package/dist/transport.js +63 -21
- package/dist/utils.js +36 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="https://raw.githubusercontent.com/
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Retraceai-tech/retrace-sdk/main/assets/banner.gif" alt="Retrace" width="480" />
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -207,7 +207,7 @@ Typed errors for auth failures, credit exhaustion, rate limiting, and enforcemen
|
|
|
207
207
|
## Links
|
|
208
208
|
|
|
209
209
|
- [Documentation](https://docs.retraceai.tech)
|
|
210
|
-
- [GitHub](https://github.com/
|
|
210
|
+
- [GitHub](https://github.com/Retraceai-tech/retrace-sdk)
|
|
211
211
|
- [npm](https://www.npmjs.com/package/retrace-sdk)
|
|
212
212
|
|
|
213
213
|
## License
|
|
@@ -219,5 +219,6 @@ export function uninstallAnthropicInterceptor() {
|
|
|
219
219
|
proto.create = originalCreate;
|
|
220
220
|
}).catch(() => { });
|
|
221
221
|
installed = false;
|
|
222
|
+
installStarted = false; // allow a subsequent installAnthropicInterceptor() to re-patch (was a no-op)
|
|
222
223
|
onSpanCallback = null;
|
|
223
224
|
}
|
|
@@ -307,5 +307,6 @@ export function uninstallOpenAIInterceptor() {
|
|
|
307
307
|
proto.create = originalCreate;
|
|
308
308
|
}).catch(() => { });
|
|
309
309
|
installed = false;
|
|
310
|
+
installStarted = false; // allow a subsequent installOpenAIInterceptor() to re-patch (was a no-op)
|
|
310
311
|
onSpanCallback = null;
|
|
311
312
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -69,13 +69,16 @@ export declare class WSTransport implements Transport {
|
|
|
69
69
|
private flushViaHttp;
|
|
70
70
|
}
|
|
71
71
|
export declare class HTTPTransport implements Transport {
|
|
72
|
-
private
|
|
73
|
-
private
|
|
72
|
+
private pending;
|
|
73
|
+
private spanTrace;
|
|
74
74
|
private backlog;
|
|
75
75
|
private readonly backlogCap;
|
|
76
76
|
private dropped;
|
|
77
|
+
private entry;
|
|
77
78
|
send(eventType: string, data: Record<string, unknown>): void;
|
|
78
|
-
|
|
79
|
+
/** Flush one trace (on its trace_ended) or, when no id is given, every pending trace (shutdown). */
|
|
80
|
+
flush(traceId?: string): Promise<void>;
|
|
81
|
+
private flushOne;
|
|
79
82
|
/** POST one complete trace payload. Returns "ok" | "reject" (non-retryable 4xx) | "retry"
|
|
80
83
|
* (429 / 5xx / network after retries exhausted — caller should retain the payload). */
|
|
81
84
|
private upload;
|
package/dist/transport.js
CHANGED
|
@@ -87,7 +87,7 @@ export class WSTransport {
|
|
|
87
87
|
// (no-op on runtimes without `_socket`, e.g. the global WebSocket). Graceful shutdown still
|
|
88
88
|
// drains via flush()/beforeExit.
|
|
89
89
|
socket?._socket?.unref?.();
|
|
90
|
-
socket.send(JSON.stringify({ type: "auth", api_key: getConfig().apiKey }));
|
|
90
|
+
socket.send(JSON.stringify({ type: "auth", api_key: getConfig().apiKey, resumable: getConfig().allowRemoteReplay }));
|
|
91
91
|
});
|
|
92
92
|
socket.addEventListener("message", (ev) => {
|
|
93
93
|
try {
|
|
@@ -347,40 +347,82 @@ export class WSTransport {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
export class HTTPTransport {
|
|
350
|
-
|
|
351
|
-
spans
|
|
350
|
+
// Buffer per-trace so concurrent traces in one process (parallel requests / Promise.all) never
|
|
351
|
+
// clobber each other. The previous single traceData/spans pair merged interleaved traces into one
|
|
352
|
+
// corrupt POST (trace B's trace_started overwrote A; A's flush sent B's data + both sets of spans).
|
|
353
|
+
// Keyed by trace id; spans route by their trace_id.
|
|
354
|
+
pending = new Map();
|
|
355
|
+
// span_ended events carry no trace_id (parity with the WS protocol), so remember each span's trace
|
|
356
|
+
// from its span_started to route the end event to the right per-trace buffer.
|
|
357
|
+
spanTrace = new Map();
|
|
352
358
|
// Bounded offline backlog (parity with WSTransport's queue + the Python HTTPTransport): a trace
|
|
353
359
|
// that fails to upload after exhausting transient retries is RETAINED here and retried on a later
|
|
354
360
|
// flush instead of being silently dropped. Bounded so a long outage can't grow memory unbounded.
|
|
355
361
|
backlog = [];
|
|
356
362
|
backlogCap = 1000;
|
|
357
363
|
dropped = 0;
|
|
364
|
+
entry(traceId) {
|
|
365
|
+
let e = this.pending.get(traceId);
|
|
366
|
+
if (!e) {
|
|
367
|
+
e = { data: {}, spans: [] };
|
|
368
|
+
this.pending.set(traceId, e);
|
|
369
|
+
}
|
|
370
|
+
return e;
|
|
371
|
+
}
|
|
358
372
|
send(eventType, data) {
|
|
359
373
|
if (eventType === "trace_started") {
|
|
360
|
-
|
|
374
|
+
const id = data.id;
|
|
375
|
+
if (!id)
|
|
376
|
+
return;
|
|
377
|
+
const e = this.entry(id);
|
|
378
|
+
e.data = { ...e.data, ...data };
|
|
361
379
|
}
|
|
362
|
-
else if (eventType === "span_started"
|
|
363
|
-
|
|
380
|
+
else if (eventType === "span_started") {
|
|
381
|
+
const tid = data.trace_id;
|
|
382
|
+
if (!tid)
|
|
383
|
+
return;
|
|
384
|
+
this.spanTrace.set(data.id, tid);
|
|
385
|
+
this.entry(tid).spans.push({ ...data, _event: eventType });
|
|
386
|
+
}
|
|
387
|
+
else if (eventType === "span_ended") {
|
|
388
|
+
// span_ended has no trace_id — route by the span id captured at span_started.
|
|
389
|
+
const tid = data.trace_id || this.spanTrace.get(data.id);
|
|
390
|
+
if (!tid)
|
|
391
|
+
return;
|
|
392
|
+
this.entry(tid).spans.push({ ...data, _event: eventType });
|
|
364
393
|
}
|
|
365
394
|
else if (eventType === "trace_ended") {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
const id = data.id;
|
|
396
|
+
if (!id)
|
|
397
|
+
return;
|
|
398
|
+
const e = this.pending.get(id);
|
|
399
|
+
if (e)
|
|
400
|
+
Object.assign(e.data, data);
|
|
401
|
+
void this.flush(id);
|
|
369
402
|
}
|
|
370
403
|
}
|
|
371
|
-
|
|
404
|
+
/** Flush one trace (on its trace_ended) or, when no id is given, every pending trace (shutdown). */
|
|
405
|
+
async flush(traceId) {
|
|
372
406
|
// Opportunistically retry anything stranded by a previous outage first.
|
|
373
407
|
if (this.backlog.length)
|
|
374
408
|
await this.drainBacklog();
|
|
375
|
-
if (
|
|
409
|
+
if (traceId !== undefined) {
|
|
410
|
+
await this.flushOne(traceId);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
for (const id of [...this.pending.keys()])
|
|
414
|
+
await this.flushOne(id);
|
|
415
|
+
}
|
|
416
|
+
async flushOne(traceId) {
|
|
417
|
+
const e = this.pending.get(traceId);
|
|
418
|
+
if (!e)
|
|
376
419
|
return;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
this.spans
|
|
382
|
-
// On a transient failure, RETAIN the trace in the bounded backlog instead of dropping it
|
|
383
|
-
// previous code cleared the buffer unconditionally, losing the trace under sustained 429/5xx.
|
|
420
|
+
// Remove first so a concurrent flush (e.g. trace_ended then shutdown drain) can't double-send.
|
|
421
|
+
this.pending.delete(traceId);
|
|
422
|
+
for (const ev of e.spans)
|
|
423
|
+
this.spanTrace.delete(ev.id);
|
|
424
|
+
const payload = JSON.stringify({ ...e.data, spans: this.buildSpans(e.spans) });
|
|
425
|
+
// On a transient failure, RETAIN the trace in the bounded backlog instead of dropping it.
|
|
384
426
|
if ((await this.upload(payload)) === "retry")
|
|
385
427
|
this.enqueueBacklog(payload);
|
|
386
428
|
}
|
|
@@ -438,9 +480,9 @@ export class HTTPTransport {
|
|
|
438
480
|
this.backlog.shift(); // "ok" (delivered) or "reject" (unrecoverable) → remove
|
|
439
481
|
}
|
|
440
482
|
}
|
|
441
|
-
buildSpans() {
|
|
483
|
+
buildSpans(spans) {
|
|
442
484
|
const merged = new Map();
|
|
443
|
-
for (const ev of
|
|
485
|
+
for (const ev of spans) {
|
|
444
486
|
const { _event, ...rest } = ev;
|
|
445
487
|
const id = rest.id;
|
|
446
488
|
if (_event === "span_started") {
|
|
@@ -460,7 +502,7 @@ export class HTTPTransport {
|
|
|
460
502
|
await this.flush();
|
|
461
503
|
}
|
|
462
504
|
hasPendingData() {
|
|
463
|
-
return this.
|
|
505
|
+
return this.pending.size > 0 || this.backlog.length > 0;
|
|
464
506
|
}
|
|
465
507
|
}
|
|
466
508
|
export function createTransport(mode = "auto") {
|
package/dist/utils.js
CHANGED
|
@@ -6,7 +6,6 @@ export function genId() {
|
|
|
6
6
|
return globalThis.crypto.randomUUID();
|
|
7
7
|
}
|
|
8
8
|
const _textEncoder = new TextEncoder();
|
|
9
|
-
const _textDecoder = new TextDecoder();
|
|
10
9
|
export function nowIso() {
|
|
11
10
|
return new Date().toISOString();
|
|
12
11
|
}
|
|
@@ -34,17 +33,47 @@ export function shouldSample(rate, seed, key) {
|
|
|
34
33
|
}
|
|
35
34
|
return ((hash >>> 0) / 4294967296) < rate;
|
|
36
35
|
}
|
|
36
|
+
/** Truncate a string to at most `maxBytes` UTF-8 bytes WITHOUT splitting a multi-byte codepoint, so
|
|
37
|
+
* there's no garbled/replacement-char tail. Iterates whole code points (via the string iterator). */
|
|
38
|
+
function sliceStringToBytes(s, maxBytes) {
|
|
39
|
+
if (maxBytes <= 0)
|
|
40
|
+
return "";
|
|
41
|
+
if (_textEncoder.encode(s).length <= maxBytes)
|
|
42
|
+
return s;
|
|
43
|
+
let out = "";
|
|
44
|
+
let n = 0;
|
|
45
|
+
for (const ch of s) {
|
|
46
|
+
const b = _textEncoder.encode(ch).length;
|
|
47
|
+
if (n + b > maxBytes)
|
|
48
|
+
break;
|
|
49
|
+
out += ch;
|
|
50
|
+
n += b;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
37
54
|
export function truncateJson(obj, maxBytes = 10240) {
|
|
55
|
+
let s;
|
|
38
56
|
try {
|
|
39
|
-
|
|
40
|
-
const bytes = _textEncoder.encode(s);
|
|
41
|
-
if (bytes.length <= maxBytes)
|
|
42
|
-
return obj;
|
|
43
|
-
return JSON.parse(_textDecoder.decode(bytes.subarray(0, maxBytes)));
|
|
57
|
+
s = JSON.stringify(obj);
|
|
44
58
|
}
|
|
45
59
|
catch {
|
|
46
|
-
|
|
60
|
+
s = undefined;
|
|
47
61
|
}
|
|
62
|
+
// Not JSON-serializable (circular ref, BigInt, …): char-safe string slice, not a broken parse.
|
|
63
|
+
if (typeof s !== "string")
|
|
64
|
+
return sliceStringToBytes(String(obj), maxBytes);
|
|
65
|
+
const byteLen = _textEncoder.encode(s).length;
|
|
66
|
+
if (byteLen <= maxBytes)
|
|
67
|
+
return obj;
|
|
68
|
+
// Over the limit. Return a self-describing, VALID marker with a UTF-8-safe preview. The old code
|
|
69
|
+
// byte-sliced the JSON mid-codepoint and JSON.parse()'d the fragment — which almost always threw,
|
|
70
|
+
// falling back to String(obj) = "[object Object]" for any object (total data loss) and a garbled
|
|
71
|
+
// tail for strings. `wasTruncated` still flags this so the server refuses to byte-replay it.
|
|
72
|
+
return {
|
|
73
|
+
_truncated: true,
|
|
74
|
+
_original_bytes: byteLen,
|
|
75
|
+
_preview: sliceStringToBytes(s, Math.max(0, maxBytes - 64)),
|
|
76
|
+
};
|
|
48
77
|
}
|
|
49
78
|
/** True if truncateJson(obj, maxBytes) would drop bytes. Used to flag a span's output as truncated
|
|
50
79
|
* so the server refuses to byte-replay it (the replayed value would differ from the original). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retrace-sdk",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.8",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"author": "Yash Bogam",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/Retraceai-tech/retrace-sdk",
|
|
32
32
|
"directory": "packages/sdk-typescript"
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://retraceai.tech/docs/sdk-typescript",
|