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@latest

Requires 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 patched

Forgetting 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

OptionDescription
kindRequired. The span kind (see below).
nameSpan label. Defaults to the function name.
captureInput / captureOutputCapture args / return value (default true).
role, goalAgent role and objective (kind: 'AGENT').
toolName, parametersTool identifier and parameter schema (kind: 'TOOL').
model, dimensionEmbedding model and dimension (kind: 'EMBEDDING').
maskPer-span redaction function.

To omit content globally, set traceContent: false in init() (or NEATLOGS_TRACE_CONTENT=false).

Span kinds

span() accepts these eight kinds:

KindUse for
WORKFLOWTop-level entry point — one per trace root
AGENTA reasoning step that calls an LLM and decides what to do next
CHAINA fixed sequence of steps with no branching LLM decisions
TOOLA function the agent calls to interact with the world
RETRIEVERA vector search or document lookup
EMBEDDINGA function that produces embeddings
GUARDRAILA safety or validation check
MCP_TOOLA 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) (from neatlogs/ai) — the Vercel AI SDK (generateText, streamText, …).
  • wrapMastra(entity) (from neatlogs/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):

KeyLibrary
openaiopenai
anthropic@anthropic-ai/sdk
bedrock@aws-sdk/client-bedrock-runtime
langchain@langchain/core (covers LangGraph)
mcp@modelcontextprotocol/sdk
beeaibeeai-framework
claude_agent_sdk@anthropic-ai/claude-agent-sdk
google_genai@google/genai (custom instrumentor)
mastraMastra (custom instrumentor)
ai_sdkVercel 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

OptionTypeDefaultDescription
apiKeystringNEATLOGS_API_KEY envProject API key. Export disabled if unset.
workflowNamestringfrom process.argv[1]Label all traces appear under.
instrumentationsstring[]Libraries to auto-instrument.
tagsstring[]Tags on all spans.
sessionIdstringGroup multi-turn traces under one session.
autoSessionbooleanfalseAuto-generate a session ID.
endUserIdstringYour app's end-user on every trace (process-global default; set per-request via trace() on a server).
endUserMetadataRecord<string, any>Arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }).
metadataRecord<string, any>Custom metadata on all spans.
captureLogsbooleanfalseEnable log() capture.
traceContentbooleantrueCapture input/output content.
sampleRatenumber1.0Fraction of traces to export.
maskMaskFunctionGlobal client-side redaction.
piiEnabled / piiSpanTypesteam settingOverride server-side PII redaction.
batchSizenumber100Max spans per export batch.
flushIntervalnumber5Seconds between batch flushes.
debugbooleanfalseVerbose 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.

On this page