@fluxgate/sdk 0.0.2-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +341 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +72 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +257 -0
- package/dist/types/types.d.ts +63 -0
- package/dist/types/types.js +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 [Your Name or Organization]
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# @fluxgate/sdk
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Core tracking functionality for FluxGate. This package provides the base `FluxGate` class that sends usage data to the FluxGate API.
|
|
6
|
+
|
|
7
|
+
## 📦 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @fluxgate/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 🚀 Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { FluxGate } from "@fluxgate/sdk";
|
|
17
|
+
|
|
18
|
+
const fluxgate = new FluxGate({
|
|
19
|
+
apiKey: process.env.FLUXGATE_API_KEY,
|
|
20
|
+
endpoint: "https://fluxgate.app/api/events", // optional
|
|
21
|
+
timeout: 5000, // optional, default: 5000ms
|
|
22
|
+
debug: false, // optional, default: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Record a usage event
|
|
26
|
+
const response = await fluxgate.recordEvent({
|
|
27
|
+
usage: {
|
|
28
|
+
inputTokens: 100,
|
|
29
|
+
outputTokens: 50,
|
|
30
|
+
cachedTokens: 20,
|
|
31
|
+
model: "gpt-4",
|
|
32
|
+
provider: "openai",
|
|
33
|
+
latencyInMs: 1500,
|
|
34
|
+
isStreamed: false,
|
|
35
|
+
},
|
|
36
|
+
status: "SUCCESS",
|
|
37
|
+
metadata: {
|
|
38
|
+
feature: "chatbot",
|
|
39
|
+
user: "user-123",
|
|
40
|
+
sessionId: "session-456",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(response);
|
|
45
|
+
// { id: "event-123", createdAt: "2026-05-05T...", cost: 0.001 }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 📖 API Reference
|
|
49
|
+
|
|
50
|
+
### `FluxGate`
|
|
51
|
+
|
|
52
|
+
The main class for tracking LLM usage events.
|
|
53
|
+
|
|
54
|
+
#### Constructor
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
new FluxGate(config: FluxGateConfig)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Configuration Options:**
|
|
61
|
+
|
|
62
|
+
| Option | Type | Required | Default | Description |
|
|
63
|
+
| ---------- | --------- | -------- | --------------------------------- | ------------------------------- |
|
|
64
|
+
| `apiKey` | `string` | ✅ | - | Your FluxGate API key |
|
|
65
|
+
| `endpoint` | `string` | ❌ | `https://fluxgate.app/api/events` | API endpoint URL |
|
|
66
|
+
| `timeout` | `number` | ❌ | `5000` | Request timeout in milliseconds |
|
|
67
|
+
| `debug` | `boolean` | ❌ | `false` | Enable debug logging |
|
|
68
|
+
|
|
69
|
+
#### Methods
|
|
70
|
+
|
|
71
|
+
##### `recordEvent(event: LLMEvent)`
|
|
72
|
+
|
|
73
|
+
Records a usage event to FluxGate.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface LLMEvent {
|
|
79
|
+
usage: AiEventUsage;
|
|
80
|
+
status?: AiEventStatus | { status: AiEventStatus; errorMessage?: string };
|
|
81
|
+
metadata?: AiEventMetadata;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Usage Object:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface AiEventUsage {
|
|
89
|
+
inputTokens: number;
|
|
90
|
+
outputTokens: number;
|
|
91
|
+
cachedTokens?: number;
|
|
92
|
+
model?: string;
|
|
93
|
+
provider?: string;
|
|
94
|
+
latencyInMs?: number;
|
|
95
|
+
isStreamed?: boolean;
|
|
96
|
+
streamingDurationInMs?: number;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Status Types:**
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
type AiEventStatus =
|
|
104
|
+
| "SUCCESS"
|
|
105
|
+
| "ERROR"
|
|
106
|
+
| "BLOCKED"
|
|
107
|
+
| "MAX_TOKENS"
|
|
108
|
+
| "CONTENT_FILTER"
|
|
109
|
+
| "RECITATION"
|
|
110
|
+
| "MALFORMED_REQUEST";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Metadata Object:**
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
interface AiEventMetadata {
|
|
117
|
+
feature?: string;
|
|
118
|
+
step?: string;
|
|
119
|
+
user?: string | TrackedUser;
|
|
120
|
+
sessionId?: string;
|
|
121
|
+
conversationId?: string;
|
|
122
|
+
[key: string]: unknown; // Custom fields allowed
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface TrackedUser {
|
|
126
|
+
id: string;
|
|
127
|
+
name?: string;
|
|
128
|
+
email?: string;
|
|
129
|
+
image?: string;
|
|
130
|
+
monthlyRevenue?: number;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Returns:**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
Promise<CreateAiEventResponse | null>;
|
|
138
|
+
|
|
139
|
+
interface CreateAiEventResponse {
|
|
140
|
+
id: string;
|
|
141
|
+
createdAt: string;
|
|
142
|
+
cost: number | null;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 💡 Usage Examples
|
|
147
|
+
|
|
148
|
+
### Basic Usage
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const fluxgate = new FluxGate({
|
|
152
|
+
apiKey: "your-api-key",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await fluxgate.recordEvent({
|
|
156
|
+
usage: {
|
|
157
|
+
inputTokens: 100,
|
|
158
|
+
outputTokens: 50,
|
|
159
|
+
},
|
|
160
|
+
status: "SUCCESS",
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### With Full Metadata
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await fluxgate.recordEvent({
|
|
168
|
+
usage: {
|
|
169
|
+
inputTokens: 200,
|
|
170
|
+
outputTokens: 150,
|
|
171
|
+
cachedTokens: 50,
|
|
172
|
+
model: "gpt-4-turbo",
|
|
173
|
+
provider: "openai",
|
|
174
|
+
latencyInMs: 2500,
|
|
175
|
+
isStreamed: true,
|
|
176
|
+
streamingDurationInMs: 3000,
|
|
177
|
+
},
|
|
178
|
+
status: "SUCCESS",
|
|
179
|
+
metadata: {
|
|
180
|
+
feature: "code-generation",
|
|
181
|
+
step: "implementation",
|
|
182
|
+
user: {
|
|
183
|
+
id: "user-123",
|
|
184
|
+
name: "John Doe",
|
|
185
|
+
email: "john@example.com",
|
|
186
|
+
monthlyRevenue: 99.99,
|
|
187
|
+
},
|
|
188
|
+
sessionId: "session-abc",
|
|
189
|
+
conversationId: "conv-xyz",
|
|
190
|
+
customField: "any custom data",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Error Tracking
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await fluxgate.recordEvent({
|
|
199
|
+
usage: {
|
|
200
|
+
inputTokens: 100,
|
|
201
|
+
outputTokens: 0,
|
|
202
|
+
},
|
|
203
|
+
status: {
|
|
204
|
+
status: "ERROR",
|
|
205
|
+
errorMessage: "API rate limit exceeded",
|
|
206
|
+
},
|
|
207
|
+
metadata: {
|
|
208
|
+
feature: "chatbot",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### With Debug Mode
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const fluxgate = new FluxGate({
|
|
217
|
+
apiKey: "your-api-key",
|
|
218
|
+
debug: true, // Logs all events to console
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await fluxgate.recordEvent({
|
|
222
|
+
usage: {
|
|
223
|
+
inputTokens: 100,
|
|
224
|
+
outputTokens: 50,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
// [fluxgate] FluxGate initialized { endpoint: '...', timeout: 5000 }
|
|
228
|
+
// [fluxgate] Sending event to ...: { ... }
|
|
229
|
+
// [fluxgate] Event sent successfully. Status: 200. Response: { "id": "evt_...", ... }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Custom Endpoint
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const fluxgate = new FluxGate({
|
|
236
|
+
apiKey: "your-api-key",
|
|
237
|
+
endpoint: "https://your-custom-domain.com/api/track",
|
|
238
|
+
timeout: 10000,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 🔧 Advanced Usage
|
|
243
|
+
|
|
244
|
+
### Using with SDK Wrappers
|
|
245
|
+
|
|
246
|
+
This package is typically used through provider-specific wrappers:
|
|
247
|
+
|
|
248
|
+
- [`@fluxgate/openai`](../openai/README.md) - For OpenAI
|
|
249
|
+
- [`@fluxgate/gemini`](../gemini/README.md) - For Google Gemini
|
|
250
|
+
- [`@fluxgate/anthropic`](../anthropic/README.md) - For Anthropic
|
|
251
|
+
|
|
252
|
+
### Direct Integration
|
|
253
|
+
|
|
254
|
+
If you're integrating a custom provider:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { FluxGate, type LLMEvent } from "@fluxgate/sdk";
|
|
258
|
+
|
|
259
|
+
const fluxgate = new FluxGate({ apiKey: "your-api-key" });
|
|
260
|
+
|
|
261
|
+
async function trackMyCustomLLM(prompt: string) {
|
|
262
|
+
const start = performance.now();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const response = await myCustomLLM.generate(prompt);
|
|
266
|
+
|
|
267
|
+
await fluxgate.recordEvent({
|
|
268
|
+
usage: {
|
|
269
|
+
inputTokens: response.inputTokens,
|
|
270
|
+
outputTokens: response.outputTokens,
|
|
271
|
+
model: "custom-model",
|
|
272
|
+
provider: "custom-provider",
|
|
273
|
+
latencyInMs: performance.now() - start,
|
|
274
|
+
},
|
|
275
|
+
status: "SUCCESS",
|
|
276
|
+
metadata: {
|
|
277
|
+
feature: "my-feature",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return response;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
await fluxgate.recordEvent({
|
|
284
|
+
usage: {
|
|
285
|
+
inputTokens: 0,
|
|
286
|
+
outputTokens: 0,
|
|
287
|
+
},
|
|
288
|
+
status: {
|
|
289
|
+
status: "ERROR",
|
|
290
|
+
errorMessage: error.message,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## 🛡️ Error Handling
|
|
299
|
+
|
|
300
|
+
The tracker is designed to never break your application:
|
|
301
|
+
|
|
302
|
+
- Network errors are caught and logged (in debug mode)
|
|
303
|
+
- Timeouts are handled gracefully
|
|
304
|
+
- Returns `null` if tracking fails
|
|
305
|
+
- Your main LLM calls continue regardless of tracking status
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const result = await tracker.recordEvent(event);
|
|
309
|
+
if (result === null) {
|
|
310
|
+
// Tracking failed, but your app continues
|
|
311
|
+
console.log("Failed to track event");
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## 📊 Type Exports
|
|
316
|
+
|
|
317
|
+
All types are exported for use in your application:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import type {
|
|
321
|
+
LLMEvent,
|
|
322
|
+
CreateAiEventResponse,
|
|
323
|
+
TrackedUser,
|
|
324
|
+
AiEventMetadata,
|
|
325
|
+
FluxGateCostTrackingResponse,
|
|
326
|
+
WithTracking,
|
|
327
|
+
AiEventStatus,
|
|
328
|
+
AiEventUsage,
|
|
329
|
+
ExtractedUsage,
|
|
330
|
+
FluxGateConfig,
|
|
331
|
+
} from "@fluxgate/sdk";
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## 🔗 Related Packages
|
|
335
|
+
|
|
336
|
+
- [@fluxgate/openai](../openai) - OpenAI SDK wrapper
|
|
337
|
+
- [@fluxgate/gemini](../gemini) - Gemini SDK wrapper
|
|
338
|
+
|
|
339
|
+
## 📝 License
|
|
340
|
+
|
|
341
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CreateAiEventResponse, LLMEvent, FluxGateConfig } from "./types/types.js";
|
|
2
|
+
export declare class FluxGate {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private endpoint;
|
|
5
|
+
private timeout;
|
|
6
|
+
private debug;
|
|
7
|
+
constructor(config: FluxGateConfig);
|
|
8
|
+
recordEvent(event: LLMEvent): Promise<CreateAiEventResponse | null>;
|
|
9
|
+
}
|
|
10
|
+
export type { LLMEvent, CreateAiEventResponse, TrackedUser, AiEventMetadata, FluxGateCostTrackingResponse, WithTracking, AiEventStatus, AiEventUsage, ExtractedUsage, FluxGateConfig, } from "./types/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class FluxGate {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
if (!config.apiKey) {
|
|
4
|
+
throw new Error("FluxGate requires an apiKey in config");
|
|
5
|
+
}
|
|
6
|
+
this.apiKey = config.apiKey;
|
|
7
|
+
this.endpoint = config.endpoint || "https://fluxgate.app/api/events";
|
|
8
|
+
this.timeout = config.timeout || 5000;
|
|
9
|
+
this.debug = config.debug || false;
|
|
10
|
+
if (this.debug) {
|
|
11
|
+
console.log("[fluxgate] FluxGate initialized", {
|
|
12
|
+
endpoint: this.endpoint,
|
|
13
|
+
timeout: this.timeout,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async recordEvent(event) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
if (this.debug) {
|
|
20
|
+
console.log(`[fluxgate] Sending event to ${this.endpoint}:`, JSON.stringify(event, null, 2));
|
|
21
|
+
}
|
|
22
|
+
if (!event.status) {
|
|
23
|
+
event.status = "SUCCESS";
|
|
24
|
+
}
|
|
25
|
+
const fetchPromise = fetch(this.endpoint, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
30
|
+
"User-Agent": "@fluxgate/sdk/0.0.2-dev.0",
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(event),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
controller.abort();
|
|
38
|
+
reject(new Error(`Request timeout after ${this.timeout}ms`));
|
|
39
|
+
}, this.timeout);
|
|
40
|
+
});
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (this.debug) {
|
|
47
|
+
console.error("[fluxgate] Network error sending event:", err);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
cost: 0,
|
|
51
|
+
createdAt: new Date().toISOString(),
|
|
52
|
+
id: "error-" + Math.random().toString(36).substring(2, 15),
|
|
53
|
+
status: "ERROR",
|
|
54
|
+
};
|
|
55
|
+
// throw err;
|
|
56
|
+
}
|
|
57
|
+
let trackingData = null;
|
|
58
|
+
try {
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
trackingData = JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (this.debug) {
|
|
64
|
+
console.error("[fluxgate] Failed to parse response:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (this.debug) {
|
|
68
|
+
console.log(`[fluxgate] Event sent successfully. Status: ${response.status}. Response: ${JSON.stringify(trackingData)}`);
|
|
69
|
+
}
|
|
70
|
+
return trackingData;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import { FluxGate } from "./index.js";
|
|
3
|
+
describe("FluxGate", () => {
|
|
4
|
+
let instance;
|
|
5
|
+
const mockApiKey = "test-api-key";
|
|
6
|
+
const mockEndpoint = "https://test.example.com/api/events";
|
|
7
|
+
// Mock fetch globally
|
|
8
|
+
const originalFetch = global.fetch;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
instance = new FluxGate({
|
|
11
|
+
apiKey: mockApiKey,
|
|
12
|
+
endpoint: mockEndpoint,
|
|
13
|
+
timeout: 5000,
|
|
14
|
+
debug: false,
|
|
15
|
+
});
|
|
16
|
+
// Mock fetch
|
|
17
|
+
global.fetch = vi.fn();
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
global.fetch = originalFetch;
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
describe("constructor", () => {
|
|
24
|
+
it("should throw error if apiKey is not provided", () => {
|
|
25
|
+
expect(() => new FluxGate({
|
|
26
|
+
apiKey: "",
|
|
27
|
+
})).toThrow("FluxGate requires an apiKey in config");
|
|
28
|
+
});
|
|
29
|
+
it("should use default endpoint if not provided", () => {
|
|
30
|
+
const defaultTracker = new FluxGate({ apiKey: "test" });
|
|
31
|
+
expect(defaultTracker).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
it("should use custom endpoint if provided", () => {
|
|
34
|
+
const customTracker = new FluxGate({
|
|
35
|
+
apiKey: "test",
|
|
36
|
+
endpoint: "https://custom.example.com",
|
|
37
|
+
});
|
|
38
|
+
expect(customTracker).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
it("should use default timeout if not provided", () => {
|
|
41
|
+
const defaultTracker = new FluxGate({ apiKey: "test" });
|
|
42
|
+
expect(defaultTracker).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("recordEvent", () => {
|
|
46
|
+
it("should send event to the correct endpoint", async () => {
|
|
47
|
+
const mockResponse = {
|
|
48
|
+
id: "event-123",
|
|
49
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
50
|
+
cost: 0.001,
|
|
51
|
+
};
|
|
52
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
53
|
+
status: 200,
|
|
54
|
+
statusText: "OK",
|
|
55
|
+
text: async () => JSON.stringify(mockResponse),
|
|
56
|
+
});
|
|
57
|
+
const event = {
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: 100,
|
|
60
|
+
outputTokens: 50,
|
|
61
|
+
model: "gpt-4",
|
|
62
|
+
provider: "openai",
|
|
63
|
+
latencyInMs: 1000,
|
|
64
|
+
},
|
|
65
|
+
status: "SUCCESS",
|
|
66
|
+
metadata: {
|
|
67
|
+
feature: "chat",
|
|
68
|
+
user: "user-123",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const result = await instance.recordEvent(event);
|
|
72
|
+
expect(fetch).toHaveBeenCalledWith(mockEndpoint, expect.objectContaining({
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: expect.objectContaining({
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
Authorization: `Bearer ${mockApiKey}`,
|
|
77
|
+
"User-Agent": "@fluxgate/sdk/0.0.2-dev.0",
|
|
78
|
+
}),
|
|
79
|
+
body: JSON.stringify(event),
|
|
80
|
+
}));
|
|
81
|
+
expect(result).toEqual(mockResponse);
|
|
82
|
+
});
|
|
83
|
+
it("should default status to SUCCESS if not provided", async () => {
|
|
84
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
85
|
+
status: 200,
|
|
86
|
+
statusText: "OK",
|
|
87
|
+
text: async () => JSON.stringify({
|
|
88
|
+
id: "123",
|
|
89
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
90
|
+
cost: 0.001,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
const event = {
|
|
94
|
+
usage: {
|
|
95
|
+
inputTokens: 100,
|
|
96
|
+
outputTokens: 50,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
await instance.recordEvent(event);
|
|
100
|
+
const callArgs = vi.mocked(fetch).mock.calls[0];
|
|
101
|
+
const body = JSON.parse(callArgs[1]?.body);
|
|
102
|
+
expect(body.status).toBe("SUCCESS");
|
|
103
|
+
});
|
|
104
|
+
it("should handle fetch errors gracefully", async () => {
|
|
105
|
+
const mockResponse = {
|
|
106
|
+
id: "event-123",
|
|
107
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
108
|
+
cost: 0.001,
|
|
109
|
+
};
|
|
110
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
111
|
+
status: 500,
|
|
112
|
+
statusText: "Internal Server Error",
|
|
113
|
+
text: async () => JSON.stringify(mockResponse),
|
|
114
|
+
});
|
|
115
|
+
const event = {
|
|
116
|
+
usage: {
|
|
117
|
+
inputTokens: 100,
|
|
118
|
+
outputTokens: 50,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const result = await instance.recordEvent(event);
|
|
122
|
+
expect(result).toEqual(mockResponse);
|
|
123
|
+
});
|
|
124
|
+
it("should handle invalid JSON response", async () => {
|
|
125
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
126
|
+
status: 200,
|
|
127
|
+
statusText: "OK",
|
|
128
|
+
text: async () => "invalid json",
|
|
129
|
+
});
|
|
130
|
+
const event = {
|
|
131
|
+
usage: {
|
|
132
|
+
inputTokens: 100,
|
|
133
|
+
outputTokens: 50,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const result = await instance.recordEvent(event);
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
it("should respect timeout setting", async () => {
|
|
140
|
+
const shortTimeoutTracker = new FluxGate({
|
|
141
|
+
apiKey: mockApiKey,
|
|
142
|
+
endpoint: mockEndpoint,
|
|
143
|
+
timeout: 100,
|
|
144
|
+
});
|
|
145
|
+
vi.mocked(fetch).mockImplementation(() => new Promise((resolve) => {
|
|
146
|
+
setTimeout(() => resolve({
|
|
147
|
+
status: 200,
|
|
148
|
+
statusText: "OK",
|
|
149
|
+
text: async () => JSON.stringify({}),
|
|
150
|
+
}), 1000);
|
|
151
|
+
}));
|
|
152
|
+
const event = {
|
|
153
|
+
usage: {
|
|
154
|
+
inputTokens: 100,
|
|
155
|
+
outputTokens: 50,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
await expect(shortTimeoutTracker.recordEvent(event)).resolves.toHaveProperty("status", "ERROR");
|
|
159
|
+
});
|
|
160
|
+
it("should include complex metadata in the event", async () => {
|
|
161
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
162
|
+
status: 200,
|
|
163
|
+
statusText: "OK",
|
|
164
|
+
text: async () => JSON.stringify({
|
|
165
|
+
id: "123",
|
|
166
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
167
|
+
cost: 0.001,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
const event = {
|
|
171
|
+
usage: {
|
|
172
|
+
inputTokens: 100,
|
|
173
|
+
outputTokens: 50,
|
|
174
|
+
cachedTokens: 20,
|
|
175
|
+
model: "gpt-4",
|
|
176
|
+
provider: "openai",
|
|
177
|
+
latencyInMs: 1500,
|
|
178
|
+
isStreamed: true,
|
|
179
|
+
streamingDurationInMs: 2000,
|
|
180
|
+
},
|
|
181
|
+
status: "SUCCESS",
|
|
182
|
+
metadata: {
|
|
183
|
+
feature: "chat",
|
|
184
|
+
step: "generation",
|
|
185
|
+
user: {
|
|
186
|
+
id: "user-123",
|
|
187
|
+
name: "Test User",
|
|
188
|
+
email: "test@example.com",
|
|
189
|
+
monthlyRevenue: 99.99,
|
|
190
|
+
},
|
|
191
|
+
sessionId: "session-456",
|
|
192
|
+
conversationId: "conv-789",
|
|
193
|
+
customField: "custom value",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
await instance.recordEvent(event);
|
|
197
|
+
const callArgs = vi.mocked(fetch).mock.calls[0];
|
|
198
|
+
const body = JSON.parse(callArgs[1]?.body);
|
|
199
|
+
expect(body.usage).toEqual(event.usage);
|
|
200
|
+
expect(body.metadata).toEqual(event.metadata);
|
|
201
|
+
});
|
|
202
|
+
it("should handle status with error message", async () => {
|
|
203
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
204
|
+
status: 200,
|
|
205
|
+
statusText: "OK",
|
|
206
|
+
text: async () => JSON.stringify({
|
|
207
|
+
id: "123",
|
|
208
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
209
|
+
cost: null,
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
const event = {
|
|
213
|
+
usage: {
|
|
214
|
+
inputTokens: 100,
|
|
215
|
+
outputTokens: 0,
|
|
216
|
+
},
|
|
217
|
+
status: {
|
|
218
|
+
status: "ERROR",
|
|
219
|
+
errorMessage: "API call failed",
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
await instance.recordEvent(event);
|
|
223
|
+
const callArgs = vi.mocked(fetch).mock.calls[0];
|
|
224
|
+
const body = JSON.parse(callArgs[1]?.body);
|
|
225
|
+
expect(body.status).toEqual(event.status);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe("debug mode", () => {
|
|
229
|
+
it("should log debug information when debug is enabled", async () => {
|
|
230
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
231
|
+
const debugTracker = new FluxGate({
|
|
232
|
+
apiKey: mockApiKey,
|
|
233
|
+
endpoint: mockEndpoint,
|
|
234
|
+
debug: true,
|
|
235
|
+
});
|
|
236
|
+
expect(consoleSpy).toHaveBeenCalledWith("[fluxgate] FluxGate initialized", expect.any(Object));
|
|
237
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
238
|
+
status: 200,
|
|
239
|
+
statusText: "OK",
|
|
240
|
+
text: async () => JSON.stringify({
|
|
241
|
+
id: "123",
|
|
242
|
+
createdAt: "2026-05-05T00:00:00Z",
|
|
243
|
+
cost: 0.001,
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
const event = {
|
|
247
|
+
usage: {
|
|
248
|
+
inputTokens: 100,
|
|
249
|
+
outputTokens: 50,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
await debugTracker.recordEvent(event);
|
|
253
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("[fluxgate] Sending event"), expect.any(String));
|
|
254
|
+
consoleSpy.mockRestore();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type AiEventStatus = "SUCCESS" | "ERROR" | "BLOCKED" | "MAX_TOKENS" | "CONTENT_FILTER" | "RECITATION" | "MALFORMED_REQUEST";
|
|
2
|
+
export type TrackedUser = {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
image?: string;
|
|
7
|
+
monthlyRevenue?: number;
|
|
8
|
+
};
|
|
9
|
+
export type AiEventUsage = {
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
cachedTokens?: number;
|
|
13
|
+
model?: string;
|
|
14
|
+
provider?: string;
|
|
15
|
+
latencyInMs?: number;
|
|
16
|
+
isStreamed?: boolean;
|
|
17
|
+
streamingDurationInMs?: number;
|
|
18
|
+
};
|
|
19
|
+
export type AiEventMetadata = {
|
|
20
|
+
feature?: string;
|
|
21
|
+
step?: string;
|
|
22
|
+
user?: string | TrackedUser;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
conversationId?: string;
|
|
25
|
+
timestamp?: Date;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
export type LLMEvent = {
|
|
29
|
+
usage: AiEventUsage;
|
|
30
|
+
status?: AiEventStatus | {
|
|
31
|
+
status: AiEventStatus;
|
|
32
|
+
errorMessage?: string;
|
|
33
|
+
};
|
|
34
|
+
metadata?: AiEventMetadata;
|
|
35
|
+
};
|
|
36
|
+
export type CreateAiEventResponse = {
|
|
37
|
+
id: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
cost: number | null;
|
|
40
|
+
status: "SUCCESS" | "ERROR";
|
|
41
|
+
};
|
|
42
|
+
export interface FluxGateConfig {
|
|
43
|
+
apiKey: string;
|
|
44
|
+
endpoint?: string;
|
|
45
|
+
debug?: boolean;
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}
|
|
48
|
+
export type FluxGateCostTrackingResponse = {
|
|
49
|
+
status: AiEventStatus;
|
|
50
|
+
cost: number | null;
|
|
51
|
+
trackingId: string | null;
|
|
52
|
+
createdAt: string | null;
|
|
53
|
+
errorMessage?: string;
|
|
54
|
+
};
|
|
55
|
+
export type WithTracking<T> = T & {
|
|
56
|
+
fluxGateCostTrackingResponse: FluxGateCostTrackingResponse;
|
|
57
|
+
};
|
|
58
|
+
export type ExtractedUsage = {
|
|
59
|
+
inputTokens: number;
|
|
60
|
+
outputTokens: number;
|
|
61
|
+
cachedTokens: number;
|
|
62
|
+
totalTokens: number;
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxgate/sdk",
|
|
3
|
+
"version": "0.0.2-dev.0",
|
|
4
|
+
"description": "Core tracking SDK for FluxGate token usage, latency, and cost monitoring.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./types": {
|
|
13
|
+
"types": "./dist/types/types.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run --root ../../",
|
|
26
|
+
"test:watch": "vitest --root ../../"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"llm",
|
|
30
|
+
"tokens",
|
|
31
|
+
"tracker",
|
|
32
|
+
"openai",
|
|
33
|
+
"claude",
|
|
34
|
+
"gemini",
|
|
35
|
+
"monitoring",
|
|
36
|
+
"costs"
|
|
37
|
+
],
|
|
38
|
+
"author": "FluxGate Team",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/yehova73/fluxgate-npm.git",
|
|
43
|
+
"directory": "packages/sdk"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/yehova73/fluxgate-npm/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://fluxgate.app",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^20.11.0",
|
|
54
|
+
"typescript": "^5.3.3"
|
|
55
|
+
},
|
|
56
|
+
"type": "module",
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|