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 +251 -0
- package/dist/index.cjs +464 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +438 -0
- package/dist/index.mjs +63 -0
- package/package.json +35 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|