TypeScript SDK
Install, initialize, and instrument TypeScript/Node.js LLM and agent apps with Neatlogs.
The TypeScript SDK installs from npm as neatlogs. Wrap the AI clients you already use, add spans to your own functions, and track prompt templates — with an idiomatic, fully-async TypeScript API. This page covers everything from install to the full init() reference; use the contents on the right to jump around.
New to tracing? A trace is the record of one run of your app; a span is one step inside it (an LLM call, a tool call). You instrument once, then read traces in the dashboard. See Introduction.
Install
npm install neatlogs@latestRequires Node.js ≥ 18. To auto-instrument a provider, install its OpenInference peer dependency too (e.g. @arizeai/openinference-instrumentation-openai). See Supported Libraries.
Your first trace
init() is async — always await it. Then wrap your client with the matching wrap* helper.
import { init, wrapOpenAI, flush, shutdown } from 'neatlogs';
import OpenAI from 'openai';
async function main() {
await init({ apiKey: process.env.NEATLOGS_API_KEY, workflowName: 'my-first-app' });
const client = wrapOpenAI(new OpenAI());
const res = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});
console.log(res.choices[0].message.content);
await flush();
await shutdown();
}
main().catch(console.error);Run it with tsx, ts-node, or after compiling — a trace appears under my-first-app within a few seconds. wrapOpenAI opens a WORKFLOW root automatically (named after your workflowName) with the LLM span nested inside. No extra wrapper needed.
wrapOpenAI / wrapAnthropic patch the specific client you pass, so import order doesn't matter for them. Direct wrappers exist for many providers — see Integrations for OpenAI, Anthropic, Azure, Bedrock, Vertex, OpenRouter, and the Vercel AI SDK / Mastra / Claude Agent SDK, each with a runnable example.
The async-main and init-order rules
init(), flush(), and shutdown() are all async. Most setups don't allow top-level await, so wrap your program in an async function main().
When using instrumentations: [...] (rather than a wrap* helper), the library must be imported after init(). Static import statements are hoisted above your code, so use a dynamic import():
// ✅ init() first, then dynamically import the library
await init({ instrumentations: ['openai'] });
const { OpenAI } = await import('openai'); // patched
const client = new OpenAI();
// ❌ a static top-of-file import is hoisted above init()
import OpenAI from 'openai'; // runs BEFORE init() → NOT patchedForgetting await shutdown() (or await flush()) before a short-lived script exits is the most common reason traces don't appear. Long-running servers export continuously — call flush() + shutdown() once on server shutdown.
Instrument your own code with span()
span() is a higher-order function: pass it your function and it returns an instrumented version with the same signature. Each call creates a span capturing inputs, output, timing, and errors.
import { span } from 'neatlogs';
const handleRequest = span({ kind: 'WORKFLOW' }, async (userInput: string) => {
return await supportAgent(userInput);
});
const supportAgent = span({ kind: 'AGENT', name: 'support_agent' }, async (message: string) => {
// ...
});
const getOrderStatus = span({ kind: 'TOOL', toolName: 'get_order_status' }, async (orderId: string) => {
return ordersDb.get(orderId);
});Because each wrapper nests under whatever span is active when it runs, the trace tree mirrors your real call hierarchy.
Grouping calls into one trace. A single wrap*-ed call renders on its own — the wrap* helpers (wrapOpenAI, wrapAnthropic, wrapBedrock, wrapVertexAI, wrapAzureOpenAI, wrapOpenRouterAgent) open a WORKFLOW root automatically. But each call with no surrounding context becomes its own trace, so when a run makes several calls (or mixes them with your own functions), wrap the entry point with span({ kind: 'WORKFLOW' }, …) — it becomes the single root and everything nests under it.
One exception: the instrumentations: [...] path does not auto-root (it patches the library through OpenInference, not our wrapper), so always give those runs a span({ kind: 'WORKFLOW' }) root.
@Span decorator (class methods)
For class methods, use the TC39 Stage 3 decorator (TypeScript 5.0+, experimentalDecorators: false):
import { Span } from 'neatlogs';
class ResearchAgent {
@Span({ kind: 'AGENT', role: 'researcher' })
async run(query: string) {
return await this.search(query);
}
@Span({ kind: 'TOOL', toolName: 'web-search' })
async search(query: string) {
return { results: ['...'] };
}
}SpanOptions
| Option | Description |
|---|---|
kind | Required. The span kind (see below). |
name | Span label. Defaults to the function name. |
captureInput / captureOutput | Capture args / return value (default true). |
role, goal | Agent role and objective (kind: 'AGENT'). |
toolName, parameters | Tool identifier and parameter schema (kind: 'TOOL'). |
model, dimension | Embedding model and dimension (kind: 'EMBEDDING'). |
mask | Per-span redaction function. |
To omit content globally, set traceContent: false in init() (or NEATLOGS_TRACE_CONTENT=false).
Span kinds
span() accepts these eight kinds:
| Kind | Use for |
|---|---|
WORKFLOW | Top-level entry point — one per trace root |
AGENT | A reasoning step that calls an LLM and decides what to do next |
CHAIN | A fixed sequence of steps with no branching LLM decisions |
TOOL | A function the agent calls to interact with the world |
RETRIEVER | A vector search or document lookup |
EMBEDDING | A function that produces embeddings |
GUARDRAIL | A safety or validation check |
MCP_TOOL | A tool exposed over the Model Context Protocol |
Passing any other kind throws. LLM, RERANKER, and VECTOR_STORE are created with trace() instead — in TypeScript these aren't in the SpanKind union, so cast the kind (e.g. kind: 'LLM' as any). For the complete catalogue, see Span Kinds.
Inline spans with trace()
Where span() wraps a reusable function, trace() runs a callback inline — for prompt-template tracking, custom span kinds, or grouping an ad-hoc block. The callback receives the active span so you can set attributes.
import { trace } from 'neatlogs';
const docs = await trace({ name: 'retrieve', kind: 'RETRIEVER' }, async (activeSpan) => {
const results = await retriever.search(query, 5);
activeSpan.setAttribute('neatlogs.retrieval.query', query);
return results;
});Prompt templates
PromptTemplate (system/instruction) and UserPromptTemplate (user turn) accept a string or message array with {{variable}} placeholders. Pass them to trace() so the template and compiled variables are captured on the span.
import { trace, PromptTemplate, UserPromptTemplate } from 'neatlogs';
const systemTpl = new PromptTemplate('You are a {{role}} assistant.');
const userTpl = new UserPromptTemplate('{{question}}');
const answer = await trace(
{ name: 'qa', kind: 'LLM', promptTemplate: systemTpl, userPromptTemplate: userTpl },
async () => {
const system = systemTpl.compile({ role: 'helpful' });
const user = userTpl.compile({ question: 'What is TypeScript?' });
return callLLM(system, user);
},
);For prompts managed in the dashboard, the module-level functions (getPrompt, createPrompt, updatePrompt, saveAsVersion, …) and the PromptClient class are available after init(). See Prompt Templates.
log()
With captureLogs: true in init(), log() records a timestamped step inside the active span. Templates use single-brace {key} placeholders; the level key sets severity.
import { log } from 'neatlogs';
log('Retrieved {count} documents in {ms}ms', { count: 5, ms: 120 });Framework & provider helpers
Beyond instrumentations: [...], dedicated helpers attach tracing to a specific object or framework:
wrapOpenAI(client)/wrapAnthropic(client)— direct client wrappers.wrapAISDK(ai)(fromneatlogs/ai) — the Vercel AI SDK (generateText,streamText, …).wrapMastra(entity)(fromneatlogs/mastra) — Mastra agents, workflows, vectors.langchainHandler()— a LangChain/LangGraph callback handler.openaiAgentsProcessor()— a trace processor for the OpenAI Agents SDK.strandsHooks(agent)/piAgentHooks(agent)— Strands and Pi agents.- Provider subpaths:
neatlogs/azure-openai,neatlogs/bedrock,neatlogs/vertex-ai,neatlogs/openrouter-agent,neatlogs/claude-agent-sdk.
Each provider and framework has a runnable example in Integrations.
Supported libraries
Auto-instrumented via instrumentations: [...] (install the matching OpenInference peer dependency):
| Key | Library |
|---|---|
openai | openai |
anthropic | @anthropic-ai/sdk |
bedrock | @aws-sdk/client-bedrock-runtime |
langchain | @langchain/core (covers LangGraph) |
mcp | @modelcontextprotocol/sdk |
beeai | beeai-framework |
claude_agent_sdk | @anthropic-ai/claude-agent-sdk |
google_genai | @google/genai (custom instrumentor) |
mastra | Mastra (custom instrumentor) |
ai_sdk | Vercel AI SDK — opt in via wrapAISDK(ai) |
HTTP auto-instrumentation (fetch/undici) is always enabled for trace-context propagation — you don't list it. Frameworks that don't auto-instrument on init use the dedicated wrappers above.
init() reference
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | NEATLOGS_API_KEY env | Project API key. Export disabled if unset. |
workflowName | string | from process.argv[1] | Label all traces appear under. |
instrumentations | string[] | — | Libraries to auto-instrument. |
tags | string[] | — | Tags on all spans. |
sessionId | string | — | Group multi-turn traces under one session. |
autoSession | boolean | false | Auto-generate a session ID. |
endUserId | string | — | Your app's end-user on every trace (process-global default; set per-request via trace() on a server). |
endUserMetadata | Record<string, any> | — | Arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). |
metadata | Record<string, any> | — | Custom metadata on all spans. |
captureLogs | boolean | false | Enable log() capture. |
traceContent | boolean | true | Capture input/output content. |
sampleRate | number | 1.0 | Fraction of traces to export. |
mask | MaskFunction | — | Global client-side redaction. |
piiEnabled / piiSpanTypes | — | team setting | Override server-side PII redaction. |
batchSize | number | 100 | Max spans per export batch. |
flushInterval | number | 5 | Seconds between batch flushes. |
debug | boolean | false | Verbose logging. |
Lifecycle: await flush() exports buffered spans now; await shutdown() flushes and shuts down (resets state so init() can run again). Env vars: NEATLOGS_API_KEY, NEATLOGS_DISABLE_EXPORT, NEATLOGS_TRACE_CONTENT.
For PII redaction concepts see PII Redaction; to attach your app's users to traces see End-User Identity. Building in Python? See the Python SDK.