retrace-sdk 0.1.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.
@@ -0,0 +1,133 @@
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 backoff = 1000;
13
+ private queue: string[] = [];
14
+
15
+ connect() {
16
+ const cfg = getConfig();
17
+ const url = `${cfg.wsUrl}/ws/v1/stream`;
18
+ this.ws = new WebSocket(url);
19
+
20
+ this.ws.on("open", () => {
21
+ this.ws!.send(JSON.stringify({ type: "auth", api_key: cfg.apiKey }));
22
+ });
23
+
24
+ this.ws.on("message", (raw) => {
25
+ const msg = JSON.parse(raw.toString());
26
+ if (msg.type === "auth_ok") {
27
+ this.connected = true;
28
+ this.backoff = 1000;
29
+ this.flushQueue();
30
+ } else if (msg.type === "ping") {
31
+ this.ws?.send(JSON.stringify({ type: "pong" }));
32
+ }
33
+ });
34
+
35
+ this.ws.on("close", () => {
36
+ this.connected = false;
37
+ this.ws = null;
38
+ setTimeout(() => this.reconnect(), this.backoff);
39
+ this.backoff = Math.min(this.backoff * 2, 30000);
40
+ });
41
+
42
+ this.ws.on("error", () => {
43
+ this.ws?.close();
44
+ });
45
+ }
46
+
47
+ private reconnect() {
48
+ if (!this.connected && !this.ws) this.connect();
49
+ }
50
+
51
+ private flushQueue() {
52
+ while (this.queue.length && this.connected) {
53
+ this.ws?.send(this.queue.shift()!);
54
+ }
55
+ }
56
+
57
+ send(eventType: string, data: Record<string, unknown>) {
58
+ const msg = JSON.stringify({ type: eventType, data });
59
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
60
+ this.ws.send(msg);
61
+ } else {
62
+ this.queue.push(msg);
63
+ if (!this.ws) this.connect();
64
+ }
65
+ }
66
+
67
+ close() {
68
+ if (this.ws) {
69
+ this.ws.close();
70
+ this.ws = null;
71
+ }
72
+ this.connected = false;
73
+ }
74
+ }
75
+
76
+ export class HTTPTransport implements Transport {
77
+ private traceData: Record<string, unknown> | null = null;
78
+ private spans: Record<string, unknown>[] = [];
79
+
80
+ send(eventType: string, data: Record<string, unknown>) {
81
+ if (eventType === "trace_started") {
82
+ this.traceData = data;
83
+ } else if (eventType === "span_started" || eventType === "span_ended") {
84
+ this.spans.push({ ...data, _event: eventType });
85
+ } else if (eventType === "trace_ended") {
86
+ if (this.traceData) Object.assign(this.traceData, data);
87
+ this.flush();
88
+ }
89
+ }
90
+
91
+ flush() {
92
+ if (!this.traceData) return;
93
+ const cfg = getConfig();
94
+ const url = `${cfg.baseUrl}/api/v1/traces`;
95
+ const body = { ...this.traceData, spans: this.buildSpans() };
96
+ fetch(url, {
97
+ method: "POST",
98
+ headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
99
+ body: JSON.stringify(body),
100
+ }).catch(() => {});
101
+ this.traceData = null;
102
+ this.spans = [];
103
+ }
104
+
105
+ private buildSpans(): Record<string, unknown>[] {
106
+ const merged = new Map<string, Record<string, unknown>>();
107
+ for (const ev of this.spans) {
108
+ const { _event, ...rest } = ev;
109
+ const id = rest.id as string;
110
+ if (_event === "span_started") {
111
+ merged.set(id, rest);
112
+ } else if (_event === "span_ended" && merged.has(id)) {
113
+ Object.assign(merged.get(id)!, rest);
114
+ }
115
+ }
116
+ return [...merged.values()];
117
+ }
118
+
119
+ close() {
120
+ this.flush();
121
+ }
122
+ }
123
+
124
+ export function createTransport(mode: "ws" | "http" | "auto" = "auto"): Transport {
125
+ if (mode === "http") return new HTTPTransport();
126
+ if (mode === "ws") return new WSTransport();
127
+ // Auto: try WS
128
+ try {
129
+ return new WSTransport();
130
+ } catch {
131
+ return new HTTPTransport();
132
+ }
133
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,23 @@
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 ADDED
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import { defineConfig } from "vitest/config";
2
+ export default defineConfig({ test: { globals: true, environment: "node", include: ["src/**/*.test.ts"] } });