@visibe.ai/node 0.1.24 → 0.1.25

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.
@@ -132,6 +132,170 @@ class Visibe {
132
132
  });
133
133
  }
134
134
  // ---------------------------------------------------------------------------
135
+ // runWithSession() — group all auto-instrumented LLM calls inside fn()
136
+ // into a single named trace. Unlike track(), no specific client is required;
137
+ // all already-instrumented clients are automatically captured.
138
+ // ---------------------------------------------------------------------------
139
+ async runWithSession(name, fn) {
140
+ const traceId = (0, node_crypto_1.randomUUID)();
141
+ const startedAt = new Date().toISOString();
142
+ const startMs = Date.now();
143
+ await this.apiClient.createTrace({
144
+ trace_id: traceId,
145
+ name,
146
+ framework: 'session',
147
+ started_at: startedAt,
148
+ ...(this.sessionId ? { session_id: this.sessionId } : {}),
149
+ });
150
+ let llmCallCount = 0;
151
+ let toolCallCount = 0;
152
+ let totalInput = 0;
153
+ let totalOutput = 0;
154
+ let totalCost = 0;
155
+ const groupCtx = {
156
+ traceId,
157
+ onLLMSpan: (inputTokens, outputTokens, cost) => {
158
+ llmCallCount++;
159
+ totalInput += inputTokens;
160
+ totalOutput += outputTokens;
161
+ totalCost += cost;
162
+ },
163
+ onToolSpan: () => { toolCallCount++; },
164
+ };
165
+ return group_context_1.activeGroupTraceStorage.run(groupCtx, async () => {
166
+ let status = 'completed';
167
+ try {
168
+ return await fn();
169
+ }
170
+ catch (err) {
171
+ status = 'failed';
172
+ throw err;
173
+ }
174
+ finally {
175
+ const durationMs = Date.now() - startMs;
176
+ // CRITICAL ORDER: flush spans first, then complete the trace.
177
+ this.batcher.flush();
178
+ const sent = await this.apiClient.completeTrace(traceId, {
179
+ status,
180
+ ended_at: new Date().toISOString(),
181
+ duration_ms: durationMs,
182
+ llm_call_count: llmCallCount,
183
+ total_cost: parseFloat(totalCost.toFixed(6)),
184
+ total_tokens: totalInput + totalOutput,
185
+ total_input_tokens: totalInput,
186
+ total_output_tokens: totalOutput,
187
+ });
188
+ printTraceSummary({
189
+ name,
190
+ llmCallCount,
191
+ toolCallCount,
192
+ totalTokens: totalInput + totalOutput,
193
+ totalCost,
194
+ durationMs,
195
+ status,
196
+ }, sent);
197
+ }
198
+ });
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // middleware() — Express / Connect / Fastify compatible middleware.
202
+ //
203
+ // Groups every LLM call made during a single HTTP request into one trace.
204
+ // Uses AsyncLocalStorage so concurrent requests are fully isolated — each
205
+ // request gets its own trace regardless of how many are in flight at once.
206
+ //
207
+ // Usage:
208
+ // app.use(visibe.middleware())
209
+ // app.use(visibe.middleware({ name: (req) => `${req.method} ${req.url}` }))
210
+ // ---------------------------------------------------------------------------
211
+ middleware(options) {
212
+ return (req, res, next) => {
213
+ // No-op immediately if the SDK is disabled (no/invalid API key).
214
+ if (!this.apiClient._enabled) {
215
+ next();
216
+ return;
217
+ }
218
+ const traceId = (0, node_crypto_1.randomUUID)();
219
+ const startedAt = new Date().toISOString();
220
+ const startMs = Date.now();
221
+ // Resolve the trace name from options or fall back to "METHOD /url".
222
+ const name = typeof options?.name === 'function'
223
+ ? options.name(req)
224
+ : options?.name ?? `${req.method ?? 'HTTP'} ${req.url ?? '/'}`;
225
+ // Fire createTrace without awaiting — we own the traceId so spans
226
+ // can be buffered safely until the trace creation lands on the backend.
227
+ // Awaiting here would add a network round-trip to every request.
228
+ this.apiClient.createTrace({
229
+ trace_id: traceId,
230
+ name,
231
+ framework: 'http',
232
+ started_at: startedAt,
233
+ ...(this.sessionId ? { session_id: this.sessionId } : {}),
234
+ }).catch(() => { });
235
+ let llmCallCount = 0;
236
+ let toolCallCount = 0;
237
+ let totalInput = 0;
238
+ let totalOutput = 0;
239
+ let totalCost = 0;
240
+ const groupCtx = {
241
+ traceId,
242
+ onLLMSpan: (inputTokens, outputTokens, cost) => {
243
+ llmCallCount++;
244
+ totalInput += inputTokens;
245
+ totalOutput += outputTokens;
246
+ totalCost += cost;
247
+ },
248
+ onToolSpan: () => { toolCallCount++; },
249
+ };
250
+ // Guard against both 'finish' and 'close' firing for the same response.
251
+ // 'finish' fires when all data is written; 'close' fires when the
252
+ // underlying connection is closed (e.g. client disconnected early).
253
+ // We only want to complete the trace once.
254
+ let completed = false;
255
+ const complete = () => {
256
+ if (completed)
257
+ return;
258
+ completed = true;
259
+ const durationMs = Date.now() - startMs;
260
+ // Treat any 4xx/5xx as a failed trace.
261
+ const status = res.statusCode >= 400 ? 'failed' : 'completed';
262
+ // Flush buffered spans before completing the trace so the backend
263
+ // has all spans available when it processes the PATCH request.
264
+ this.batcher.flush();
265
+ this.apiClient.completeTrace(traceId, {
266
+ status,
267
+ ended_at: new Date().toISOString(),
268
+ duration_ms: durationMs,
269
+ llm_call_count: llmCallCount,
270
+ total_cost: parseFloat(totalCost.toFixed(6)),
271
+ total_tokens: totalInput + totalOutput,
272
+ total_input_tokens: totalInput,
273
+ total_output_tokens: totalOutput,
274
+ }).catch(() => { });
275
+ // Only print in debug mode — middleware runs on every request and
276
+ // would produce too much noise in production by default.
277
+ if (this.debug) {
278
+ printTraceSummary({
279
+ name,
280
+ llmCallCount,
281
+ toolCallCount,
282
+ totalTokens: totalInput + totalOutput,
283
+ totalCost,
284
+ durationMs,
285
+ status,
286
+ }, true);
287
+ }
288
+ };
289
+ // Run next() inside the ALS context so all async operations spawned
290
+ // by the route handler inherit this request's groupCtx automatically.
291
+ group_context_1.activeGroupTraceStorage.run(groupCtx, () => {
292
+ res.on('finish', complete);
293
+ res.on('close', complete);
294
+ next();
295
+ });
296
+ };
297
+ }
298
+ // ---------------------------------------------------------------------------
135
299
  // flushSpans() — called by shutdown() in index.ts.
136
300
  // ---------------------------------------------------------------------------
137
301
  flushSpans() {
@@ -129,6 +129,170 @@ export class Visibe {
129
129
  });
130
130
  }
131
131
  // ---------------------------------------------------------------------------
132
+ // runWithSession() — group all auto-instrumented LLM calls inside fn()
133
+ // into a single named trace. Unlike track(), no specific client is required;
134
+ // all already-instrumented clients are automatically captured.
135
+ // ---------------------------------------------------------------------------
136
+ async runWithSession(name, fn) {
137
+ const traceId = randomUUID();
138
+ const startedAt = new Date().toISOString();
139
+ const startMs = Date.now();
140
+ await this.apiClient.createTrace({
141
+ trace_id: traceId,
142
+ name,
143
+ framework: 'session',
144
+ started_at: startedAt,
145
+ ...(this.sessionId ? { session_id: this.sessionId } : {}),
146
+ });
147
+ let llmCallCount = 0;
148
+ let toolCallCount = 0;
149
+ let totalInput = 0;
150
+ let totalOutput = 0;
151
+ let totalCost = 0;
152
+ const groupCtx = {
153
+ traceId,
154
+ onLLMSpan: (inputTokens, outputTokens, cost) => {
155
+ llmCallCount++;
156
+ totalInput += inputTokens;
157
+ totalOutput += outputTokens;
158
+ totalCost += cost;
159
+ },
160
+ onToolSpan: () => { toolCallCount++; },
161
+ };
162
+ return activeGroupTraceStorage.run(groupCtx, async () => {
163
+ let status = 'completed';
164
+ try {
165
+ return await fn();
166
+ }
167
+ catch (err) {
168
+ status = 'failed';
169
+ throw err;
170
+ }
171
+ finally {
172
+ const durationMs = Date.now() - startMs;
173
+ // CRITICAL ORDER: flush spans first, then complete the trace.
174
+ this.batcher.flush();
175
+ const sent = await this.apiClient.completeTrace(traceId, {
176
+ status,
177
+ ended_at: new Date().toISOString(),
178
+ duration_ms: durationMs,
179
+ llm_call_count: llmCallCount,
180
+ total_cost: parseFloat(totalCost.toFixed(6)),
181
+ total_tokens: totalInput + totalOutput,
182
+ total_input_tokens: totalInput,
183
+ total_output_tokens: totalOutput,
184
+ });
185
+ printTraceSummary({
186
+ name,
187
+ llmCallCount,
188
+ toolCallCount,
189
+ totalTokens: totalInput + totalOutput,
190
+ totalCost,
191
+ durationMs,
192
+ status,
193
+ }, sent);
194
+ }
195
+ });
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // middleware() — Express / Connect / Fastify compatible middleware.
199
+ //
200
+ // Groups every LLM call made during a single HTTP request into one trace.
201
+ // Uses AsyncLocalStorage so concurrent requests are fully isolated — each
202
+ // request gets its own trace regardless of how many are in flight at once.
203
+ //
204
+ // Usage:
205
+ // app.use(visibe.middleware())
206
+ // app.use(visibe.middleware({ name: (req) => `${req.method} ${req.url}` }))
207
+ // ---------------------------------------------------------------------------
208
+ middleware(options) {
209
+ return (req, res, next) => {
210
+ // No-op immediately if the SDK is disabled (no/invalid API key).
211
+ if (!this.apiClient._enabled) {
212
+ next();
213
+ return;
214
+ }
215
+ const traceId = randomUUID();
216
+ const startedAt = new Date().toISOString();
217
+ const startMs = Date.now();
218
+ // Resolve the trace name from options or fall back to "METHOD /url".
219
+ const name = typeof options?.name === 'function'
220
+ ? options.name(req)
221
+ : options?.name ?? `${req.method ?? 'HTTP'} ${req.url ?? '/'}`;
222
+ // Fire createTrace without awaiting — we own the traceId so spans
223
+ // can be buffered safely until the trace creation lands on the backend.
224
+ // Awaiting here would add a network round-trip to every request.
225
+ this.apiClient.createTrace({
226
+ trace_id: traceId,
227
+ name,
228
+ framework: 'http',
229
+ started_at: startedAt,
230
+ ...(this.sessionId ? { session_id: this.sessionId } : {}),
231
+ }).catch(() => { });
232
+ let llmCallCount = 0;
233
+ let toolCallCount = 0;
234
+ let totalInput = 0;
235
+ let totalOutput = 0;
236
+ let totalCost = 0;
237
+ const groupCtx = {
238
+ traceId,
239
+ onLLMSpan: (inputTokens, outputTokens, cost) => {
240
+ llmCallCount++;
241
+ totalInput += inputTokens;
242
+ totalOutput += outputTokens;
243
+ totalCost += cost;
244
+ },
245
+ onToolSpan: () => { toolCallCount++; },
246
+ };
247
+ // Guard against both 'finish' and 'close' firing for the same response.
248
+ // 'finish' fires when all data is written; 'close' fires when the
249
+ // underlying connection is closed (e.g. client disconnected early).
250
+ // We only want to complete the trace once.
251
+ let completed = false;
252
+ const complete = () => {
253
+ if (completed)
254
+ return;
255
+ completed = true;
256
+ const durationMs = Date.now() - startMs;
257
+ // Treat any 4xx/5xx as a failed trace.
258
+ const status = res.statusCode >= 400 ? 'failed' : 'completed';
259
+ // Flush buffered spans before completing the trace so the backend
260
+ // has all spans available when it processes the PATCH request.
261
+ this.batcher.flush();
262
+ this.apiClient.completeTrace(traceId, {
263
+ status,
264
+ ended_at: new Date().toISOString(),
265
+ duration_ms: durationMs,
266
+ llm_call_count: llmCallCount,
267
+ total_cost: parseFloat(totalCost.toFixed(6)),
268
+ total_tokens: totalInput + totalOutput,
269
+ total_input_tokens: totalInput,
270
+ total_output_tokens: totalOutput,
271
+ }).catch(() => { });
272
+ // Only print in debug mode — middleware runs on every request and
273
+ // would produce too much noise in production by default.
274
+ if (this.debug) {
275
+ printTraceSummary({
276
+ name,
277
+ llmCallCount,
278
+ toolCallCount,
279
+ totalTokens: totalInput + totalOutput,
280
+ totalCost,
281
+ durationMs,
282
+ status,
283
+ }, true);
284
+ }
285
+ };
286
+ // Run next() inside the ALS context so all async operations spawned
287
+ // by the route handler inherit this request's groupCtx automatically.
288
+ activeGroupTraceStorage.run(groupCtx, () => {
289
+ res.on('finish', complete);
290
+ res.on('close', complete);
291
+ next();
292
+ });
293
+ };
294
+ }
295
+ // ---------------------------------------------------------------------------
132
296
  // flushSpans() — called by shutdown() in index.ts.
133
297
  // ---------------------------------------------------------------------------
134
298
  flushSpans() {
@@ -1,5 +1,5 @@
1
1
  import { APIClient, SpanBatcher } from './api';
2
- import type { InitOptions } from './types';
2
+ import type { InitOptions, MiddlewareOptions, MiddlewareRequest, MiddlewareResponse } from './types';
3
3
  export type { InitOptions };
4
4
  export declare class Visibe {
5
5
  readonly apiClient: APIClient;
@@ -14,6 +14,8 @@ export declare class Visibe {
14
14
  }): void;
15
15
  uninstrument(client: object): void;
16
16
  track<T>(client: object, name: string, fn: () => Promise<T>): Promise<T>;
17
+ runWithSession<T>(name: string, fn: () => Promise<T>): Promise<T>;
18
+ middleware(options?: MiddlewareOptions): (req: MiddlewareRequest, res: MiddlewareResponse, next: () => void) => void;
17
19
  flushSpans(): void;
18
20
  buildLLMSpan(opts: {
19
21
  spanId: string;
@@ -4,4 +4,4 @@ export declare function detectFrameworks(): Record<string, boolean>;
4
4
  export declare function init(options?: InitOptions): Visibe;
5
5
  export declare function shutdown(): Promise<void>;
6
6
  export { Visibe } from './client';
7
- export type { InitOptions, TraceSummary, SpanType, SpanStatus, TraceStatus, LLMProvider } from './types';
7
+ export type { InitOptions, MiddlewareOptions, MiddlewareRequest, MiddlewareResponse, TraceSummary, SpanType, SpanStatus, TraceStatus, LLMProvider } from './types';
@@ -19,3 +19,20 @@ export interface TraceSummary {
19
19
  durationMs: number;
20
20
  status: TraceStatus;
21
21
  }
22
+ export interface MiddlewareRequest {
23
+ method?: string;
24
+ url?: string;
25
+ }
26
+ export interface MiddlewareResponse {
27
+ statusCode: number;
28
+ on(event: 'finish' | 'close', listener: () => void): this;
29
+ }
30
+ export interface MiddlewareOptions {
31
+ /**
32
+ * Trace name for each request.
33
+ * - Omit for the default: "METHOD /url" (e.g. "POST /api/chat")
34
+ * - Pass a string for a fixed name
35
+ * - Pass a function to derive the name from the request
36
+ */
37
+ name?: string | ((req: MiddlewareRequest) => string);
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visibe.ai/node",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "AI Agent Observability — Track OpenAI, LangChain, LangGraph, Bedrock, Vercel AI, Anthropic",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",