@verica-app/observability
v0.1.6
Published
Two-line LLM tracing for Verica: init(token) and your OpenAI/Anthropic calls land as evaluable traces.
Maintainers
Readme
@verica-app/observability
Two-line LLM tracing for Verica: your OpenAI/Anthropic calls land as evaluable traces.
Install
npm install @verica-app/observabilityUse (CommonJS / bundled apps)
const { init, wrapGoogleGenAI } = require('@verica-app/observability');
init({
token: process.env.VERICA_TOKEN, // ingest-scoped API token
});
// Require the clients AFTER init. OpenAI and Anthropic are auto-traced:
const OpenAI = require('openai');
const Anthropic = require('@anthropic-ai/sdk');
// Gemini (@google/genai) has no community instrumentation, so wrap it:
const { GoogleGenAI } = require('@google/genai');
const ai = wrapGoogleGenAI(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }));Use (ESM)
ESM imports cannot be monkey-patched after the fact; pass the module in:
import { init } from '@verica-app/observability';
import * as openai from 'openai';
init({ token: process.env.VERICA_TOKEN, openaiModule: openai });Google Gemini
wrapGoogleGenAI returns the client with models.generateContent traced (see
the CommonJS example above); use it in ESM too by passing the constructed
client. Streaming (generateContentStream) passes through untraced for now.
Sessions (multi-turn)
Traces sharing a conversationId reassemble into a Verica session, one turn
per request.
Resend history exactly as sent. Turns are stored as deltas: at ingest, Verica matches the history you resend against what previous turns already stored, and that match is exact (byte-identical text). If your app mutates prior messages between requests (for example, appending "Respond in JSON" to the last user message and stripping it from earlier turns when rebuilding the history), no prefix ever matches and every turn falls back to storing, and showing, the full conversation again. Keep injected instructions in the system prompt, or resend them exactly as originally sent.
Agent loops
By default each LLM call lands as its own single-span trace. Wrap a whole
agent run in withSpan so every auto-instrumented call inside it (and any
nested withSpan / withTool) becomes a child: one trace per run.
withTool does the same and marks the span as a tool execution (the GenAI
semconv gen_ai.operation.name = execute_tool + gen_ai.tool.name, which
Verica detects for tool-span counting and span_check).
const { withSpan, withTool } = require('@verica-app/observability');
const answer = await withSpan('agent.run', async () => {
await client.chat.completions.create({ ... }); // child span
const weather = withTool('lookup_weather', () => lookupWeather('Cordoba'));
return client.chat.completions.create({ ... }); // child span
});Both take a name and a callback (sync or async) and return its value/promise
untouched. The span name is your string verbatim. On a thrown error the span
is marked with ERROR status and the exception re-raises unchanged. Fail-open:
without init() (or if tracing is broken) the callback just runs and nothing
is emitted. See examples/agent.cjs for a full run with two tools.
Tags
Tags land on each trace (traces.tags): filter the workbench by them, and bind
criteria to them so evaluation preselects the right criteria per tag.
await init({ token: process.env.VERICA_TOKEN!, tags: ['web', 'prod'] });
const reply = await withTags(['chat', 'premium'], () =>
withConversation(`chat-${chatId}`, () => openai.chat.completions.create({ ... })),
);Per-request tags UNION with the globals (dedup, order preserved); nested calls
accumulate. Scoping uses AsyncLocalStorage, so it survives awaits and never
leaks between concurrent requests. withConversation scopes the session id the
same way (overrides the global conversationId; null suppresses it). The
server caps at 20 tags x 120 chars.
Serverless
Call await flush() (or await shutdown()) before the runtime freezes so the
span batch is exported.
Options
| Option / env var | Default | Notes |
| ------------------------------------------- | ---------- | ------------------------------------------------------ |
| token / VERICA_TOKEN | (required) | ingest-scoped API token |
| captureContent / VERICA_CAPTURE_CONTENT | true | send prompt/response content |
| conversationId | (none) | stamps gen_ai.conversation.id |
| tags | [] | trace tags; scope with withTags / withConversation |
| serviceName / OTEL_SERVICE_NAME | app | resource service.name |
| debug / VERICA_DEBUG | false | log export errors |
Fail-open by design: if Verica is unreachable or the token is invalid, spans are
dropped and your app is never affected. Export errors are silent unless debug
is on.
