observa-sdk 0.0.1

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 ADDED
@@ -0,0 +1,251 @@
1
+ # Observa SDK
2
+
3
+ Enterprise-grade observability SDK for AI applications. Track and monitor LLM interactions with zero friction.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install observa-sdk
9
+ ```
10
+
11
+ ## Getting Started
12
+
13
+ ### 1. Sign Up
14
+
15
+ Get your API key by signing up at [https://app.observa.ai/signup](https://app.observa.ai/signup) (or your Observa API endpoint).
16
+
17
+ The signup process automatically:
18
+ - Creates your tenant account
19
+ - Sets up a default "Production" project
20
+ - Provisions your Tinybird token
21
+ - Generates your JWT API key
22
+
23
+ You'll receive your API key immediately after signup.
24
+
25
+ ### 2. Install SDK
26
+
27
+ ```bash
28
+ npm install observa-sdk
29
+ ```
30
+
31
+ ### 3. Initialize SDK
32
+
33
+ ```typescript
34
+ import { init } from "observa-sdk";
35
+
36
+ const observa = init({
37
+ apiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // Your API key from signup
38
+ });
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ### JWT-based API Key (Recommended)
44
+
45
+ After signing up, you'll receive a JWT-formatted API key that automatically encodes your tenant and project context:
46
+
47
+ ```typescript
48
+ import { init } from "observa-sdk";
49
+
50
+ // Initialize with JWT API key from signup (automatically extracts tenant/project context)
51
+ const observa = init({
52
+ apiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // Your API key from signup
53
+ });
54
+
55
+ // Track AI interactions with simple wrapping
56
+ const response = await observa.track(
57
+ { query: "What is the weather?" },
58
+ () => fetch("https://api.openai.com/v1/chat/completions", {
59
+ method: "POST",
60
+ headers: { /* ... */ },
61
+ body: JSON.stringify({ /* ... */ }),
62
+ })
63
+ );
64
+ ```
65
+
66
+ ### Legacy API Key Format
67
+
68
+ ```typescript
69
+ // For backward compatibility, you can still provide tenantId/projectId explicitly
70
+ const observa = init({
71
+ apiKey: "your-api-key",
72
+ tenantId: "acme_corp",
73
+ projectId: "prod_app",
74
+ environment: "prod", // optional, defaults to "dev"
75
+ });
76
+ ```
77
+
78
+ ## Multi-Tenant Architecture
79
+
80
+ Observa SDK uses a **multi-tenant shared runtime architecture** for optimal cost, scalability, and operational simplicity.
81
+
82
+ ### Architecture Pattern
83
+
84
+ - **Shared Infrastructure**: Single Tinybird/ClickHouse cluster shared across all tenants
85
+ - **Data Isolation**: Multi-layer isolation via partitioning, token scoping, and row-level security
86
+ - **Performance**: Partitioned by tenant_id for efficient queries
87
+ - **Security**: JWT-based authentication with automatic tenant context extraction
88
+
89
+ ### Data Storage
90
+
91
+ All tenant data is stored in a single shared table with:
92
+
93
+ - **Partitioning**: `PARTITION BY (tenant_id, toYYYYMM(date))`
94
+ - **Ordering**: `ORDER BY (tenant_id, project_id, timestamp, trace_id)`
95
+ - **Isolation**: Physical separation at partition level + logical separation via token scoping
96
+
97
+ ### Security Model
98
+
99
+ 1. **JWT Authentication**: API keys are JWTs encoding tenant/project context
100
+ 2. **Token Scoping**: Each tenant gets a Tinybird token scoped to their `tenant_id`
101
+ 3. **Automatic Filtering**: All queries automatically filtered by tenant context
102
+ 4. **Row-Level Security**: Token-based access control prevents cross-tenant access
103
+
104
+ ## JWT API Key Format
105
+
106
+ The SDK supports JWT-formatted API keys that encode tenant context:
107
+
108
+ ```json
109
+ {
110
+ "tenantId": "acme_corp",
111
+ "projectId": "prod_app",
112
+ "environment": "prod",
113
+ "iat": 1234567890,
114
+ "exp": 1234654290
115
+ }
116
+ ```
117
+
118
+ **JWT Structure**:
119
+ - `tenantId` (required): Unique identifier for the tenant/organization
120
+ - `projectId` (required): Project identifier within the tenant
121
+ - `environment` (optional): `"dev"` or `"prod"` (defaults to `"dev"`)
122
+ - `iat` (optional): Issued at timestamp
123
+ - `exp` (optional): Expiration timestamp
124
+
125
+ When using a JWT API key, the SDK automatically extracts `tenantId` and `projectId` - you don't need to provide them in the config.
126
+
127
+ ## Configuration
128
+
129
+ ```typescript
130
+ interface ObservaInitConfig {
131
+ // API key (JWT or legacy format)
132
+ apiKey: string;
133
+
134
+ // Tenant context (optional if API key is JWT, required for legacy keys)
135
+ tenantId?: string;
136
+ projectId?: string;
137
+ environment?: "dev" | "prod";
138
+
139
+ // SDK behavior
140
+ mode?: "development" | "production";
141
+ sampleRate?: number; // 0..1, default: 1.0
142
+ maxResponseChars?: number; // default: 50000
143
+ }
144
+ ```
145
+
146
+ ### Options
147
+
148
+ - **apiKey**: Your Observa API key (JWT format recommended)
149
+ - **tenantId** / **projectId**: Required only for legacy API keys
150
+ - **environment**: `"dev"` or `"prod"` (defaults to `"dev"`)
151
+ - **mode**: SDK mode - `"development"` logs traces to console, `"production"` sends to Observa
152
+ - **sampleRate**: Fraction of traces to record (0.0 to 1.0)
153
+ - **maxResponseChars**: Maximum response size to capture (prevents huge payloads)
154
+
155
+ ## API Reference
156
+
157
+ ### `init(config: ObservaInitConfig)`
158
+
159
+ Initialize the Observa SDK instance.
160
+
161
+ ### `observa.track(event, action)`
162
+
163
+ Track an AI interaction.
164
+
165
+ **Parameters**:
166
+ - `event.query` (required): The user query/prompt
167
+ - `event.context` (optional): Additional context
168
+ - `event.model` (optional): Model identifier
169
+ - `event.metadata` (optional): Custom metadata
170
+ - `action`: Function that returns a `Promise<Response>` (typically a fetch call)
171
+
172
+ **Returns**: `Promise<Response>` (the original response, unmodified)
173
+
174
+ **Example**:
175
+ ```typescript
176
+ const response = await observa.track(
177
+ {
178
+ query: "What is machine learning?",
179
+ model: "gpt-4",
180
+ metadata: { userId: "123" },
181
+ },
182
+ () => fetch("https://api.openai.com/v1/chat/completions", {
183
+ method: "POST",
184
+ headers: {
185
+ "Authorization": `Bearer ${openaiKey}`,
186
+ "Content-Type": "application/json",
187
+ },
188
+ body: JSON.stringify({
189
+ model: "gpt-4",
190
+ messages: [{ role: "user", content: "What is machine learning?" }],
191
+ }),
192
+ })
193
+ );
194
+ ```
195
+
196
+ ## Data Captured
197
+
198
+ The SDK automatically captures:
199
+
200
+ - **Request Data**: Query, context, model, metadata
201
+ - **Response Data**: Full response text, response length
202
+ - **Token Usage**: Prompt tokens, completion tokens, total tokens
203
+ - **Performance Metrics**: Latency, time-to-first-token, streaming duration
204
+ - **Response Metadata**: Status codes, finish reasons, response IDs
205
+ - **Trace Information**: Trace ID, span ID, timestamps
206
+
207
+ ## Development Mode
208
+
209
+ In development mode (`mode: "development"`), the SDK:
210
+
211
+ - Logs beautifully formatted traces to the console
212
+ - Still sends data to Observa (for testing)
213
+ - Shows tenant context, performance metrics, and token usage
214
+
215
+ ## Production Mode
216
+
217
+ In production mode (`mode: "production"` or `NODE_ENV=production`):
218
+
219
+ - Data is sent to Observa backend
220
+ - No console logs (except errors)
221
+ - Optimized for performance
222
+
223
+ ## Multi-Tenant Isolation Guarantees
224
+
225
+ 1. **Storage Layer**: Data partitioned by `tenant_id` (physical separation)
226
+ 2. **Application Layer**: JWT encodes tenant context (logical separation)
227
+ 3. **API Layer**: Token-scoped access (row-level security)
228
+ 4. **Query Layer**: Automatic tenant filtering (no cross-tenant queries possible)
229
+
230
+ ## Browser & Node.js Support
231
+
232
+ The SDK works in both browser and Node.js environments:
233
+
234
+ - **Browser**: Uses `atob` for base64 decoding
235
+ - **Node.js**: Uses `Buffer` for base64 decoding
236
+ - **Universal**: No environment-specific dependencies
237
+
238
+ ## Onboarding Flow
239
+
240
+ 1. **Sign Up**: Visit the signup page and provide your email and company name
241
+ 2. **Get API Key**: Receive your JWT API key immediately
242
+ 3. **Install SDK**: `npm install observa-sdk`
243
+ 4. **Initialize**: Use your API key to initialize the SDK
244
+ 5. **Start Tracking**: Begin tracking your AI interactions
245
+
246
+ The entire onboarding process takes less than 5 minutes, and you can start tracking traces immediately after signup.
247
+
248
+ ## License
249
+
250
+ MIT
251
+
package/dist/index.cjs ADDED
@@ -0,0 +1,464 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Observa: () => Observa,
24
+ init: () => init
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ function getNodeEnv() {
28
+ try {
29
+ const proc = globalThis.process;
30
+ return proc?.env?.NODE_ENV;
31
+ } catch {
32
+ return void 0;
33
+ }
34
+ }
35
+ function decodeJWT(token) {
36
+ try {
37
+ const parts = token.split(".");
38
+ if (parts.length !== 3) {
39
+ return null;
40
+ }
41
+ const payload = parts[1];
42
+ if (!payload) {
43
+ return null;
44
+ }
45
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
46
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
47
+ let decoded;
48
+ try {
49
+ if (typeof atob !== "undefined") {
50
+ decoded = atob(padded);
51
+ } else {
52
+ const BufferClass = globalThis.Buffer;
53
+ if (BufferClass) {
54
+ decoded = BufferClass.from(padded, "base64").toString("utf-8");
55
+ } else {
56
+ return null;
57
+ }
58
+ }
59
+ } catch {
60
+ return null;
61
+ }
62
+ return JSON.parse(decoded);
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function extractTenantContextFromAPIKey(apiKey) {
68
+ const payload = decodeJWT(apiKey);
69
+ if (!payload) {
70
+ return null;
71
+ }
72
+ const tenantId = payload.tenantId;
73
+ const projectId = payload.projectId;
74
+ if (!tenantId || !projectId) {
75
+ return null;
76
+ }
77
+ const result = {
78
+ tenantId,
79
+ projectId
80
+ };
81
+ if (payload.environment !== void 0) {
82
+ result.environment = payload.environment;
83
+ }
84
+ return result;
85
+ }
86
+ function parseSSEChunk(chunk) {
87
+ const lines = chunk.split("\n");
88
+ for (const line of lines) {
89
+ if (!line.startsWith("data: ")) continue;
90
+ const payload = line.slice(6).trim();
91
+ if (payload === "[DONE]") return { done: true };
92
+ try {
93
+ return JSON.parse(payload);
94
+ } catch {
95
+ }
96
+ }
97
+ return {};
98
+ }
99
+ function extractMetadataFromChunks(chunks) {
100
+ let tokensPrompt;
101
+ let tokensCompletion;
102
+ let tokensTotal;
103
+ let model;
104
+ let finishReason;
105
+ let responseId;
106
+ let systemFingerprint;
107
+ for (const chunk of chunks) {
108
+ const parsed = parseSSEChunk(chunk);
109
+ if (parsed?.usage) {
110
+ tokensPrompt = parsed.usage.prompt_tokens ?? tokensPrompt;
111
+ tokensCompletion = parsed.usage.completion_tokens ?? tokensCompletion;
112
+ tokensTotal = parsed.usage.total_tokens ?? tokensTotal;
113
+ }
114
+ if (parsed?.model && !model) model = parsed.model;
115
+ if (parsed?.id && !responseId) responseId = parsed.id;
116
+ if (parsed?.system_fingerprint && !systemFingerprint)
117
+ systemFingerprint = parsed.system_fingerprint;
118
+ const fr = parsed?.choices?.[0]?.finish_reason;
119
+ if (fr && !finishReason) finishReason = fr;
120
+ }
121
+ return {
122
+ tokensPrompt: tokensPrompt ?? null,
123
+ tokensCompletion: tokensCompletion ?? null,
124
+ tokensTotal: tokensTotal ?? null,
125
+ model: model ?? null,
126
+ finishReason: finishReason ?? null,
127
+ responseId: responseId ?? null,
128
+ systemFingerprint: systemFingerprint ?? null
129
+ };
130
+ }
131
+ function formatBeautifulLog(trace) {
132
+ const colors = {
133
+ reset: "\x1B[0m",
134
+ bright: "\x1B[1m",
135
+ dim: "\x1B[2m",
136
+ blue: "\x1B[34m",
137
+ cyan: "\x1B[36m",
138
+ green: "\x1B[32m",
139
+ yellow: "\x1B[33m",
140
+ magenta: "\x1B[35m",
141
+ gray: "\x1B[90m"
142
+ };
143
+ const formatValue = (label, value, color = colors.cyan) => `${colors.dim}${label}:${colors.reset} ${color}${value}${colors.reset}`;
144
+ console.log("\n" + "\u2550".repeat(90));
145
+ console.log(
146
+ `${colors.bright}${colors.blue}\u{1F50D} OBSERVA TRACE${colors.reset} ${colors.gray}${trace.traceId}${colors.reset}`
147
+ );
148
+ console.log("\u2500".repeat(90));
149
+ console.log(`${colors.bright}\u{1F3F7} Tenant${colors.reset}`);
150
+ console.log(` ${formatValue("tenantId", trace.tenantId, colors.gray)}`);
151
+ console.log(` ${formatValue("projectId", trace.projectId, colors.gray)}`);
152
+ console.log(` ${formatValue("env", trace.environment, colors.gray)}`);
153
+ console.log(`
154
+ ${colors.bright}\u{1F4CB} Request${colors.reset}`);
155
+ console.log(
156
+ ` ${formatValue(
157
+ "Timestamp",
158
+ new Date(trace.timestamp).toLocaleString(),
159
+ colors.gray
160
+ )}`
161
+ );
162
+ if (trace.model)
163
+ console.log(` ${formatValue("Model", trace.model, colors.yellow)}`);
164
+ const queryPreview = trace.query.length > 80 ? trace.query.slice(0, 80) + "..." : trace.query;
165
+ console.log(` ${formatValue("Query", queryPreview, colors.green)}`);
166
+ if (trace.context) {
167
+ const ctxPreview = trace.context.length > 120 ? trace.context.slice(0, 120) + "..." : trace.context;
168
+ console.log(` ${formatValue("Context", ctxPreview, colors.cyan)}`);
169
+ }
170
+ console.log(`
171
+ ${colors.bright}\u26A1 Performance${colors.reset}`);
172
+ console.log(
173
+ ` ${formatValue("Latency", `${trace.latencyMs}ms`, colors.green)}`
174
+ );
175
+ if (trace.timeToFirstTokenMs != null) {
176
+ console.log(
177
+ ` ${formatValue("TTFB", `${trace.timeToFirstTokenMs}ms`, colors.cyan)}`
178
+ );
179
+ }
180
+ if (trace.streamingDurationMs != null) {
181
+ console.log(
182
+ ` ${formatValue(
183
+ "Streaming",
184
+ `${trace.streamingDurationMs}ms`,
185
+ colors.cyan
186
+ )}`
187
+ );
188
+ }
189
+ console.log(`
190
+ ${colors.bright}\u{1FA99} Tokens${colors.reset}`);
191
+ if (trace.tokensPrompt != null)
192
+ console.log(` ${formatValue("Prompt", trace.tokensPrompt)}`);
193
+ if (trace.tokensCompletion != null)
194
+ console.log(` ${formatValue("Completion", trace.tokensCompletion)}`);
195
+ if (trace.tokensTotal != null)
196
+ console.log(
197
+ ` ${formatValue(
198
+ "Total",
199
+ trace.tokensTotal,
200
+ colors.bright + colors.yellow
201
+ )}`
202
+ );
203
+ console.log(`
204
+ ${colors.bright}\u{1F4E4} Response${colors.reset}`);
205
+ console.log(
206
+ ` ${formatValue(
207
+ "Length",
208
+ `${trace.responseLength.toLocaleString()} chars`,
209
+ colors.cyan
210
+ )}`
211
+ );
212
+ if (trace.status != null) {
213
+ const statusColor = trace.status >= 200 && trace.status < 300 ? colors.green : colors.yellow;
214
+ console.log(
215
+ ` ${formatValue(
216
+ "Status",
217
+ `${trace.status} ${trace.statusText ?? ""}`,
218
+ statusColor
219
+ )}`
220
+ );
221
+ }
222
+ if (trace.finishReason)
223
+ console.log(
224
+ ` ${formatValue("Finish", trace.finishReason, colors.magenta)}`
225
+ );
226
+ const respPreview = trace.response.length > 300 ? trace.response.slice(0, 300) + "..." : trace.response;
227
+ console.log(`
228
+ ${colors.bright}\u{1F4AC} Response Preview${colors.reset}`);
229
+ console.log(`${colors.dim}${respPreview}${colors.reset}`);
230
+ if (trace.metadata && Object.keys(trace.metadata).length) {
231
+ console.log(`
232
+ ${colors.bright}\u{1F4CE} Metadata${colors.reset}`);
233
+ for (const [k, v] of Object.entries(trace.metadata)) {
234
+ const valueStr = typeof v === "object" ? JSON.stringify(v) : String(v);
235
+ console.log(` ${formatValue(k, valueStr, colors.gray)}`);
236
+ }
237
+ }
238
+ console.log("\u2550".repeat(90) + "\n");
239
+ }
240
+ var Observa = class {
241
+ apiKey;
242
+ tenantId;
243
+ projectId;
244
+ environment;
245
+ apiUrl;
246
+ isProduction;
247
+ sampleRate;
248
+ maxResponseChars;
249
+ constructor(config) {
250
+ this.apiKey = config.apiKey;
251
+ let apiUrlEnv;
252
+ try {
253
+ const proc = globalThis.process;
254
+ apiUrlEnv = proc?.env?.OBSERVA_API_URL;
255
+ } catch {
256
+ }
257
+ this.apiUrl = config.apiUrl || apiUrlEnv || "https://api.observa.ai";
258
+ const jwtContext = extractTenantContextFromAPIKey(config.apiKey);
259
+ if (jwtContext) {
260
+ this.tenantId = jwtContext.tenantId;
261
+ this.projectId = jwtContext.projectId;
262
+ this.environment = jwtContext.environment ?? config.environment ?? "dev";
263
+ } else {
264
+ if (!config.tenantId || !config.projectId) {
265
+ throw new Error(
266
+ "Observa SDK: tenantId and projectId are required when using legacy API key format. Either provide a JWT-formatted API key (which encodes tenant/project context) or explicitly provide tenantId and projectId in the config."
267
+ );
268
+ }
269
+ this.tenantId = config.tenantId;
270
+ this.projectId = config.projectId;
271
+ this.environment = config.environment ?? "dev";
272
+ }
273
+ if (!this.tenantId || !this.projectId) {
274
+ throw new Error(
275
+ "Observa SDK: tenantId and projectId must be set. This should never happen - please report this error."
276
+ );
277
+ }
278
+ const nodeEnv = getNodeEnv();
279
+ this.isProduction = config.mode === "production" || nodeEnv === "production";
280
+ this.sampleRate = typeof config.sampleRate === "number" ? config.sampleRate : 1;
281
+ this.maxResponseChars = config.maxResponseChars ?? 5e4;
282
+ console.log(
283
+ `\u{1F4A7} Observa SDK Initialized (${this.isProduction ? "production" : "development"})`
284
+ );
285
+ if (!this.isProduction) {
286
+ console.log(`\u{1F517} [Observa] API URL: ${this.apiUrl}`);
287
+ console.log(`\u{1F517} [Observa] Tenant: ${this.tenantId}`);
288
+ console.log(`\u{1F517} [Observa] Project: ${this.projectId}`);
289
+ console.log(
290
+ `\u{1F517} [Observa] Auth: ${jwtContext ? "JWT (auto-extracted)" : "Legacy (config)"}`
291
+ );
292
+ }
293
+ }
294
+ async track(event, action) {
295
+ if (this.sampleRate < 1 && Math.random() > this.sampleRate) {
296
+ return action();
297
+ }
298
+ const startTime = Date.now();
299
+ const traceId = crypto.randomUUID();
300
+ const spanId = traceId;
301
+ const originalResponse = await action();
302
+ if (!originalResponse.body) return originalResponse;
303
+ const responseHeaders = {};
304
+ originalResponse.headers.forEach((value, key) => {
305
+ responseHeaders[key] = value;
306
+ });
307
+ const [stream1, stream2] = originalResponse.body.tee();
308
+ this.captureStream({
309
+ stream: stream2,
310
+ event,
311
+ traceId,
312
+ spanId,
313
+ parentSpanId: null,
314
+ startTime,
315
+ status: originalResponse.status,
316
+ statusText: originalResponse.statusText,
317
+ headers: responseHeaders
318
+ });
319
+ return new Response(stream1, {
320
+ headers: originalResponse.headers,
321
+ status: originalResponse.status,
322
+ statusText: originalResponse.statusText
323
+ });
324
+ }
325
+ async captureStream(args) {
326
+ const {
327
+ stream,
328
+ event,
329
+ traceId,
330
+ spanId,
331
+ parentSpanId,
332
+ startTime,
333
+ status,
334
+ statusText,
335
+ headers
336
+ } = args;
337
+ try {
338
+ const reader = stream.getReader();
339
+ const decoder = new TextDecoder();
340
+ let fullResponse = "";
341
+ let firstTokenTime;
342
+ const chunks = [];
343
+ let buffer = "";
344
+ while (true) {
345
+ const { done, value } = await reader.read();
346
+ if (done) break;
347
+ if (!firstTokenTime && value && value.length > 0) {
348
+ firstTokenTime = Date.now();
349
+ }
350
+ const chunk = decoder.decode(value, { stream: true });
351
+ chunks.push(chunk);
352
+ buffer += chunk;
353
+ const lines = buffer.split("\n");
354
+ buffer = lines.pop() || "";
355
+ for (const line of lines) {
356
+ if (!line.startsWith("data: ")) continue;
357
+ const data = line.slice(6).trim();
358
+ if (!data || data === "[DONE]") continue;
359
+ try {
360
+ const parsed = JSON.parse(data);
361
+ if (parsed?.choices?.[0]?.delta?.content) {
362
+ fullResponse += parsed.choices[0].delta.content;
363
+ } else if (parsed?.choices?.[0]?.text) {
364
+ fullResponse += parsed.choices[0].text;
365
+ } else if (typeof parsed?.content === "string") {
366
+ fullResponse += parsed.content;
367
+ }
368
+ } catch {
369
+ fullResponse += data;
370
+ }
371
+ }
372
+ if (fullResponse.length > this.maxResponseChars) {
373
+ fullResponse = fullResponse.slice(0, this.maxResponseChars) + "\u2026[TRUNCATED]";
374
+ break;
375
+ }
376
+ }
377
+ if (buffer.trim()) {
378
+ fullResponse += buffer;
379
+ }
380
+ const endTime = Date.now();
381
+ const latencyMs = endTime - startTime;
382
+ const timeToFirstTokenMs = firstTokenTime != null ? firstTokenTime - startTime : null;
383
+ const streamingDurationMs = firstTokenTime != null ? endTime - firstTokenTime : null;
384
+ const extracted = extractMetadataFromChunks(chunks);
385
+ if (!this.tenantId || !this.projectId) {
386
+ throw new Error(
387
+ "Observa SDK: tenantId and projectId must be set. This indicates a SDK configuration error."
388
+ );
389
+ }
390
+ const traceData = {
391
+ traceId,
392
+ spanId,
393
+ parentSpanId,
394
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
395
+ tenantId: this.tenantId,
396
+ projectId: this.projectId,
397
+ environment: this.environment,
398
+ query: event.query,
399
+ ...event.context !== void 0 && { context: event.context },
400
+ ...(extracted.model ?? event.model) !== void 0 && {
401
+ model: extracted.model ?? event.model
402
+ },
403
+ ...event.metadata !== void 0 && { metadata: event.metadata },
404
+ response: fullResponse,
405
+ responseLength: fullResponse.length,
406
+ tokensPrompt: extracted.tokensPrompt ?? null,
407
+ tokensCompletion: extracted.tokensCompletion ?? null,
408
+ tokensTotal: extracted.tokensTotal ?? null,
409
+ latencyMs,
410
+ timeToFirstTokenMs,
411
+ streamingDurationMs,
412
+ status: status ?? null,
413
+ statusText: statusText ?? null,
414
+ finishReason: extracted.finishReason ?? null,
415
+ responseId: extracted.responseId ?? null,
416
+ systemFingerprint: extracted.systemFingerprint ?? null,
417
+ ...headers !== void 0 && { headers }
418
+ };
419
+ await this.sendTrace(traceData);
420
+ } catch (err) {
421
+ console.error("[Observa] Error capturing stream:", err);
422
+ }
423
+ }
424
+ async sendTrace(trace) {
425
+ if (!this.isProduction) {
426
+ formatBeautifulLog(trace);
427
+ }
428
+ try {
429
+ const url = `${this.apiUrl}/api/v1/traces/ingest`;
430
+ const response = await fetch(url, {
431
+ method: "POST",
432
+ headers: {
433
+ Authorization: `Bearer ${this.apiKey}`,
434
+ "Content-Type": "application/json"
435
+ },
436
+ body: JSON.stringify(trace)
437
+ });
438
+ if (!response.ok) {
439
+ const errorText = await response.text().catch(() => "Unknown error");
440
+ let errorJson;
441
+ try {
442
+ errorJson = JSON.parse(errorText);
443
+ } catch {
444
+ errorJson = { error: errorText };
445
+ }
446
+ console.error(
447
+ `[Observa] Backend API error: ${response.status} ${response.statusText}`,
448
+ errorJson.error || errorText
449
+ );
450
+ } else if (!this.isProduction) {
451
+ console.log(`\u2705 [Observa] Trace sent successfully`);
452
+ console.log(` Trace ID: ${trace.traceId}`);
453
+ }
454
+ } catch (error) {
455
+ console.error("[Observa] Failed to send trace:", error);
456
+ }
457
+ }
458
+ };
459
+ var init = (config) => new Observa(config);
460
+ // Annotate the CommonJS export names for ESM import in node:
461
+ 0 && (module.exports = {
462
+ Observa,
463
+ init
464
+ });
@@ -0,0 +1,33 @@
1
+ interface ObservaInitConfig {
2
+ apiKey: string;
3
+ tenantId?: string;
4
+ projectId?: string;
5
+ environment?: "dev" | "prod";
6
+ apiUrl?: string;
7
+ mode?: "development" | "production";
8
+ sampleRate?: number;
9
+ maxResponseChars?: number;
10
+ }
11
+ interface TrackEventInput {
12
+ query: string;
13
+ context?: string;
14
+ model?: string;
15
+ metadata?: Record<string, any>;
16
+ }
17
+ declare class Observa {
18
+ private apiKey;
19
+ private tenantId;
20
+ private projectId;
21
+ private environment;
22
+ private apiUrl;
23
+ private isProduction;
24
+ private sampleRate;
25
+ private maxResponseChars;
26
+ constructor(config: ObservaInitConfig);
27
+ track(event: TrackEventInput, action: () => Promise<Response>): Promise<Response>;
28
+ private captureStream;
29
+ private sendTrace;
30
+ }
31
+ declare const init: (config: ObservaInitConfig) => Observa;
32
+
33
+ export { Observa, type ObservaInitConfig, type TrackEventInput, init };
@@ -0,0 +1,33 @@
1
+ interface ObservaInitConfig {
2
+ apiKey: string;
3
+ tenantId?: string;
4
+ projectId?: string;
5
+ environment?: "dev" | "prod";
6
+ apiUrl?: string;
7
+ mode?: "development" | "production";
8
+ sampleRate?: number;
9
+ maxResponseChars?: number;
10
+ }
11
+ interface TrackEventInput {
12
+ query: string;
13
+ context?: string;
14
+ model?: string;
15
+ metadata?: Record<string, any>;
16
+ }
17
+ declare class Observa {
18
+ private apiKey;
19
+ private tenantId;
20
+ private projectId;
21
+ private environment;
22
+ private apiUrl;
23
+ private isProduction;
24
+ private sampleRate;
25
+ private maxResponseChars;
26
+ constructor(config: ObservaInitConfig);
27
+ track(event: TrackEventInput, action: () => Promise<Response>): Promise<Response>;
28
+ private captureStream;
29
+ private sendTrace;
30
+ }
31
+ declare const init: (config: ObservaInitConfig) => Observa;
32
+
33
+ export { Observa, type ObservaInitConfig, type TrackEventInput, init };
package/dist/index.js ADDED
@@ -0,0 +1,438 @@
1
+ // src/index.ts
2
+ function getNodeEnv() {
3
+ try {
4
+ const proc = globalThis.process;
5
+ return proc?.env?.NODE_ENV;
6
+ } catch {
7
+ return void 0;
8
+ }
9
+ }
10
+ function decodeJWT(token) {
11
+ try {
12
+ const parts = token.split(".");
13
+ if (parts.length !== 3) {
14
+ return null;
15
+ }
16
+ const payload = parts[1];
17
+ if (!payload) {
18
+ return null;
19
+ }
20
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
21
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
22
+ let decoded;
23
+ try {
24
+ if (typeof atob !== "undefined") {
25
+ decoded = atob(padded);
26
+ } else {
27
+ const BufferClass = globalThis.Buffer;
28
+ if (BufferClass) {
29
+ decoded = BufferClass.from(padded, "base64").toString("utf-8");
30
+ } else {
31
+ return null;
32
+ }
33
+ }
34
+ } catch {
35
+ return null;
36
+ }
37
+ return JSON.parse(decoded);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ function extractTenantContextFromAPIKey(apiKey) {
43
+ const payload = decodeJWT(apiKey);
44
+ if (!payload) {
45
+ return null;
46
+ }
47
+ const tenantId = payload.tenantId;
48
+ const projectId = payload.projectId;
49
+ if (!tenantId || !projectId) {
50
+ return null;
51
+ }
52
+ const result = {
53
+ tenantId,
54
+ projectId
55
+ };
56
+ if (payload.environment !== void 0) {
57
+ result.environment = payload.environment;
58
+ }
59
+ return result;
60
+ }
61
+ function parseSSEChunk(chunk) {
62
+ const lines = chunk.split("\n");
63
+ for (const line of lines) {
64
+ if (!line.startsWith("data: ")) continue;
65
+ const payload = line.slice(6).trim();
66
+ if (payload === "[DONE]") return { done: true };
67
+ try {
68
+ return JSON.parse(payload);
69
+ } catch {
70
+ }
71
+ }
72
+ return {};
73
+ }
74
+ function extractMetadataFromChunks(chunks) {
75
+ let tokensPrompt;
76
+ let tokensCompletion;
77
+ let tokensTotal;
78
+ let model;
79
+ let finishReason;
80
+ let responseId;
81
+ let systemFingerprint;
82
+ for (const chunk of chunks) {
83
+ const parsed = parseSSEChunk(chunk);
84
+ if (parsed?.usage) {
85
+ tokensPrompt = parsed.usage.prompt_tokens ?? tokensPrompt;
86
+ tokensCompletion = parsed.usage.completion_tokens ?? tokensCompletion;
87
+ tokensTotal = parsed.usage.total_tokens ?? tokensTotal;
88
+ }
89
+ if (parsed?.model && !model) model = parsed.model;
90
+ if (parsed?.id && !responseId) responseId = parsed.id;
91
+ if (parsed?.system_fingerprint && !systemFingerprint)
92
+ systemFingerprint = parsed.system_fingerprint;
93
+ const fr = parsed?.choices?.[0]?.finish_reason;
94
+ if (fr && !finishReason) finishReason = fr;
95
+ }
96
+ return {
97
+ tokensPrompt: tokensPrompt ?? null,
98
+ tokensCompletion: tokensCompletion ?? null,
99
+ tokensTotal: tokensTotal ?? null,
100
+ model: model ?? null,
101
+ finishReason: finishReason ?? null,
102
+ responseId: responseId ?? null,
103
+ systemFingerprint: systemFingerprint ?? null
104
+ };
105
+ }
106
+ function formatBeautifulLog(trace) {
107
+ const colors = {
108
+ reset: "\x1B[0m",
109
+ bright: "\x1B[1m",
110
+ dim: "\x1B[2m",
111
+ blue: "\x1B[34m",
112
+ cyan: "\x1B[36m",
113
+ green: "\x1B[32m",
114
+ yellow: "\x1B[33m",
115
+ magenta: "\x1B[35m",
116
+ gray: "\x1B[90m"
117
+ };
118
+ const formatValue = (label, value, color = colors.cyan) => `${colors.dim}${label}:${colors.reset} ${color}${value}${colors.reset}`;
119
+ console.log("\n" + "\u2550".repeat(90));
120
+ console.log(
121
+ `${colors.bright}${colors.blue}\u{1F50D} OBSERVA TRACE${colors.reset} ${colors.gray}${trace.traceId}${colors.reset}`
122
+ );
123
+ console.log("\u2500".repeat(90));
124
+ console.log(`${colors.bright}\u{1F3F7} Tenant${colors.reset}`);
125
+ console.log(` ${formatValue("tenantId", trace.tenantId, colors.gray)}`);
126
+ console.log(` ${formatValue("projectId", trace.projectId, colors.gray)}`);
127
+ console.log(` ${formatValue("env", trace.environment, colors.gray)}`);
128
+ console.log(`
129
+ ${colors.bright}\u{1F4CB} Request${colors.reset}`);
130
+ console.log(
131
+ ` ${formatValue(
132
+ "Timestamp",
133
+ new Date(trace.timestamp).toLocaleString(),
134
+ colors.gray
135
+ )}`
136
+ );
137
+ if (trace.model)
138
+ console.log(` ${formatValue("Model", trace.model, colors.yellow)}`);
139
+ const queryPreview = trace.query.length > 80 ? trace.query.slice(0, 80) + "..." : trace.query;
140
+ console.log(` ${formatValue("Query", queryPreview, colors.green)}`);
141
+ if (trace.context) {
142
+ const ctxPreview = trace.context.length > 120 ? trace.context.slice(0, 120) + "..." : trace.context;
143
+ console.log(` ${formatValue("Context", ctxPreview, colors.cyan)}`);
144
+ }
145
+ console.log(`
146
+ ${colors.bright}\u26A1 Performance${colors.reset}`);
147
+ console.log(
148
+ ` ${formatValue("Latency", `${trace.latencyMs}ms`, colors.green)}`
149
+ );
150
+ if (trace.timeToFirstTokenMs != null) {
151
+ console.log(
152
+ ` ${formatValue("TTFB", `${trace.timeToFirstTokenMs}ms`, colors.cyan)}`
153
+ );
154
+ }
155
+ if (trace.streamingDurationMs != null) {
156
+ console.log(
157
+ ` ${formatValue(
158
+ "Streaming",
159
+ `${trace.streamingDurationMs}ms`,
160
+ colors.cyan
161
+ )}`
162
+ );
163
+ }
164
+ console.log(`
165
+ ${colors.bright}\u{1FA99} Tokens${colors.reset}`);
166
+ if (trace.tokensPrompt != null)
167
+ console.log(` ${formatValue("Prompt", trace.tokensPrompt)}`);
168
+ if (trace.tokensCompletion != null)
169
+ console.log(` ${formatValue("Completion", trace.tokensCompletion)}`);
170
+ if (trace.tokensTotal != null)
171
+ console.log(
172
+ ` ${formatValue(
173
+ "Total",
174
+ trace.tokensTotal,
175
+ colors.bright + colors.yellow
176
+ )}`
177
+ );
178
+ console.log(`
179
+ ${colors.bright}\u{1F4E4} Response${colors.reset}`);
180
+ console.log(
181
+ ` ${formatValue(
182
+ "Length",
183
+ `${trace.responseLength.toLocaleString()} chars`,
184
+ colors.cyan
185
+ )}`
186
+ );
187
+ if (trace.status != null) {
188
+ const statusColor = trace.status >= 200 && trace.status < 300 ? colors.green : colors.yellow;
189
+ console.log(
190
+ ` ${formatValue(
191
+ "Status",
192
+ `${trace.status} ${trace.statusText ?? ""}`,
193
+ statusColor
194
+ )}`
195
+ );
196
+ }
197
+ if (trace.finishReason)
198
+ console.log(
199
+ ` ${formatValue("Finish", trace.finishReason, colors.magenta)}`
200
+ );
201
+ const respPreview = trace.response.length > 300 ? trace.response.slice(0, 300) + "..." : trace.response;
202
+ console.log(`
203
+ ${colors.bright}\u{1F4AC} Response Preview${colors.reset}`);
204
+ console.log(`${colors.dim}${respPreview}${colors.reset}`);
205
+ if (trace.metadata && Object.keys(trace.metadata).length) {
206
+ console.log(`
207
+ ${colors.bright}\u{1F4CE} Metadata${colors.reset}`);
208
+ for (const [k, v] of Object.entries(trace.metadata)) {
209
+ const valueStr = typeof v === "object" ? JSON.stringify(v) : String(v);
210
+ console.log(` ${formatValue(k, valueStr, colors.gray)}`);
211
+ }
212
+ }
213
+ console.log("\u2550".repeat(90) + "\n");
214
+ }
215
+ var Observa = class {
216
+ apiKey;
217
+ tenantId;
218
+ projectId;
219
+ environment;
220
+ apiUrl;
221
+ isProduction;
222
+ sampleRate;
223
+ maxResponseChars;
224
+ constructor(config) {
225
+ this.apiKey = config.apiKey;
226
+ let apiUrlEnv;
227
+ try {
228
+ const proc = globalThis.process;
229
+ apiUrlEnv = proc?.env?.OBSERVA_API_URL;
230
+ } catch {
231
+ }
232
+ this.apiUrl = config.apiUrl || apiUrlEnv || "https://api.observa.ai";
233
+ const jwtContext = extractTenantContextFromAPIKey(config.apiKey);
234
+ if (jwtContext) {
235
+ this.tenantId = jwtContext.tenantId;
236
+ this.projectId = jwtContext.projectId;
237
+ this.environment = jwtContext.environment ?? config.environment ?? "dev";
238
+ } else {
239
+ if (!config.tenantId || !config.projectId) {
240
+ throw new Error(
241
+ "Observa SDK: tenantId and projectId are required when using legacy API key format. Either provide a JWT-formatted API key (which encodes tenant/project context) or explicitly provide tenantId and projectId in the config."
242
+ );
243
+ }
244
+ this.tenantId = config.tenantId;
245
+ this.projectId = config.projectId;
246
+ this.environment = config.environment ?? "dev";
247
+ }
248
+ if (!this.tenantId || !this.projectId) {
249
+ throw new Error(
250
+ "Observa SDK: tenantId and projectId must be set. This should never happen - please report this error."
251
+ );
252
+ }
253
+ const nodeEnv = getNodeEnv();
254
+ this.isProduction = config.mode === "production" || nodeEnv === "production";
255
+ this.sampleRate = typeof config.sampleRate === "number" ? config.sampleRate : 1;
256
+ this.maxResponseChars = config.maxResponseChars ?? 5e4;
257
+ console.log(
258
+ `\u{1F4A7} Observa SDK Initialized (${this.isProduction ? "production" : "development"})`
259
+ );
260
+ if (!this.isProduction) {
261
+ console.log(`\u{1F517} [Observa] API URL: ${this.apiUrl}`);
262
+ console.log(`\u{1F517} [Observa] Tenant: ${this.tenantId}`);
263
+ console.log(`\u{1F517} [Observa] Project: ${this.projectId}`);
264
+ console.log(
265
+ `\u{1F517} [Observa] Auth: ${jwtContext ? "JWT (auto-extracted)" : "Legacy (config)"}`
266
+ );
267
+ }
268
+ }
269
+ async track(event, action) {
270
+ if (this.sampleRate < 1 && Math.random() > this.sampleRate) {
271
+ return action();
272
+ }
273
+ const startTime = Date.now();
274
+ const traceId = crypto.randomUUID();
275
+ const spanId = traceId;
276
+ const originalResponse = await action();
277
+ if (!originalResponse.body) return originalResponse;
278
+ const responseHeaders = {};
279
+ originalResponse.headers.forEach((value, key) => {
280
+ responseHeaders[key] = value;
281
+ });
282
+ const [stream1, stream2] = originalResponse.body.tee();
283
+ this.captureStream({
284
+ stream: stream2,
285
+ event,
286
+ traceId,
287
+ spanId,
288
+ parentSpanId: null,
289
+ startTime,
290
+ status: originalResponse.status,
291
+ statusText: originalResponse.statusText,
292
+ headers: responseHeaders
293
+ });
294
+ return new Response(stream1, {
295
+ headers: originalResponse.headers,
296
+ status: originalResponse.status,
297
+ statusText: originalResponse.statusText
298
+ });
299
+ }
300
+ async captureStream(args) {
301
+ const {
302
+ stream,
303
+ event,
304
+ traceId,
305
+ spanId,
306
+ parentSpanId,
307
+ startTime,
308
+ status,
309
+ statusText,
310
+ headers
311
+ } = args;
312
+ try {
313
+ const reader = stream.getReader();
314
+ const decoder = new TextDecoder();
315
+ let fullResponse = "";
316
+ let firstTokenTime;
317
+ const chunks = [];
318
+ let buffer = "";
319
+ while (true) {
320
+ const { done, value } = await reader.read();
321
+ if (done) break;
322
+ if (!firstTokenTime && value && value.length > 0) {
323
+ firstTokenTime = Date.now();
324
+ }
325
+ const chunk = decoder.decode(value, { stream: true });
326
+ chunks.push(chunk);
327
+ buffer += chunk;
328
+ const lines = buffer.split("\n");
329
+ buffer = lines.pop() || "";
330
+ for (const line of lines) {
331
+ if (!line.startsWith("data: ")) continue;
332
+ const data = line.slice(6).trim();
333
+ if (!data || data === "[DONE]") continue;
334
+ try {
335
+ const parsed = JSON.parse(data);
336
+ if (parsed?.choices?.[0]?.delta?.content) {
337
+ fullResponse += parsed.choices[0].delta.content;
338
+ } else if (parsed?.choices?.[0]?.text) {
339
+ fullResponse += parsed.choices[0].text;
340
+ } else if (typeof parsed?.content === "string") {
341
+ fullResponse += parsed.content;
342
+ }
343
+ } catch {
344
+ fullResponse += data;
345
+ }
346
+ }
347
+ if (fullResponse.length > this.maxResponseChars) {
348
+ fullResponse = fullResponse.slice(0, this.maxResponseChars) + "\u2026[TRUNCATED]";
349
+ break;
350
+ }
351
+ }
352
+ if (buffer.trim()) {
353
+ fullResponse += buffer;
354
+ }
355
+ const endTime = Date.now();
356
+ const latencyMs = endTime - startTime;
357
+ const timeToFirstTokenMs = firstTokenTime != null ? firstTokenTime - startTime : null;
358
+ const streamingDurationMs = firstTokenTime != null ? endTime - firstTokenTime : null;
359
+ const extracted = extractMetadataFromChunks(chunks);
360
+ if (!this.tenantId || !this.projectId) {
361
+ throw new Error(
362
+ "Observa SDK: tenantId and projectId must be set. This indicates a SDK configuration error."
363
+ );
364
+ }
365
+ const traceData = {
366
+ traceId,
367
+ spanId,
368
+ parentSpanId,
369
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
370
+ tenantId: this.tenantId,
371
+ projectId: this.projectId,
372
+ environment: this.environment,
373
+ query: event.query,
374
+ ...event.context !== void 0 && { context: event.context },
375
+ ...(extracted.model ?? event.model) !== void 0 && {
376
+ model: extracted.model ?? event.model
377
+ },
378
+ ...event.metadata !== void 0 && { metadata: event.metadata },
379
+ response: fullResponse,
380
+ responseLength: fullResponse.length,
381
+ tokensPrompt: extracted.tokensPrompt ?? null,
382
+ tokensCompletion: extracted.tokensCompletion ?? null,
383
+ tokensTotal: extracted.tokensTotal ?? null,
384
+ latencyMs,
385
+ timeToFirstTokenMs,
386
+ streamingDurationMs,
387
+ status: status ?? null,
388
+ statusText: statusText ?? null,
389
+ finishReason: extracted.finishReason ?? null,
390
+ responseId: extracted.responseId ?? null,
391
+ systemFingerprint: extracted.systemFingerprint ?? null,
392
+ ...headers !== void 0 && { headers }
393
+ };
394
+ await this.sendTrace(traceData);
395
+ } catch (err) {
396
+ console.error("[Observa] Error capturing stream:", err);
397
+ }
398
+ }
399
+ async sendTrace(trace) {
400
+ if (!this.isProduction) {
401
+ formatBeautifulLog(trace);
402
+ }
403
+ try {
404
+ const url = `${this.apiUrl}/api/v1/traces/ingest`;
405
+ const response = await fetch(url, {
406
+ method: "POST",
407
+ headers: {
408
+ Authorization: `Bearer ${this.apiKey}`,
409
+ "Content-Type": "application/json"
410
+ },
411
+ body: JSON.stringify(trace)
412
+ });
413
+ if (!response.ok) {
414
+ const errorText = await response.text().catch(() => "Unknown error");
415
+ let errorJson;
416
+ try {
417
+ errorJson = JSON.parse(errorText);
418
+ } catch {
419
+ errorJson = { error: errorText };
420
+ }
421
+ console.error(
422
+ `[Observa] Backend API error: ${response.status} ${response.statusText}`,
423
+ errorJson.error || errorText
424
+ );
425
+ } else if (!this.isProduction) {
426
+ console.log(`\u2705 [Observa] Trace sent successfully`);
427
+ console.log(` Trace ID: ${trace.traceId}`);
428
+ }
429
+ } catch (error) {
430
+ console.error("[Observa] Failed to send trace:", error);
431
+ }
432
+ }
433
+ };
434
+ var init = (config) => new Observa(config);
435
+ export {
436
+ Observa,
437
+ init
438
+ };
package/dist/index.mjs ADDED
@@ -0,0 +1,63 @@
1
+ // src/index.ts
2
+ var Observa = class {
3
+ apiKey;
4
+ baseUrl;
5
+ constructor(config) {
6
+ this.apiKey = config.apiKey;
7
+ this.baseUrl = config.baseUrl || "https://api.observa.ai";
8
+ console.log("\u{1F4A7} Observa SDK Initialized");
9
+ }
10
+ async track(event, action) {
11
+ const startTime = Date.now();
12
+ const traceId = crypto.randomUUID();
13
+ const originalResponse = await action();
14
+ if (!originalResponse.body) return originalResponse;
15
+ const [stream1, stream2] = originalResponse.body.tee();
16
+ this.captureStream(stream2, event, traceId, startTime);
17
+ return new Response(stream1, {
18
+ headers: originalResponse.headers,
19
+ status: originalResponse.status,
20
+ statusText: originalResponse.statusText
21
+ });
22
+ }
23
+ async captureStream(stream, event, traceId, startTime) {
24
+ try {
25
+ const reader = stream.getReader();
26
+ const decoder = new TextDecoder();
27
+ let fullResponse = "";
28
+ while (true) {
29
+ const { done, value } = await reader.read();
30
+ if (done) break;
31
+ fullResponse += decoder.decode(value, { stream: true });
32
+ }
33
+ const latency = Date.now() - startTime;
34
+ await this.sendTrace({
35
+ traceId,
36
+ ...event,
37
+ response: fullResponse,
38
+ latency
39
+ });
40
+ } catch (err) {
41
+ console.error("[Raindrop] Error capturing stream:", err);
42
+ }
43
+ }
44
+ async sendTrace(payload) {
45
+ try {
46
+ await fetch(`${this.baseUrl}/v1/traces`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Authorization: `Bearer ${this.apiKey}`
51
+ },
52
+ body: JSON.stringify(payload)
53
+ });
54
+ } catch (e) {
55
+ console.error("[Raindrop] Failed to send trace");
56
+ }
57
+ }
58
+ };
59
+ var init = (config) => new Observa(config);
60
+ export {
61
+ Observa,
62
+ init
63
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "observa-sdk",
3
+ "version": "0.0.1",
4
+ "description": "Enterprise-grade observability SDK for AI applications. Track and monitor LLM interactions with zero friction.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "observability",
26
+ "llm",
27
+ "ai",
28
+ "observa",
29
+ "monitoring",
30
+ "tracing",
31
+ "analytics"
32
+ ],
33
+ "author": "Nicka",
34
+ "license": "MIT"
35
+ }