@tracelanedev/sdk

Instrumentation for TypeScript AI agents, built on the OpenTelemetry Node SDK.
Spans are emitted via OTLP/HTTP (JSON) to the endpoint you configure —
https://gateway.tracelane.dev on Tracelane Cloud, or a receiver you run.
Install
npm install @tracelanedev/sdk
pnpm add @tracelanedev/sdk
Fastest path — route through the gateway (no SDK)
For Tracelane Cloud, the shortest path to your first trace needs no SDK at
all: point your existing client's base URL at the gateway and use your
tlane_… key. The gateway routes the call and captures the trace.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://gateway.tracelane.dev/v1",
apiKey: process.env.TRACELANE_API_KEY!,
});
await client.chat.completions.create({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "Hello" }],
});
That captures the model call. Use this SDK when you want the shape of your
agent — the planner step, each tool call, the summariser — as a nested trace
rather than one span per model call. It exports OTLP to Tracelane Cloud's
gateway, a self-hosted Tracelane ingest, or your own collector (Jaeger, Tempo, …).
Sessions — group turns into one conversation
/sessions groups traces by conversation
id. The id travels as the x-conversation-id request header (the gateway also
accepts x-session-id), and it lands on the span as gen_ai.conversation.id.
This SDK is what sets it — an un-sessioned call sends no session header and
carries no conversation id.
Wrap the turn and every call inside the scope joins the session:
import { instrumentOpenAI, withSession } from "@tracelanedev/sdk";
instrumentOpenAI(client);
await withSession(conversationId, async () => {
await client.chat.completions.create({ model: "claude-sonnet-4-6", messages });
await client.chat.completions.create({ model: "claude-sonnet-4-6", messages: followUp });
});
withSession is backed by AsyncLocalStorage, so overlapping conversations in
one server process never bleed into each other. For a single-conversation script
or CLI, setSession(id) sets it process-wide instead, and getSession() reads
back whichever is active.
Auto-attach covers the four adapters that wrap a request-options argument:
instrumentOpenAI, instrumentAnthropic, instrumentLiteLLM and
instrumentOpenRouter. For every other client — including the no-SDK gateway
path above — pass the header yourself:
import { sessionHeaders } from "@tracelanedev/sdk";
await client.chat.completions.create(
{ model: "claude-sonnet-4-6", messages },
{ headers: sessionHeaders(conversationId) },
);
sessionHeaders() with no argument returns the active session, or {} when
there is none, so it is always safe to spread. A header you set explicitly always
wins over the ambient session.
A session id must be non-empty visible ASCII, at most 256 characters. Anything
else throws at the call you wrote, rather than being dropped in transit and
leaving the session silently empty — and it is rejected, never truncated, because
a truncated id would split one conversation into two.
SDK quick start (OTLP export)
Two steps: init() once at startup, then wrap each client with its
instrument* function. There is no zero-config magic in v1 — wrapping is
explicit, so what's traced is exactly what you opted in.
import { init, instrumentAnthropic } from "@tracelanedev/sdk";
import Anthropic from "@anthropic-ai/sdk";
init({
endpoint: "https://gateway.tracelane.dev",
apiKey: process.env.TRACELANE_API_KEY!,
serviceName: "my-agent",
});
const client = new Anthropic();
instrumentAnthropic(client);
await client.messages.create({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 128,
});
Your key needs the ingest scope. Keys minted before scopes existed carry
the full API surface and work as-is. A key scoped to chat and read does
not — the gateway answers 403 insufficient_scope and names ingest in the
body. Tick Ingest in Settings → API Keys; it is on by default for new
keys.
init() options
endpoint | yes | OTLP HTTP endpoint. https://gateway.tracelane.dev for Cloud, or a receiver you run (e.g. http://localhost:4318). Spans POST to ${endpoint}/v1/traces. |
apiKey | yes | Your tlane_… key. Sent as the x-tracelane-api-key header. |
serviceName | no | Resource service.name (default unknown-service). |
sampleRate | no | 0.0–1.0 (default 1.0 — full trace; the tail sampler decides). |
Call shutdown() on exit to flush pending spans (an automatic flush is also
registered on beforeExit).
Streaming (v1 limitation)
Streamed calls (stream: true) pass through untouched and still produce a
span with model + latency, marked tracelane.streaming = true. Token usage
and finish reason are not captured for streamed responses yet — that
is not implemented. A once-per-process runtime warning says exactly this.
Instrumented libraries
Each library has its own instrument*(client) function — import it from the
package root or the matching subpath. Call it once, after constructing the
client (or, for module-level libraries, after import).
@anthropic-ai/sdk | instrumentAnthropic(client) | messages.create, streaming, tool use |
openai | instrumentOpenAI(client) | chat.completions, embeddings, Responses |
@openai/agents | instrumentOpenAIAgents(...) | agent steps, tool calls, handoffs |
langchain | instrumentLangGraph(graph) | chains, agents, tool calls |
@modelcontextprotocol/sdk | instrumentMCP(...) | tool_call, tool_result |
| Vercel AI SDK | instrumentVercelAI(...) | generateText, streamText, generateObject |
Full list (one export per library): instrumentAnthropic, instrumentOpenAI,
instrumentOpenAIAsync, instrumentLiteLLM, instrumentOpenRouter,
instrumentLangGraph, instrumentOpenAIAgents, instrumentVercelAI,
instrumentMCP, instrumentClaudeCode, instrumentCursor,
instrumentPinecone, instrumentQdrant, instrumentComposio,
instrumentBrowserbase, instrumentE2B, instrumentMem0, instrumentLetta,
instrumentFirecrawl.
Zero-config autoInstrument() is not in v1 — calling it throws with a
pointer to this explicit API. Auto-detection is not implemented.
Next.js App Router
Initialise in instrumentation.ts (runs once per server process):
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { init } = await import("@tracelanedev/sdk");
init({
endpoint: "http://localhost:4318",
apiKey: process.env.TRACELANE_API_KEY!,
serviceName: "my-nextjs-app",
});
}
}
Manual spans
The SDK sets up a standard OpenTelemetry tracer provider, so custom spans use
@opentelemetry/api directly — no Tracelane-specific wrapper:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("my-agent");
const hits = await tracer.startActiveSpan("retrieval", async (span) => {
span.setAttribute("retrieval.top_k", 10);
const results = await vectorStore.search(query, { topK: 10 });
span.end();
return results;
});
Design invariants
- Telemetry goes to your configured
endpoint only — the SDK never calls home.
- Instrumentation is additive —
instrument* patches a client in place and does
not modify the OpenAI/Anthropic module exports.
- Prompt and completion text is not captured, and there is no switch you need
to set. The SDK never sends it, and the gateway stores it only for tenants an
operator has explicitly allowlisted in its own
tracelane.yaml — off for
everyone otherwise. Earlier revisions of this file told you to set
TRACELANE_TRACE_CONTENT=false; no component has ever read that variable,
so the instruction was inert and the honest statement is the stronger one.
- Zero runtime dependencies beyond the OpenTelemetry SDK.
Documentation
Full docs at docs.tracelane.dev/sdk-typescript.
Stack
TypeScript 5.5+ strict, Biome (lint + format), Vitest.
License
Apache 2.0 — see LICENSE.