retrace-sdk 0.1.6 → 0.2.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/src/trace.ts DELETED
@@ -1,135 +0,0 @@
1
- import { genId, nowIso, utcNow } from "./utils.js";
2
-
3
- export enum SpanType {
4
- LLM_CALL = "llm_call",
5
- TOOL_CALL = "tool_call",
6
- TOOL_RESULT = "tool_result",
7
- REASONING = "reasoning",
8
- ACTION = "action",
9
- ERROR = "error",
10
- FORK_POINT = "fork_point",
11
- }
12
-
13
- export enum TraceStatus {
14
- RUNNING = "running",
15
- COMPLETED = "completed",
16
- FAILED = "failed",
17
- }
18
-
19
- export interface SpanData {
20
- id: string;
21
- trace_id: string;
22
- parent_id: string | null;
23
- span_type: string;
24
- name: string;
25
- model?: string;
26
- input?: unknown;
27
- output?: unknown;
28
- input_tokens?: number;
29
- output_tokens?: number;
30
- cost?: number;
31
- duration_ms?: number;
32
- metadata?: Record<string, unknown>;
33
- started_at: string;
34
- ended_at?: string;
35
- error?: string;
36
- }
37
-
38
- export interface TraceData {
39
- id: string;
40
- name?: string;
41
- input?: unknown;
42
- output?: unknown;
43
- status: string;
44
- total_tokens: number;
45
- total_cost: number;
46
- total_duration_ms: number;
47
- metadata?: Record<string, unknown>;
48
- started_at: string;
49
- ended_at?: string;
50
- spans?: SpanData[];
51
- project_id?: string;
52
- }
53
-
54
- export class SpanBuilder {
55
- private data: Partial<SpanData> & { id: string; span_type: string; name: string };
56
- private _startTime?: Date;
57
-
58
- constructor(name: string, spanType: SpanType) {
59
- this.data = { id: genId(), span_type: spanType, name, trace_id: "", parent_id: null, started_at: nowIso() };
60
- }
61
-
62
- setModel(model: string) { this.data.model = model; return this; }
63
- setInput(input: unknown) { this.data.input = input; return this; }
64
- setOutput(output: unknown) { this.data.output = output; return this; }
65
- setParentId(id: string) { this.data.parent_id = id; return this; }
66
- setTraceId(id: string) { this.data.trace_id = id; return this; }
67
- setMetadata(m: Record<string, unknown>) { this.data.metadata = m; return this; }
68
-
69
- start(): this {
70
- this._startTime = utcNow();
71
- this.data.started_at = this._startTime.toISOString();
72
- return this;
73
- }
74
-
75
- end(output?: unknown, error?: string): SpanData {
76
- const now = utcNow();
77
- this.data.ended_at = now.toISOString();
78
- if (output !== undefined) this.data.output = output;
79
- if (error) this.data.error = error;
80
- if (this._startTime) {
81
- this.data.duration_ms = now.getTime() - this._startTime.getTime();
82
- }
83
- return this.data as SpanData;
84
- }
85
-
86
- get id() { return this.data.id; }
87
- toData(): SpanData { return this.data as SpanData; }
88
- }
89
-
90
- export class TraceBuilder {
91
- private data: TraceData;
92
- private _startTime?: Date;
93
-
94
- constructor() {
95
- this.data = {
96
- id: genId(),
97
- status: TraceStatus.RUNNING,
98
- total_tokens: 0,
99
- total_cost: 0,
100
- total_duration_ms: 0,
101
- started_at: nowIso(),
102
- spans: [],
103
- };
104
- }
105
-
106
- start(name?: string, input?: unknown): this {
107
- this._startTime = utcNow();
108
- this.data.started_at = this._startTime.toISOString();
109
- if (name) this.data.name = name;
110
- if (input !== undefined) this.data.input = input;
111
- return this;
112
- }
113
-
114
- end(output?: unknown, status: TraceStatus = TraceStatus.COMPLETED): TraceData {
115
- const now = utcNow();
116
- this.data.ended_at = now.toISOString();
117
- this.data.status = status;
118
- if (output !== undefined) this.data.output = output;
119
- if (this._startTime) {
120
- this.data.total_duration_ms = now.getTime() - this._startTime.getTime();
121
- }
122
- return this.data;
123
- }
124
-
125
- addSpan(span: SpanData) {
126
- this.data.spans!.push(span);
127
- this.data.total_tokens += (span.input_tokens || 0) + (span.output_tokens || 0);
128
- this.data.total_cost += span.cost || 0;
129
- }
130
-
131
- get id() { return this.data.id; }
132
- setProjectId(id: string) { this.data.project_id = id; }
133
- setMetadata(m: Record<string, unknown>) { this.data.metadata = m; }
134
- toDict(): TraceData { return this.data; }
135
- }
@@ -1,80 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { configure } from "./config.js";
3
-
4
- // Mock WebSocket to never connect (simulates unreachable WS endpoint)
5
- const mockWsInstances: Array<Record<string, unknown>> = [];
6
- vi.mock("ws", () => {
7
- const MockWebSocket = function (this: Record<string, unknown>) {
8
- this.readyState = 0;
9
- this._listeners = {} as Record<string, Array<() => void>>;
10
- this.on = (event: string, cb: () => void) => { ((this._listeners as Record<string, Array<() => void>>)[event] ??= []).push(cb); return this; };
11
- this.send = vi.fn();
12
- this.close = vi.fn(() => {
13
- this.readyState = 3;
14
- for (const cb of ((this._listeners as Record<string, Array<() => void>>)["close"] ?? [])) cb();
15
- });
16
- mockWsInstances.push(this);
17
- } as unknown as { new(): Record<string, unknown>; OPEN: number };
18
- MockWebSocket.OPEN = 1;
19
- return { default: MockWebSocket };
20
- });
21
-
22
- beforeEach(() => {
23
- configure({ apiKey: "rt_live_test", baseUrl: "http://localhost:3001", enabled: true });
24
- mockWsInstances.length = 0;
25
- });
26
-
27
- afterEach(() => {
28
- vi.unstubAllGlobals();
29
- });
30
-
31
- describe("Auto transport fallback on early close()", () => {
32
- it("flushes to HTTP when close() is called before WS connects", async () => {
33
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
34
- vi.stubGlobal("fetch", mockFetch);
35
-
36
- const { createTransport } = await import("./transport.js");
37
- const transport = createTransport("auto");
38
-
39
- // Send trace events immediately (before WS could connect)
40
- transport.send("trace_started", { id: "t1", name: "fast-trace", status: "running", started_at: "2026-01-01T00:00:00Z", total_tokens: 0, total_cost: 0, total_duration_ms: 0 });
41
- transport.send("span_started", { id: "s1", trace_id: "t1", span_type: "llm_call", name: "call", started_at: "2026-01-01T00:00:00Z" });
42
- transport.send("span_ended", { id: "s1", ended_at: "2026-01-01T00:00:01Z", output: "response" });
43
- transport.send("trace_ended", { id: "t1", ended_at: "2026-01-01T00:00:01Z", status: "completed" });
44
-
45
- // Close immediately — WS has NOT connected yet
46
- transport.close();
47
-
48
- // Should have fallen back to HTTP and flushed
49
- expect(mockFetch).toHaveBeenCalledTimes(1);
50
- const [url, opts] = mockFetch.mock.calls[0];
51
- expect(url).toBe("http://localhost:3001/api/v1/traces");
52
- const body = JSON.parse(opts.body);
53
- expect(body.id).toBe("t1");
54
- expect(body.name).toBe("fast-trace");
55
- expect(body.spans).toHaveLength(1);
56
- expect(body.spans[0].output).toBe("response");
57
- });
58
-
59
- it("does NOT reconnect after intentional close()", async () => {
60
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
61
-
62
- const { WSTransport } = await import("./transport.js");
63
- const ws = new WSTransport();
64
- ws.connect();
65
-
66
- // Should have created a WS instance
67
- expect(mockWsInstances.length).toBeGreaterThan(0);
68
- const instanceCount = mockWsInstances.length;
69
-
70
- // Close intentionally
71
- ws.close();
72
-
73
- // Trigger the "close" event that was registered — already fired by our mock's close()
74
- // Wait a tick for any setTimeout reconnect to fire
75
- await new Promise(r => setTimeout(r, 1100));
76
-
77
- // Should NOT have created new WS instances (no reconnect)
78
- expect(mockWsInstances.length).toBe(instanceCount);
79
- });
80
- });
package/src/transport.ts DELETED
@@ -1,188 +0,0 @@
1
- import WebSocket from "ws";
2
- import { getConfig } from "./config.js";
3
-
4
- export interface Transport {
5
- send(eventType: string, data: Record<string, unknown>): void;
6
- close(): void;
7
- }
8
-
9
- export class WSTransport implements Transport {
10
- private ws: WebSocket | null = null;
11
- private connected = false;
12
- private closed = false;
13
- private backoff = 1000;
14
- private queue: string[] = [];
15
-
16
- get isConnected() { return this.connected; }
17
-
18
- connect() {
19
- if (this.closed) return;
20
- const cfg = getConfig();
21
- const url = `${cfg.wsUrl}/ws/v1/stream`;
22
- this.ws = new WebSocket(url);
23
-
24
- this.ws.on("open", () => {
25
- this.ws!.send(JSON.stringify({ type: "auth", api_key: cfg.apiKey }));
26
- });
27
-
28
- this.ws.on("message", (raw) => {
29
- const msg = JSON.parse(raw.toString());
30
- if (msg.type === "auth_ok") {
31
- this.connected = true;
32
- this.backoff = 1000;
33
- this.flushQueue();
34
- } else if (msg.type === "ping") {
35
- this.ws?.send(JSON.stringify({ type: "pong" }));
36
- }
37
- });
38
-
39
- this.ws.on("close", () => {
40
- this.connected = false;
41
- this.ws = null;
42
- if (!this.closed) {
43
- setTimeout(() => this.reconnect(), this.backoff);
44
- this.backoff = Math.min(this.backoff * 2, 30000);
45
- }
46
- });
47
-
48
- this.ws.on("error", () => {
49
- this.ws?.close();
50
- });
51
- }
52
-
53
- private reconnect() {
54
- if (!this.closed && !this.connected && !this.ws) this.connect();
55
- }
56
-
57
- private flushQueue() {
58
- while (this.queue.length && this.connected) {
59
- this.ws?.send(this.queue.shift()!);
60
- }
61
- }
62
-
63
- send(eventType: string, data: Record<string, unknown>) {
64
- const msg = JSON.stringify({ type: eventType, data });
65
- if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
66
- this.ws.send(msg);
67
- } else {
68
- this.queue.push(msg);
69
- if (!this.ws && !this.closed) this.connect();
70
- }
71
- }
72
-
73
- close() {
74
- this.closed = true;
75
- if (this.ws) {
76
- this.ws.close();
77
- this.ws = null;
78
- }
79
- this.connected = false;
80
- }
81
- }
82
-
83
- export class HTTPTransport implements Transport {
84
- private traceData: Record<string, unknown> | null = null;
85
- private spans: Record<string, unknown>[] = [];
86
-
87
- send(eventType: string, data: Record<string, unknown>) {
88
- if (eventType === "trace_started") {
89
- this.traceData = data;
90
- } else if (eventType === "span_started" || eventType === "span_ended") {
91
- this.spans.push({ ...data, _event: eventType });
92
- } else if (eventType === "trace_ended") {
93
- if (this.traceData) Object.assign(this.traceData, data);
94
- this.flush();
95
- }
96
- }
97
-
98
- flush() {
99
- if (!this.traceData) return;
100
- const cfg = getConfig();
101
- const url = `${cfg.baseUrl}/api/v1/traces`;
102
- const body = { ...this.traceData, spans: this.buildSpans() };
103
- fetch(url, {
104
- method: "POST",
105
- headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
106
- body: JSON.stringify(body),
107
- }).catch(() => {});
108
- this.traceData = null;
109
- this.spans = [];
110
- }
111
-
112
- private buildSpans(): Record<string, unknown>[] {
113
- const merged = new Map<string, Record<string, unknown>>();
114
- for (const ev of this.spans) {
115
- const { _event, ...rest } = ev;
116
- const id = rest.id as string;
117
- if (_event === "span_started") {
118
- merged.set(id, rest);
119
- } else if (_event === "span_ended" && merged.has(id)) {
120
- Object.assign(merged.get(id)!, rest);
121
- }
122
- }
123
- return [...merged.values()];
124
- }
125
-
126
- close() {
127
- this.flush();
128
- }
129
- }
130
-
131
- export function createTransport(mode: "ws" | "http" | "auto" = "auto"): Transport {
132
- if (mode === "http") return new HTTPTransport();
133
- if (mode === "ws") return new WSTransport();
134
-
135
- // Auto: start with WS, fall back to HTTP if connection fails within timeout
136
- const ws = new WSTransport();
137
- const http = new HTTPTransport();
138
- let useWs = true;
139
- let decided = false;
140
- const buffer: Array<{ eventType: string; data: Record<string, unknown> }> = [];
141
-
142
- const fallbackTimer = setTimeout(() => {
143
- if (!decided && !ws.isConnected) {
144
- decided = true;
145
- useWs = false;
146
- ws.close();
147
- for (const item of buffer.splice(0)) http.send(item.eventType, item.data);
148
- }
149
- }, 5000);
150
-
151
- ws.connect();
152
-
153
- const originalSend = ws.send.bind(ws);
154
- const checkConnected = () => {
155
- if (!decided && ws.isConnected) {
156
- decided = true;
157
- clearTimeout(fallbackTimer);
158
- for (const item of buffer.splice(0)) originalSend(item.eventType, item.data);
159
- }
160
- };
161
-
162
- return {
163
- send(eventType: string, data: Record<string, unknown>) {
164
- checkConnected();
165
- if (decided) {
166
- if (useWs) originalSend(eventType, data);
167
- else http.send(eventType, data);
168
- } else {
169
- buffer.push({ eventType, data });
170
- }
171
- },
172
- close() {
173
- clearTimeout(fallbackTimer);
174
- if (!decided) {
175
- // WS not yet connected — force HTTP fallback to avoid data loss
176
- decided = true;
177
- useWs = false;
178
- ws.close();
179
- for (const item of buffer.splice(0)) http.send(item.eventType, item.data);
180
- http.close();
181
- } else if (useWs) {
182
- ws.close();
183
- } else {
184
- http.close();
185
- }
186
- },
187
- };
188
- }
package/src/utils.ts DELETED
@@ -1,23 +0,0 @@
1
- import { randomUUID } from "crypto";
2
-
3
- export function genId(): string {
4
- return randomUUID();
5
- }
6
-
7
- export function nowIso(): string {
8
- return new Date().toISOString();
9
- }
10
-
11
- export function utcNow(): Date {
12
- return new Date();
13
- }
14
-
15
- export function truncateJson(obj: unknown, maxBytes = 10240): unknown {
16
- try {
17
- const s = JSON.stringify(obj);
18
- if (Buffer.byteLength(s) <= maxBytes) return obj;
19
- return JSON.parse(Buffer.from(s).subarray(0, maxBytes).toString());
20
- } catch {
21
- return String(obj).slice(0, maxBytes);
22
- }
23
- }
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "outDir": "dist",
8
- "rootDir": "src",
9
- "declaration": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "resolveJsonModule": true,
14
- "isolatedModules": true
15
- },
16
- "include": ["src"],
17
- "exclude": ["node_modules", "dist"]
18
- }
package/vitest.config.ts DELETED
@@ -1,2 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
- export default defineConfig({ test: { globals: true, environment: "node", include: ["src/**/*.test.ts"] } });