bloop-sdk 0.2.0 → 0.3.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/dist/bloop.d.ts +132 -0
- package/dist/bloop.js +434 -0
- package/dist/test-auto-instrument.d.ts +14 -0
- package/dist/test-auto-instrument.js +79 -0
- package/dist/test-tracing.d.ts +6 -0
- package/dist/test-tracing.js +188 -0
- package/package.json +3 -3
package/dist/bloop.d.ts
CHANGED
|
@@ -20,21 +20,153 @@ export interface ErrorEvent {
|
|
|
20
20
|
userIdHash?: string;
|
|
21
21
|
metadata?: Record<string, unknown>;
|
|
22
22
|
}
|
|
23
|
+
export type SpanType = "generation" | "tool" | "retrieval" | "custom";
|
|
24
|
+
export type SpanStatus = "ok" | "error";
|
|
25
|
+
export type TraceStatus = "running" | "completed" | "error";
|
|
26
|
+
export interface TraceOptions {
|
|
27
|
+
name: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
userId?: string;
|
|
30
|
+
input?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
promptName?: string;
|
|
33
|
+
promptVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SpanOptions {
|
|
36
|
+
spanType: SpanType;
|
|
37
|
+
name?: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
provider?: string;
|
|
40
|
+
input?: string;
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface SpanEndOptions {
|
|
44
|
+
inputTokens?: number;
|
|
45
|
+
outputTokens?: number;
|
|
46
|
+
cost?: number;
|
|
47
|
+
status: SpanStatus;
|
|
48
|
+
errorMessage?: string;
|
|
49
|
+
output?: string;
|
|
50
|
+
timeToFirstTokenMs?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface TraceEndOptions {
|
|
53
|
+
status: TraceStatus;
|
|
54
|
+
output?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Options for the traceGeneration convenience method. */
|
|
57
|
+
export interface TraceGenerationOptions {
|
|
58
|
+
name: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
provider?: string;
|
|
61
|
+
input?: string;
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
userId?: string;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
export declare class Span {
|
|
67
|
+
id: string;
|
|
68
|
+
parentSpanId: string | null;
|
|
69
|
+
spanType: SpanType;
|
|
70
|
+
name: string;
|
|
71
|
+
model?: string;
|
|
72
|
+
provider?: string;
|
|
73
|
+
startedAt: number;
|
|
74
|
+
input?: string;
|
|
75
|
+
metadata?: Record<string, unknown>;
|
|
76
|
+
inputTokens?: number;
|
|
77
|
+
outputTokens?: number;
|
|
78
|
+
cost?: number;
|
|
79
|
+
latencyMs?: number;
|
|
80
|
+
timeToFirstTokenMs?: number;
|
|
81
|
+
status?: SpanStatus;
|
|
82
|
+
errorMessage?: string;
|
|
83
|
+
output?: string;
|
|
84
|
+
constructor(opts: SpanOptions & {
|
|
85
|
+
parentSpanId?: string | null;
|
|
86
|
+
});
|
|
87
|
+
/** End this span and record metrics. */
|
|
88
|
+
end(opts: SpanEndOptions): void;
|
|
89
|
+
/** Serialize to server-expected format with snake_case keys. */
|
|
90
|
+
toJSON(): object;
|
|
91
|
+
}
|
|
92
|
+
export declare class Trace {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
sessionId?: string;
|
|
96
|
+
userId?: string;
|
|
97
|
+
status: TraceStatus;
|
|
98
|
+
input?: string;
|
|
99
|
+
output?: string;
|
|
100
|
+
metadata?: Record<string, unknown>;
|
|
101
|
+
promptName?: string;
|
|
102
|
+
promptVersion?: string;
|
|
103
|
+
startedAt: number;
|
|
104
|
+
endedAt?: number;
|
|
105
|
+
spans: Span[];
|
|
106
|
+
private _client;
|
|
107
|
+
constructor(opts: TraceOptions, client: BloopClient);
|
|
108
|
+
/** Create a new span within this trace. */
|
|
109
|
+
startSpan(opts: SpanOptions): Span;
|
|
110
|
+
/** End this trace and push it to the client's trace buffer. */
|
|
111
|
+
end(opts: TraceEndOptions): void;
|
|
112
|
+
/** Serialize to server-expected format. */
|
|
113
|
+
private _toPayload;
|
|
114
|
+
}
|
|
115
|
+
export declare const MODEL_COSTS: Record<string, {
|
|
116
|
+
input: number;
|
|
117
|
+
output: number;
|
|
118
|
+
}>;
|
|
23
119
|
export declare class BloopClient {
|
|
24
120
|
private config;
|
|
25
121
|
private buffer;
|
|
122
|
+
private traceBuffer;
|
|
26
123
|
private timer;
|
|
27
124
|
private signingKey;
|
|
28
125
|
private keyReady;
|
|
126
|
+
private globalHandlersInstalled;
|
|
127
|
+
private _modelCosts;
|
|
29
128
|
constructor(config: BloopConfig);
|
|
30
129
|
/** Import the HMAC key using Web Crypto API. */
|
|
31
130
|
private importKey;
|
|
131
|
+
/**
|
|
132
|
+
* Install global error handlers for uncaught exceptions and unhandled rejections.
|
|
133
|
+
* Detects runtime (Node.js vs browser) and installs appropriate handlers.
|
|
134
|
+
* Uses addEventListener (not assignment) to avoid clobbering existing handlers.
|
|
135
|
+
*/
|
|
136
|
+
installGlobalHandlers(): void;
|
|
32
137
|
/** Capture an Error object. */
|
|
33
138
|
captureError(error: Error, context?: Partial<ErrorEvent>): void;
|
|
34
139
|
/** Capture a structured error event. */
|
|
35
140
|
capture(event: ErrorEvent): void;
|
|
141
|
+
/** Start a new trace. Returns a Trace object for adding spans. */
|
|
142
|
+
startTrace(opts: TraceOptions): Trace;
|
|
143
|
+
/**
|
|
144
|
+
* Convenience method: creates a trace with a single generation span,
|
|
145
|
+
* calls the provided function, and ends both span and trace.
|
|
146
|
+
* Returns the value returned by the callback.
|
|
147
|
+
*/
|
|
148
|
+
traceGeneration<T>(opts: TraceGenerationOptions, fn: (span: Span) => Promise<T>): Promise<T>;
|
|
149
|
+
/** Set custom cost rates for a model (per-token in dollars). */
|
|
150
|
+
setModelCosts(model: string, costs: {
|
|
151
|
+
input: number;
|
|
152
|
+
output: number;
|
|
153
|
+
}): void;
|
|
154
|
+
/** Wrap an OpenAI-compatible client instance to automatically trace all LLM calls. */
|
|
155
|
+
wrapOpenAI<T extends object>(client: T): T;
|
|
156
|
+
/** Wrap an Anthropic client instance to automatically trace all messages.create calls. */
|
|
157
|
+
wrapAnthropic<T extends object>(client: T): T;
|
|
158
|
+
/** Internal: trace an OpenAI-style API call. */
|
|
159
|
+
private _traceOpenAICall;
|
|
160
|
+
/** Internal: trace an Anthropic messages.create call. */
|
|
161
|
+
private _traceAnthropicCall;
|
|
162
|
+
/** Push a completed trace payload to the buffer. Called by Trace.end(). */
|
|
163
|
+
private _pushTrace;
|
|
36
164
|
/** Flush buffered events to the server. */
|
|
37
165
|
flush(): Promise<void>;
|
|
166
|
+
/** Flush error events to the ingest endpoint. */
|
|
167
|
+
private _flushErrors;
|
|
168
|
+
/** Flush trace payloads to the traces endpoint. */
|
|
169
|
+
private _flushTraces;
|
|
38
170
|
/** Express/Koa error middleware. */
|
|
39
171
|
errorMiddleware(): (err: Error, req: any, _res: any, next: any) => void;
|
|
40
172
|
/** Stop the flush timer. Call on process shutdown. */
|
package/dist/bloop.js
CHANGED
|
@@ -1,8 +1,146 @@
|
|
|
1
|
+
// ---- Span Class ----
|
|
2
|
+
export class Span {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.id = crypto.randomUUID();
|
|
5
|
+
this.parentSpanId = opts.parentSpanId ?? null;
|
|
6
|
+
this.spanType = opts.spanType;
|
|
7
|
+
this.name = opts.name ?? opts.spanType;
|
|
8
|
+
this.model = opts.model;
|
|
9
|
+
this.provider = opts.provider;
|
|
10
|
+
this.startedAt = Date.now();
|
|
11
|
+
this.input = opts.input;
|
|
12
|
+
this.metadata = opts.metadata;
|
|
13
|
+
}
|
|
14
|
+
/** End this span and record metrics. */
|
|
15
|
+
end(opts) {
|
|
16
|
+
this.latencyMs = Date.now() - this.startedAt;
|
|
17
|
+
this.status = opts.status;
|
|
18
|
+
this.inputTokens = opts.inputTokens;
|
|
19
|
+
this.outputTokens = opts.outputTokens;
|
|
20
|
+
this.cost = opts.cost;
|
|
21
|
+
this.errorMessage = opts.errorMessage;
|
|
22
|
+
this.output = opts.output;
|
|
23
|
+
this.timeToFirstTokenMs = opts.timeToFirstTokenMs;
|
|
24
|
+
}
|
|
25
|
+
/** Serialize to server-expected format with snake_case keys. */
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
id: this.id,
|
|
29
|
+
parent_span_id: this.parentSpanId,
|
|
30
|
+
span_type: this.spanType,
|
|
31
|
+
name: this.name,
|
|
32
|
+
model: this.model ?? null,
|
|
33
|
+
provider: this.provider ?? null,
|
|
34
|
+
input: this.input ?? null,
|
|
35
|
+
output: this.output ?? null,
|
|
36
|
+
metadata: this.metadata ?? null,
|
|
37
|
+
started_at: this.startedAt,
|
|
38
|
+
input_tokens: this.inputTokens ?? null,
|
|
39
|
+
output_tokens: this.outputTokens ?? null,
|
|
40
|
+
cost: this.cost ?? null,
|
|
41
|
+
latency_ms: this.latencyMs ?? null,
|
|
42
|
+
time_to_first_token_ms: this.timeToFirstTokenMs ?? null,
|
|
43
|
+
status: this.status ?? null,
|
|
44
|
+
error_message: this.errorMessage ?? null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class Trace {
|
|
49
|
+
constructor(opts, client) {
|
|
50
|
+
this.status = "running";
|
|
51
|
+
this.spans = [];
|
|
52
|
+
this.id = crypto.randomUUID();
|
|
53
|
+
this.name = opts.name;
|
|
54
|
+
this.sessionId = opts.sessionId;
|
|
55
|
+
this.userId = opts.userId;
|
|
56
|
+
this.input = opts.input;
|
|
57
|
+
this.metadata = opts.metadata;
|
|
58
|
+
this.promptName = opts.promptName;
|
|
59
|
+
this.promptVersion = opts.promptVersion;
|
|
60
|
+
this.startedAt = Date.now();
|
|
61
|
+
this._client = client;
|
|
62
|
+
}
|
|
63
|
+
/** Create a new span within this trace. */
|
|
64
|
+
startSpan(opts) {
|
|
65
|
+
const span = new Span({
|
|
66
|
+
...opts,
|
|
67
|
+
parentSpanId: null,
|
|
68
|
+
});
|
|
69
|
+
this.spans.push(span);
|
|
70
|
+
return span;
|
|
71
|
+
}
|
|
72
|
+
/** End this trace and push it to the client's trace buffer. */
|
|
73
|
+
end(opts) {
|
|
74
|
+
this.endedAt = Date.now();
|
|
75
|
+
this.status = opts.status;
|
|
76
|
+
if (opts.output !== undefined) {
|
|
77
|
+
this.output = opts.output;
|
|
78
|
+
}
|
|
79
|
+
this._client["_pushTrace"](this._toPayload());
|
|
80
|
+
}
|
|
81
|
+
/** Serialize to server-expected format. */
|
|
82
|
+
_toPayload() {
|
|
83
|
+
return {
|
|
84
|
+
id: this.id,
|
|
85
|
+
session_id: this.sessionId ?? null,
|
|
86
|
+
user_id: this.userId ?? null,
|
|
87
|
+
name: this.name,
|
|
88
|
+
status: this.status,
|
|
89
|
+
input: this.input ?? null,
|
|
90
|
+
output: this.output ?? null,
|
|
91
|
+
metadata: this.metadata ?? null,
|
|
92
|
+
prompt_name: this.promptName ?? null,
|
|
93
|
+
prompt_version: this.promptVersion ?? null,
|
|
94
|
+
started_at: this.startedAt,
|
|
95
|
+
ended_at: this.endedAt ?? null,
|
|
96
|
+
spans: this.spans.map((s) => s.toJSON()),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---- Auto-Instrumentation: Provider Detection ----
|
|
101
|
+
const PROVIDER_MAP = {
|
|
102
|
+
"api.openai.com": "openai",
|
|
103
|
+
"api.anthropic.com": "anthropic",
|
|
104
|
+
"api.minimax.io": "minimax",
|
|
105
|
+
"api.minimaxi.com": "minimax",
|
|
106
|
+
"api.moonshot.ai": "kimi",
|
|
107
|
+
"generativelanguage.googleapis.com": "google",
|
|
108
|
+
};
|
|
109
|
+
function detectProvider(client) {
|
|
110
|
+
try {
|
|
111
|
+
const baseURL = client?.baseURL || client?._options?.baseURL || "";
|
|
112
|
+
const hostname = new URL(baseURL).hostname;
|
|
113
|
+
return PROVIDER_MAP[hostname] || hostname;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return "unknown";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---- Auto-Instrumentation: Model Pricing ----
|
|
120
|
+
export const MODEL_COSTS = {
|
|
121
|
+
// OpenAI (per token in dollars)
|
|
122
|
+
"gpt-4o": { input: 2.50 / 1000000, output: 10.00 / 1000000 },
|
|
123
|
+
"gpt-4o-mini": { input: 0.15 / 1000000, output: 0.60 / 1000000 },
|
|
124
|
+
"gpt-4-turbo": { input: 10.00 / 1000000, output: 30.00 / 1000000 },
|
|
125
|
+
// Anthropic
|
|
126
|
+
"claude-sonnet-4-5-20250929": { input: 3.00 / 1000000, output: 15.00 / 1000000 },
|
|
127
|
+
"claude-haiku-4-5-20251001": { input: 0.80 / 1000000, output: 4.00 / 1000000 },
|
|
128
|
+
// Minimax
|
|
129
|
+
"MiniMax-M1": { input: 0.40 / 1000000, output: 2.20 / 1000000 },
|
|
130
|
+
"MiniMax-Text-01": { input: 0.20 / 1000000, output: 1.10 / 1000000 },
|
|
131
|
+
// Kimi
|
|
132
|
+
"kimi-k2": { input: 0.60 / 1000000, output: 2.50 / 1000000 },
|
|
133
|
+
"moonshot-v1-8k": { input: 0.20 / 1000000, output: 2.00 / 1000000 },
|
|
134
|
+
};
|
|
135
|
+
// ---- BloopClient ----
|
|
1
136
|
export class BloopClient {
|
|
2
137
|
constructor(config) {
|
|
3
138
|
this.buffer = [];
|
|
139
|
+
this.traceBuffer = [];
|
|
4
140
|
this.timer = null;
|
|
5
141
|
this.signingKey = null;
|
|
142
|
+
this.globalHandlersInstalled = false;
|
|
143
|
+
this._modelCosts = {};
|
|
6
144
|
this.config = {
|
|
7
145
|
source: "api",
|
|
8
146
|
maxBufferSize: 20,
|
|
@@ -19,6 +157,49 @@ export class BloopClient {
|
|
|
19
157
|
const encoder = new TextEncoder();
|
|
20
158
|
this.signingKey = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
21
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Install global error handlers for uncaught exceptions and unhandled rejections.
|
|
162
|
+
* Detects runtime (Node.js vs browser) and installs appropriate handlers.
|
|
163
|
+
* Uses addEventListener (not assignment) to avoid clobbering existing handlers.
|
|
164
|
+
*/
|
|
165
|
+
installGlobalHandlers() {
|
|
166
|
+
if (this.globalHandlersInstalled)
|
|
167
|
+
return;
|
|
168
|
+
this.globalHandlersInstalled = true;
|
|
169
|
+
const isNode = typeof globalThis !== "undefined" &&
|
|
170
|
+
typeof globalThis.process?.on === "function";
|
|
171
|
+
if (isNode) {
|
|
172
|
+
const proc = globalThis.process;
|
|
173
|
+
proc.on("uncaughtException", (error) => {
|
|
174
|
+
this.captureError(error, {
|
|
175
|
+
metadata: { unhandled: true, mechanism: "uncaughtException" },
|
|
176
|
+
});
|
|
177
|
+
this.flush();
|
|
178
|
+
});
|
|
179
|
+
proc.on("unhandledRejection", (reason) => {
|
|
180
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
181
|
+
this.captureError(error, {
|
|
182
|
+
metadata: { unhandled: true, mechanism: "unhandledRejection" },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else if (typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function") {
|
|
187
|
+
globalThis.addEventListener("error", (event) => {
|
|
188
|
+
const error = event.error instanceof Error ? event.error : new Error(event.message || "Unknown error");
|
|
189
|
+
this.captureError(error, {
|
|
190
|
+
metadata: { unhandled: true, mechanism: "onerror" },
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
globalThis.addEventListener("unhandledrejection", (event) => {
|
|
194
|
+
const error = event.reason instanceof Error
|
|
195
|
+
? event.reason
|
|
196
|
+
: new Error(String(event.reason));
|
|
197
|
+
this.captureError(error, {
|
|
198
|
+
metadata: { unhandled: true, mechanism: "onunhandledrejection" },
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
22
203
|
/** Capture an Error object. */
|
|
23
204
|
captureError(error, context) {
|
|
24
205
|
this.capture({
|
|
@@ -55,8 +236,208 @@ export class BloopClient {
|
|
|
55
236
|
this.flush();
|
|
56
237
|
}
|
|
57
238
|
}
|
|
239
|
+
// ---- LLM Tracing ----
|
|
240
|
+
/** Start a new trace. Returns a Trace object for adding spans. */
|
|
241
|
+
startTrace(opts) {
|
|
242
|
+
return new Trace(opts, this);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Convenience method: creates a trace with a single generation span,
|
|
246
|
+
* calls the provided function, and ends both span and trace.
|
|
247
|
+
* Returns the value returned by the callback.
|
|
248
|
+
*/
|
|
249
|
+
async traceGeneration(opts, fn) {
|
|
250
|
+
const trace = this.startTrace({
|
|
251
|
+
name: opts.name,
|
|
252
|
+
sessionId: opts.sessionId,
|
|
253
|
+
userId: opts.userId,
|
|
254
|
+
input: opts.input,
|
|
255
|
+
metadata: opts.metadata,
|
|
256
|
+
});
|
|
257
|
+
const span = trace.startSpan({
|
|
258
|
+
spanType: "generation",
|
|
259
|
+
name: opts.name,
|
|
260
|
+
model: opts.model,
|
|
261
|
+
provider: opts.provider,
|
|
262
|
+
input: opts.input,
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
const result = await fn(span);
|
|
266
|
+
trace.end({ status: "completed", output: span.output });
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (!span.status) {
|
|
271
|
+
span.end({
|
|
272
|
+
status: "error",
|
|
273
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
trace.end({ status: "error" });
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ---- Auto-Instrumentation ----
|
|
281
|
+
/** Set custom cost rates for a model (per-token in dollars). */
|
|
282
|
+
setModelCosts(model, costs) {
|
|
283
|
+
this._modelCosts[model] = costs;
|
|
284
|
+
}
|
|
285
|
+
/** Wrap an OpenAI-compatible client instance to automatically trace all LLM calls. */
|
|
286
|
+
wrapOpenAI(client) {
|
|
287
|
+
const bloop = this;
|
|
288
|
+
const provider = detectProvider(client);
|
|
289
|
+
return new Proxy(client, {
|
|
290
|
+
get(target, prop, receiver) {
|
|
291
|
+
const val = Reflect.get(target, prop, receiver);
|
|
292
|
+
if (prop === "chat") {
|
|
293
|
+
return new Proxy(val, {
|
|
294
|
+
get(chatTarget, chatProp, chatReceiver) {
|
|
295
|
+
const chatVal = Reflect.get(chatTarget, chatProp, chatReceiver);
|
|
296
|
+
if (chatProp === "completions") {
|
|
297
|
+
return new Proxy(chatVal, {
|
|
298
|
+
get(compTarget, compProp, compReceiver) {
|
|
299
|
+
const compVal = Reflect.get(compTarget, compProp, compReceiver);
|
|
300
|
+
if (compProp === "create" && typeof compVal === "function") {
|
|
301
|
+
return async function (...args) {
|
|
302
|
+
return bloop._traceOpenAICall(provider, "chat.completions.create", compVal.bind(compTarget), args);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return compVal;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return chatVal;
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (prop === "embeddings") {
|
|
314
|
+
return new Proxy(val, {
|
|
315
|
+
get(embTarget, embProp, embReceiver) {
|
|
316
|
+
const embVal = Reflect.get(embTarget, embProp, embReceiver);
|
|
317
|
+
if (embProp === "create" && typeof embVal === "function") {
|
|
318
|
+
return async function (...args) {
|
|
319
|
+
return bloop._traceOpenAICall(provider, "embeddings.create", embVal.bind(embTarget), args);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return embVal;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return val;
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Wrap an Anthropic client instance to automatically trace all messages.create calls. */
|
|
331
|
+
wrapAnthropic(client) {
|
|
332
|
+
const bloop = this;
|
|
333
|
+
return new Proxy(client, {
|
|
334
|
+
get(target, prop, receiver) {
|
|
335
|
+
const val = Reflect.get(target, prop, receiver);
|
|
336
|
+
if (prop === "messages") {
|
|
337
|
+
return new Proxy(val, {
|
|
338
|
+
get(msgTarget, msgProp, msgReceiver) {
|
|
339
|
+
const msgVal = Reflect.get(msgTarget, msgProp, msgReceiver);
|
|
340
|
+
if (msgProp === "create" && typeof msgVal === "function") {
|
|
341
|
+
return async function (...args) {
|
|
342
|
+
return bloop._traceAnthropicCall(msgVal.bind(msgTarget), args);
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return msgVal;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return val;
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/** Internal: trace an OpenAI-style API call. */
|
|
354
|
+
async _traceOpenAICall(provider, method, fn, args) {
|
|
355
|
+
const params = args[0] || {};
|
|
356
|
+
const model = params.model || "unknown";
|
|
357
|
+
const traceName = `${provider}.${method}`;
|
|
358
|
+
const trace = this.startTrace({ name: traceName });
|
|
359
|
+
const span = trace.startSpan({
|
|
360
|
+
spanType: "generation",
|
|
361
|
+
name: traceName,
|
|
362
|
+
model,
|
|
363
|
+
provider,
|
|
364
|
+
input: params.messages
|
|
365
|
+
? JSON.stringify(params.messages.slice(-1))
|
|
366
|
+
: undefined,
|
|
367
|
+
});
|
|
368
|
+
try {
|
|
369
|
+
const response = await fn(...args);
|
|
370
|
+
// Extract usage from response
|
|
371
|
+
const usage = response?.usage;
|
|
372
|
+
const inputTokens = usage?.prompt_tokens || 0;
|
|
373
|
+
const outputTokens = usage?.completion_tokens || 0;
|
|
374
|
+
// Compute cost
|
|
375
|
+
const rates = this._modelCosts[model] || MODEL_COSTS[model];
|
|
376
|
+
let cost = 0;
|
|
377
|
+
if (rates) {
|
|
378
|
+
cost = inputTokens * rates.input + outputTokens * rates.output;
|
|
379
|
+
}
|
|
380
|
+
// Extract output
|
|
381
|
+
const output = response?.choices?.[0]?.message?.content;
|
|
382
|
+
span.end({ inputTokens, outputTokens, cost, status: "ok", output });
|
|
383
|
+
trace.end({ status: "completed", output });
|
|
384
|
+
return response;
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
span.end({
|
|
388
|
+
status: "error",
|
|
389
|
+
errorMessage: err?.message || String(err),
|
|
390
|
+
});
|
|
391
|
+
trace.end({ status: "error" });
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Internal: trace an Anthropic messages.create call. */
|
|
396
|
+
async _traceAnthropicCall(fn, args) {
|
|
397
|
+
const params = args[0] || {};
|
|
398
|
+
const model = params.model || "unknown";
|
|
399
|
+
const trace = this.startTrace({ name: "anthropic.messages.create" });
|
|
400
|
+
const span = trace.startSpan({
|
|
401
|
+
spanType: "generation",
|
|
402
|
+
name: "anthropic.messages.create",
|
|
403
|
+
model,
|
|
404
|
+
provider: "anthropic",
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
const response = await fn(...args);
|
|
408
|
+
const inputTokens = response?.usage?.input_tokens || 0;
|
|
409
|
+
const outputTokens = response?.usage?.output_tokens || 0;
|
|
410
|
+
const rates = this._modelCosts[model] || MODEL_COSTS[model];
|
|
411
|
+
let cost = 0;
|
|
412
|
+
if (rates) {
|
|
413
|
+
cost = inputTokens * rates.input + outputTokens * rates.output;
|
|
414
|
+
}
|
|
415
|
+
const output = response?.content?.[0]?.text;
|
|
416
|
+
span.end({ inputTokens, outputTokens, cost, status: "ok", output });
|
|
417
|
+
trace.end({ status: "completed", output });
|
|
418
|
+
return response;
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
span.end({
|
|
422
|
+
status: "error",
|
|
423
|
+
errorMessage: err?.message || String(err),
|
|
424
|
+
});
|
|
425
|
+
trace.end({ status: "error" });
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** Push a completed trace payload to the buffer. Called by Trace.end(). */
|
|
430
|
+
_pushTrace(payload) {
|
|
431
|
+
this.traceBuffer.push(payload);
|
|
432
|
+
}
|
|
58
433
|
/** Flush buffered events to the server. */
|
|
59
434
|
async flush() {
|
|
435
|
+
const flushErrors = this._flushErrors();
|
|
436
|
+
const flushTraces = this._flushTraces();
|
|
437
|
+
await Promise.all([flushErrors, flushTraces]);
|
|
438
|
+
}
|
|
439
|
+
/** Flush error events to the ingest endpoint. */
|
|
440
|
+
async _flushErrors() {
|
|
60
441
|
if (this.buffer.length === 0)
|
|
61
442
|
return;
|
|
62
443
|
// Ensure key is ready
|
|
@@ -90,6 +471,39 @@ export class BloopClient {
|
|
|
90
471
|
}
|
|
91
472
|
}
|
|
92
473
|
}
|
|
474
|
+
/** Flush trace payloads to the traces endpoint. */
|
|
475
|
+
async _flushTraces() {
|
|
476
|
+
if (this.traceBuffer.length === 0)
|
|
477
|
+
return;
|
|
478
|
+
// Ensure key is ready
|
|
479
|
+
await this.keyReady;
|
|
480
|
+
const traces = this.traceBuffer.splice(0);
|
|
481
|
+
const body = JSON.stringify({ traces });
|
|
482
|
+
const signature = await this.sign(body);
|
|
483
|
+
const headers = {
|
|
484
|
+
"Content-Type": "application/json",
|
|
485
|
+
"X-Signature": signature,
|
|
486
|
+
};
|
|
487
|
+
if (this.config.projectKey) {
|
|
488
|
+
headers["X-Project-Key"] = this.config.projectKey;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const resp = await fetch(`${this.config.endpoint}/v1/traces/batch`, {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers,
|
|
494
|
+
body,
|
|
495
|
+
});
|
|
496
|
+
if (!resp.ok) {
|
|
497
|
+
console.warn(`[bloop] trace flush failed: ${resp.status}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
if (typeof globalThis !== "undefined" &&
|
|
502
|
+
globalThis.process?.env?.NODE_ENV !== "production") {
|
|
503
|
+
console.warn("[bloop] trace flush error:", err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
93
507
|
/** Express/Koa error middleware. */
|
|
94
508
|
errorMiddleware() {
|
|
95
509
|
return (err, req, _res, next) => {
|
|
@@ -135,9 +549,29 @@ class HttpError extends Error {
|
|
|
135
549
|
// release: "1.0.0",
|
|
136
550
|
// });
|
|
137
551
|
//
|
|
552
|
+
// // Install global handlers (Node.js or browser)
|
|
553
|
+
// client.installGlobalHandlers();
|
|
554
|
+
//
|
|
138
555
|
// // Capture errors
|
|
139
556
|
// client.captureError(new Error("something broke"), { route: "/api/users" });
|
|
140
557
|
//
|
|
558
|
+
// // LLM Tracing
|
|
559
|
+
// const trace = client.startTrace({ name: "chat-completion", input: "Hello" });
|
|
560
|
+
// const span = trace.startSpan({ spanType: "generation", model: "gpt-4o", provider: "openai" });
|
|
561
|
+
// // ... call LLM ...
|
|
562
|
+
// span.end({ inputTokens: 100, outputTokens: 50, cost: 0.0025, status: "ok", output: "Hi!" });
|
|
563
|
+
// trace.end({ status: "completed", output: "Hi!" });
|
|
564
|
+
//
|
|
565
|
+
// // Convenience: traceGeneration
|
|
566
|
+
// const result = await client.traceGeneration(
|
|
567
|
+
// { name: "quick-gen", model: "gpt-4o", provider: "openai", input: "Hello" },
|
|
568
|
+
// async (span) => {
|
|
569
|
+
// const response = "Hi!"; // call LLM here
|
|
570
|
+
// span.end({ inputTokens: 10, outputTokens: 5, cost: 0.001, status: "ok", output: response });
|
|
571
|
+
// return response;
|
|
572
|
+
// }
|
|
573
|
+
// );
|
|
574
|
+
//
|
|
141
575
|
// // Express middleware
|
|
142
576
|
// app.use(client.errorMiddleware());
|
|
143
577
|
//
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level and behavioral tests for auto-instrumentation.
|
|
3
|
+
* `npx tsc --noEmit` must pass with this file included.
|
|
4
|
+
*
|
|
5
|
+
* Tests cover:
|
|
6
|
+
* 1. MODEL_COSTS export exists and has correct shape
|
|
7
|
+
* 2. detectProvider() helper works (tested indirectly via wrapOpenAI)
|
|
8
|
+
* 3. BloopClient.wrapOpenAI() exists, accepts an object, returns same type
|
|
9
|
+
* 4. BloopClient.wrapAnthropic() exists, accepts an object, returns same type
|
|
10
|
+
* 5. BloopClient.setModelCosts() exists with correct signature
|
|
11
|
+
* 6. Wrapped client preserves original type
|
|
12
|
+
* 7. Backward compatibility: all existing APIs still work
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level and behavioral tests for auto-instrumentation.
|
|
3
|
+
* `npx tsc --noEmit` must pass with this file included.
|
|
4
|
+
*
|
|
5
|
+
* Tests cover:
|
|
6
|
+
* 1. MODEL_COSTS export exists and has correct shape
|
|
7
|
+
* 2. detectProvider() helper works (tested indirectly via wrapOpenAI)
|
|
8
|
+
* 3. BloopClient.wrapOpenAI() exists, accepts an object, returns same type
|
|
9
|
+
* 4. BloopClient.wrapAnthropic() exists, accepts an object, returns same type
|
|
10
|
+
* 5. BloopClient.setModelCosts() exists with correct signature
|
|
11
|
+
* 6. Wrapped client preserves original type
|
|
12
|
+
* 7. Backward compatibility: all existing APIs still work
|
|
13
|
+
*/
|
|
14
|
+
import { BloopClient, MODEL_COSTS, } from "./bloop.js";
|
|
15
|
+
// ---- Test: MODEL_COSTS export exists and has correct shape ----
|
|
16
|
+
const costs = MODEL_COSTS;
|
|
17
|
+
// Verify specific models exist
|
|
18
|
+
const gpt4oCost = MODEL_COSTS["gpt-4o"];
|
|
19
|
+
const gpt4oMiniCost = MODEL_COSTS["gpt-4o-mini"];
|
|
20
|
+
// Values should be numbers (compile-time check)
|
|
21
|
+
const inputCost = MODEL_COSTS["gpt-4o"].input;
|
|
22
|
+
const outputCost = MODEL_COSTS["gpt-4o"].output;
|
|
23
|
+
// ---- Test: BloopClient construction (unchanged) ----
|
|
24
|
+
const client = new BloopClient({
|
|
25
|
+
endpoint: "https://errors.test.com",
|
|
26
|
+
projectKey: "test-key",
|
|
27
|
+
environment: "test",
|
|
28
|
+
release: "1.0.0",
|
|
29
|
+
});
|
|
30
|
+
// ---- Test: setModelCosts() method exists ----
|
|
31
|
+
client.setModelCosts("my-custom-model", { input: 0.001, output: 0.002 });
|
|
32
|
+
client.setModelCosts("gpt-4o", { input: 0.003, output: 0.01 }); // Override built-in
|
|
33
|
+
// wrapOpenAI should accept any object and return the same type
|
|
34
|
+
const mockOAI = {};
|
|
35
|
+
const wrappedOAI = client.wrapOpenAI(mockOAI);
|
|
36
|
+
// The returned type must be the same as the input type
|
|
37
|
+
function assertTypePreserved(input) {
|
|
38
|
+
return client.wrapOpenAI(input);
|
|
39
|
+
}
|
|
40
|
+
const mockAnthropic = {};
|
|
41
|
+
const wrappedAnthropic = client.wrapAnthropic(mockAnthropic);
|
|
42
|
+
// ---- Test: Backward compatibility - existing APIs still work ----
|
|
43
|
+
async function testBackwardCompat() {
|
|
44
|
+
// startTrace still works
|
|
45
|
+
const trace = client.startTrace({ name: "compat-test" });
|
|
46
|
+
const span = trace.startSpan({ spanType: "generation", model: "gpt-4o" });
|
|
47
|
+
span.end({ status: "ok", inputTokens: 10, outputTokens: 5 });
|
|
48
|
+
trace.end({ status: "completed" });
|
|
49
|
+
// traceGeneration still works
|
|
50
|
+
const result = await client.traceGeneration({ name: "compat-gen", model: "gpt-4o", provider: "openai" }, async (span) => {
|
|
51
|
+
span.end({ status: "ok" });
|
|
52
|
+
return "result";
|
|
53
|
+
});
|
|
54
|
+
const s = result;
|
|
55
|
+
// flush/close still work
|
|
56
|
+
await client.flush();
|
|
57
|
+
await client.close();
|
|
58
|
+
}
|
|
59
|
+
const extClient = {};
|
|
60
|
+
const wrappedExt = client.wrapOpenAI(extClient);
|
|
61
|
+
// customProp and someMethod should still be accessible (type-level)
|
|
62
|
+
const _cp = wrappedExt.customProp;
|
|
63
|
+
const _sm = wrappedExt.someMethod;
|
|
64
|
+
// ---- Test: wrapOpenAI and wrapAnthropic can be called with plain objects ----
|
|
65
|
+
const plainWrapped = client.wrapOpenAI({
|
|
66
|
+
chat: {
|
|
67
|
+
completions: {
|
|
68
|
+
create: async () => ({ choices: [], usage: { prompt_tokens: 0, completion_tokens: 0 } }),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const anthropicPlain = client.wrapAnthropic({
|
|
73
|
+
messages: {
|
|
74
|
+
create: async () => ({
|
|
75
|
+
content: [{ type: "text", text: "hello" }],
|
|
76
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level tests for LLM tracing support.
|
|
3
|
+
* This file exercises all new types and APIs.
|
|
4
|
+
* `npx tsc --noEmit` must pass for these tests to be "green."
|
|
5
|
+
*/
|
|
6
|
+
import { BloopClient, } from "./bloop.js";
|
|
7
|
+
// ---- Test: Types exist and are correct ----
|
|
8
|
+
const spanType = "generation";
|
|
9
|
+
const spanType2 = "tool";
|
|
10
|
+
const spanType3 = "retrieval";
|
|
11
|
+
const spanType4 = "custom";
|
|
12
|
+
const spanStatus = "ok";
|
|
13
|
+
const spanStatus2 = "error";
|
|
14
|
+
const traceStatus = "running";
|
|
15
|
+
const traceStatus2 = "completed";
|
|
16
|
+
const traceStatus3 = "error";
|
|
17
|
+
// ---- Test: TraceOptions interface ----
|
|
18
|
+
const traceOpts = {
|
|
19
|
+
name: "chat-completion",
|
|
20
|
+
sessionId: "sess-123",
|
|
21
|
+
userId: "user-456",
|
|
22
|
+
input: "Hello, world!",
|
|
23
|
+
metadata: { key: "value" },
|
|
24
|
+
promptName: "my-prompt",
|
|
25
|
+
promptVersion: "1.0",
|
|
26
|
+
};
|
|
27
|
+
// Minimal TraceOptions (only name required)
|
|
28
|
+
const traceOptsMinimal = {
|
|
29
|
+
name: "minimal-trace",
|
|
30
|
+
};
|
|
31
|
+
// ---- Test: SpanOptions interface ----
|
|
32
|
+
const spanOpts = {
|
|
33
|
+
spanType: "generation",
|
|
34
|
+
name: "gpt-4o call",
|
|
35
|
+
model: "gpt-4o",
|
|
36
|
+
provider: "openai",
|
|
37
|
+
input: "prompt text",
|
|
38
|
+
metadata: { temperature: 0.7 },
|
|
39
|
+
};
|
|
40
|
+
// Minimal SpanOptions (only spanType required)
|
|
41
|
+
const spanOptsMinimal = {
|
|
42
|
+
spanType: "tool",
|
|
43
|
+
};
|
|
44
|
+
// ---- Test: SpanEndOptions interface ----
|
|
45
|
+
const spanEndOpts = {
|
|
46
|
+
inputTokens: 100,
|
|
47
|
+
outputTokens: 50,
|
|
48
|
+
cost: 0.0025,
|
|
49
|
+
status: "ok",
|
|
50
|
+
errorMessage: undefined,
|
|
51
|
+
output: "Generated text",
|
|
52
|
+
timeToFirstTokenMs: 300,
|
|
53
|
+
};
|
|
54
|
+
// Minimal SpanEndOptions (only status required)
|
|
55
|
+
const spanEndOptsMinimal = {
|
|
56
|
+
status: "error",
|
|
57
|
+
errorMessage: "Something went wrong",
|
|
58
|
+
};
|
|
59
|
+
// ---- Test: TraceEndOptions interface ----
|
|
60
|
+
const traceEndOpts = {
|
|
61
|
+
status: "completed",
|
|
62
|
+
output: "Final response text",
|
|
63
|
+
};
|
|
64
|
+
const traceEndOptsMinimal = {
|
|
65
|
+
status: "error",
|
|
66
|
+
};
|
|
67
|
+
// ---- Test: BloopClient.startTrace() returns a Trace ----
|
|
68
|
+
const client = new BloopClient({
|
|
69
|
+
endpoint: "https://errors.test.com",
|
|
70
|
+
projectKey: "test-key",
|
|
71
|
+
environment: "test",
|
|
72
|
+
release: "1.0.0",
|
|
73
|
+
});
|
|
74
|
+
const trace = client.startTrace({
|
|
75
|
+
name: "chat-completion",
|
|
76
|
+
sessionId: "sess-123",
|
|
77
|
+
userId: "user-456",
|
|
78
|
+
input: "What is the weather?",
|
|
79
|
+
metadata: { source: "api" },
|
|
80
|
+
promptName: "weather-prompt",
|
|
81
|
+
promptVersion: "2.1",
|
|
82
|
+
});
|
|
83
|
+
// ---- Test: Trace properties ----
|
|
84
|
+
const traceId = trace.id;
|
|
85
|
+
const traceName = trace.name;
|
|
86
|
+
const traceSessionId = trace.sessionId;
|
|
87
|
+
const traceUserId = trace.userId;
|
|
88
|
+
const traceStatusProp = trace.status;
|
|
89
|
+
const traceInput = trace.input;
|
|
90
|
+
const traceOutput = trace.output;
|
|
91
|
+
const traceMetadata = trace.metadata;
|
|
92
|
+
const tracePromptName = trace.promptName;
|
|
93
|
+
const tracePromptVersion = trace.promptVersion;
|
|
94
|
+
const traceStartedAt = trace.startedAt;
|
|
95
|
+
const traceEndedAt = trace.endedAt;
|
|
96
|
+
const traceSpans = trace.spans;
|
|
97
|
+
// ---- Test: Trace.startSpan() returns a Span ----
|
|
98
|
+
const span = trace.startSpan({
|
|
99
|
+
spanType: "generation",
|
|
100
|
+
name: "gpt-4o call",
|
|
101
|
+
model: "gpt-4o",
|
|
102
|
+
provider: "openai",
|
|
103
|
+
input: "prompt",
|
|
104
|
+
metadata: { key: "val" },
|
|
105
|
+
});
|
|
106
|
+
// ---- Test: Span properties ----
|
|
107
|
+
const spanId = span.id;
|
|
108
|
+
const spanParentId = span.parentSpanId;
|
|
109
|
+
const spanSpanType = span.spanType;
|
|
110
|
+
const spanName = span.name;
|
|
111
|
+
const spanModel = span.model;
|
|
112
|
+
const spanProvider = span.provider;
|
|
113
|
+
const spanStartedAt = span.startedAt;
|
|
114
|
+
const spanInput = span.input;
|
|
115
|
+
const spanMetadata = span.metadata;
|
|
116
|
+
// ---- Test: Span.end() ----
|
|
117
|
+
span.end({
|
|
118
|
+
inputTokens: 100,
|
|
119
|
+
outputTokens: 50,
|
|
120
|
+
cost: 0.0025,
|
|
121
|
+
status: "ok",
|
|
122
|
+
output: "generated response",
|
|
123
|
+
timeToFirstTokenMs: 300,
|
|
124
|
+
});
|
|
125
|
+
// After end(), these should be set
|
|
126
|
+
const spanInputTokens = span.inputTokens;
|
|
127
|
+
const spanOutputTokens = span.outputTokens;
|
|
128
|
+
const spanCost = span.cost;
|
|
129
|
+
const spanLatencyMs = span.latencyMs;
|
|
130
|
+
const spanTtft = span.timeToFirstTokenMs;
|
|
131
|
+
const spanStatusProp = span.status;
|
|
132
|
+
const spanErrorMessage = span.errorMessage;
|
|
133
|
+
const spanOutput = span.output;
|
|
134
|
+
// ---- Test: Span.toJSON() returns object ----
|
|
135
|
+
const spanJson = span.toJSON();
|
|
136
|
+
// ---- Test: Trace.end() ----
|
|
137
|
+
trace.end({
|
|
138
|
+
status: "completed",
|
|
139
|
+
output: "Final answer about weather",
|
|
140
|
+
});
|
|
141
|
+
// ---- Test: traceGeneration convenience method ----
|
|
142
|
+
async function testTraceGeneration() {
|
|
143
|
+
const result = await client.traceGeneration({
|
|
144
|
+
name: "simple-generation",
|
|
145
|
+
model: "gpt-4o",
|
|
146
|
+
provider: "openai",
|
|
147
|
+
input: "Hello",
|
|
148
|
+
}, async (span) => {
|
|
149
|
+
// Do LLM call
|
|
150
|
+
span.end({
|
|
151
|
+
inputTokens: 10,
|
|
152
|
+
outputTokens: 5,
|
|
153
|
+
cost: 0.001,
|
|
154
|
+
status: "ok",
|
|
155
|
+
output: "Hi!",
|
|
156
|
+
});
|
|
157
|
+
return "Hi!";
|
|
158
|
+
});
|
|
159
|
+
// result should be the return value of the callback
|
|
160
|
+
const output = result;
|
|
161
|
+
}
|
|
162
|
+
// ---- Test: TraceGenerationOptions has correct shape ----
|
|
163
|
+
async function testTraceGenerationMinimal() {
|
|
164
|
+
const result = await client.traceGeneration({
|
|
165
|
+
name: "minimal-gen",
|
|
166
|
+
input: "test",
|
|
167
|
+
}, async (span) => {
|
|
168
|
+
span.end({ status: "ok" });
|
|
169
|
+
return 42;
|
|
170
|
+
});
|
|
171
|
+
const num = result;
|
|
172
|
+
}
|
|
173
|
+
// ---- Test: flush() still works (backward compat) ----
|
|
174
|
+
async function testFlush() {
|
|
175
|
+
await client.flush();
|
|
176
|
+
}
|
|
177
|
+
// ---- Test: close() still works (backward compat) ----
|
|
178
|
+
async function testClose() {
|
|
179
|
+
await client.close();
|
|
180
|
+
}
|
|
181
|
+
// ---- Test: Error tracking still works (backward compat) ----
|
|
182
|
+
function testErrorTracking() {
|
|
183
|
+
client.captureError(new Error("test error"), { route: "/api/test" });
|
|
184
|
+
client.capture({
|
|
185
|
+
errorType: "TestError",
|
|
186
|
+
message: "test message",
|
|
187
|
+
});
|
|
188
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bloop-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lightweight error reporting SDK for bloop",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Lightweight error reporting and LLM tracing SDK for bloop",
|
|
5
5
|
"main": "dist/bloop.js",
|
|
6
6
|
"types": "dist/bloop.d.ts",
|
|
7
7
|
"files": ["dist"],
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"prepublishOnly": "npm run build"
|
|
11
11
|
},
|
|
12
|
-
"keywords": ["error-tracking", "observability", "bloop"],
|
|
12
|
+
"keywords": ["error-tracking", "observability", "llm-tracing", "bloop"],
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|