trodo-node
v2.4.3
Published
Trodo Analytics SDK for Node.js — server-side event tracking
Downloads
1,509
Maintainers
Readme
trodo-node
Server-side Node.js SDK for Trodo Analytics. Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same siteId.
Installation
npm install trodo-nodeNode 18+ uses native fetch. For Node 16/17, install the optional peer dependency:
npm install trodo-node node-fetchOpenTelemetry / OTLP path (NEW in 2.4.0)
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
# .env.local
OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${TRODO_SITE_ID}The Bearer token is your site_id — same value you'd pass to trodo.init({ siteId }). Get it from the Integration Manager.
Use this when:
- NextJS + Vercel AI SDK with
@vercel/otel— auto-instrumentedgenerateText/streamText/ tool calls flow into Trodo. Passexperimental_telemetry.metadata.{userId, sessionId, agentName, ...}and they map todistinct_id/conversation_id/agent_name/ customrun.metadata. - You already run OTel (Datadog, Jaeger, Honeycomb) and want Trodo as an additional destination. Install
trodo-nodeand calltrodo.registerOTel({ siteId, mode: 'otlp' })to attach our OTLP exporter without replacing your existing setup.wrapAgent/withSpan/toolthen route through OTel so auto-instrumented children share the same trace.
// instrumentation.ts (NextJS root or src/)
import { registerOTel } from 'trodo-node';
registerOTel({
siteId: process.env.TRODO_SITE_ID!,
mode: 'otlp',
});mode: 'otlp' requires these optional peer deps:
npm install @opentelemetry/api @opentelemetry/sdk-node \
@opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/resourcesThe SDK throws a friendly install hint if you call mode: 'otlp' without them.
For richer Trodo features on top (wrapAgent, feedback, trackMcp), continue with the SDK quick start below.
Quick Start
const trodo = require('trodo-node');
trodo.init({ siteId: 'your-site-id' });
// User-bound context (recommended)
const user = trodo.forUser('user-123');
await user.track('purchase_completed', { amount: 99.99, plan: 'pro' });
await user.people.set({ plan: 'pro', company: 'Acme' });
// Flush before process exit if using batching
await trodo.shutdown();// ESM
import trodo from 'trodo-node';
trodo.init({ siteId: 'your-site-id' });Core API
trodo.init(config)
Call once at app startup.
| Option | Default | Description |
|--------|---------|-------------|
| siteId | required | Your Trodo site ID |
| apiBase | https://sdkapi.trodo.ai | API base URL |
| timeout | 10000 ms | HTTP request timeout |
| retries | 2 | Retries on network/5xx errors |
| autoEvents | false | Hook uncaughtException / unhandledRejection as server_error events |
| batchEnabled | false | Queue events and flush in batches |
| batchSize | 50 | Flush when this many events are queued |
| batchFlushIntervalMs | 5000 | Also flush every N milliseconds |
| onError | — | Callback for SDK errors (silent by default) |
| debug | false | Log API calls to stderr |
trodo.forUser(distinctId, options?)
Returns a user-bound context. No API call is made until you track an event.
const user = trodo.forUser('user-123', {
sessionId: req.cookies.trodo_session, // optional: correlate with browser session
});trodo.identify(identifyId, options?)
Creates the session and fires POST /api/sdk/identify. Use to link a distinctId to an external identifier (email, DB id). Returns the user context.
const user = await trodo.identify('[email protected]', {
sessionId: req.cookies.trodo_session,
});
// distinctId is now [email protected] — merges with browser events
await user.track('login');User context methods
await user.track(eventName, properties?) // Custom event
await user.identify(identifyId) // Merge identity
await user.walletAddress(address) // Set wallet address
await user.reset() // Clear session
await user.captureError(err, severity?) // Track server_error ('critical' | 'error' | 'warning')
// People profile
await user.people.set(properties)
await user.people.setOnce(properties)
await user.people.unset(keys)
await user.people.increment(key, amount?)
await user.people.append(key, values)
await user.people.union(key, values)
await user.people.remove(key, values)
await user.people.trackCharge(amount, properties?)
await user.people.clearCharges()
await user.people.deleteUser()
// Groups
await user.set_group(groupKey, groupId)
await user.add_group(groupKey, groupId)
await user.remove_group(groupKey, groupId)
const group = user.get_group(groupKey, groupId)
await group.set(properties)
await group.set_once(properties)
await group.increment(key, amount?)
await group.append(key, values)
await group.union(key, values)
await group.remove(key, values)
await group.unset(keys)
await group.delete()Direct call pattern
await trodo.track('user-123', 'event_name', { key: 'value' })
await trodo.people.set('user-123', { plan: 'pro' })
await trodo.set_group('user-123', 'company', 'acme')AI Agent Tracing (recommended)
One wrap around your agent captures every LLM call, tool call, and
nested step as a tree of spans — token counts, costs, inputs, outputs,
errors. Works with any stack: OpenAI, Anthropic, LangChain, Vercel AI
SDK, raw HTTP, custom tools. Cost is derived server-side from
(provider, model) — the SDK only sends tokens.
30-second quickstart
import trodo from 'trodo-node';
trodo.init({ siteId: 'your-site-id' }); // autoInstrument on by default
const { result, runId } = await trodo.wrapAgent(
'customer-support',
async (run) => {
run.setInput({ query });
const answer = await agent.run(query); // OpenAI/Anthropic/LangChain auto-captured
run.setOutput(answer);
return answer;
},
{ distinctId: userId, conversationId: sessionId },
);Open the Agent Runs dashboard — the row shows tokens in/out, cost, span count, tool count, error count, plus the full trace tree.
Auto-instrumentation
trodo.init() calls enableAutoInstrument() which registers every
installed OpenTelemetry instrumentor — no extra wiring.
| Framework | Install |
|-----------|---------|
| OpenAI | npm i @opentelemetry/instrumentation-openai |
| Anthropic | npm i @opentelemetry/instrumentation-anthropic |
| LangChain | npm i @opentelemetry/instrumentation-langchain |
| LlamaIndex | npm i @opentelemetry/instrumentation-llamaindex |
| Google Gemini | npm i @opentelemetry/instrumentation-google-generativeai |
| Vertex AI | npm i @opentelemetry/instrumentation-vertexai |
| Bedrock | npm i @opentelemetry/instrumentation-bedrock |
| Cohere | npm i @opentelemetry/instrumentation-cohere |
| Vercel AI SDK | emits OTel via experimental_telemetry: { isEnabled: true } |
| http / fetch | bundled — generic HTTP spans for raw-HTTP callers |
Opt out with trodo.init({ siteId, autoInstrument: false }).
Span helpers
Typed function wrappers for custom code — every call becomes a span
with args auto-captured as input, return value as output,
exception as error.
// trace — generic span
const prepared = trodo.trace('prepare', async (payload) => normalize(payload));
await prepared({ raw: true });
// tool — tool span (name-first OR fn-first)
const runFunnel = trodo.tool('run_funnel_query', async (teamId, preset) => {
return await db.funnel(teamId, preset);
});
await runFunnel(1, 'day7');
// llm — LLM span, auto-extracts OpenAI / Anthropic / Gemini usage
const answer = trodo.llm('answer', async (messages) => callOpenAI(messages), {
model: 'gpt-4o-mini',
provider: 'openai',
});
await answer([{ role: 'user', content: 'ping' }]);
// Records inputTokens / outputTokens from response.usage.
// retrieval — vector search / RAG retriever span
const search = trodo.retrieval('vector_search', async (q) => vecDb.query(q));
const docs = await search('users dropping off');Raw-HTTP escape hatches
If your LLM client isn't OTel-instrumented and you can't wrap it as a function, record a span post-hoc:
const resp = await fetch(url, { body: JSON.stringify(body) }).then(r => r.json());
await trodo.trackLlmCall({
model: 'gemini-2.5-flash',
provider: 'google',
inputTokens: resp.usageMetadata.promptTokenCount,
outputTokens: resp.usageMetadata.candidatesTokenCount,
prompt: body,
completion: resp,
});For advanced cases, get a raw OTel tracer — the Trodo processor is already subscribed:
const tracer = trodo.getTracer('my.module');
tracer.startActiveSpan('custom', (span) => {
span.setAttribute('gen_ai.system', 'my-llm');
span.end();
});Cross-service runs
When one service calls another, the downstream service joins the caller's run instead of creating its own — all spans nest under a single timeline.
// Caller — outbound:
await fetch(url, {
method: 'POST',
headers: { ...trodo.propagationHeaders(), 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
// Downstream (Express):
import express from 'express';
const app = express();
app.use(trodo.expressMiddleware());
// Every LLM call / tool / trace helper inside handlers now nests under
// the caller's run.
// Or manually:
await trodo.joinRun(
req.headers['x-trodo-run-id'] as string,
req.headers['x-trodo-parent-span-id'] as string,
async () => { /* ... */ },
);Long-lived sessions across processes — startRun / endRun
wrapAgent is a single-callback block — it opens and closes the run in
one function call. For sessions that live across many HTTP requests (an
MCP server, a websocket-pinned chat, scheduled jobs that resume on
different workers), use startRun to open the run from one process and
endRun to finalise it later. Between the two, any process can use
joinRun to add child spans. Same runId threads through everything.
// Process A — open the run for an MCP session.
const runId = await trodo.startRun('external_mcp_session', {
distinctId: String(userId),
conversationId: mcpSessionId,
});
await redis.set(`mcp:run:${mcpSessionId}`, runId, 'EX', 3600);
// Process B (later, possibly a different worker) — append a tool span.
const runId = await redis.get(`mcp:run:${mcpSessionId}`);
await trodo.joinRun(runId, null, async (span) => {
span.setInput(args);
span.setOutput(result);
}, { name: 'tool.run_funnel_query', kind: 'tool' });
// When the session ends (timeout sweeper, explicit close):
await trodo.endRun(runId, { status: 'ok' });Conversation binding & feedback
const { runId } = await trodo.wrapAgent(
'chat',
async (run) => { /* ... */ },
{ distinctId: userId, conversationId: sessionId },
);
await trodo.feedback(runId, { satisfaction: 'positive', rating: 5 });Agent Analytics (legacy event-based API)
The older per-event API below is still supported but superseded by
wrapAgent + span helpers above. Use it only if you're already wired
into it; new integrations should prefer the tracing API.
Before you start: register your agent in Integrations → AI Agents in the dashboard to get an agent_id (agt_xxxxxxxx).
track_agent_call — inbound message / LLM invocation
await trodo.track_agent_call({
agentId: 'agt_abc12345',
conversationId: 'conv_xyz',
messageId: 'msg_001',
prompt: userMessage,
model: 'gpt-4o',
provider: 'openai',
systemPromptVersion: 'v2', // optional — track prompt iterations
distinctId: userId, // optional — link to a Trodo user
metadata: { threadSource: 'slack', locale: 'en' }, // optional — stored in agent_calls.metadata (JSONB)
});track_tool_use — tool/function call within a turn
await trodo.track_tool_use({
agentId: 'agt_abc12345',
conversationId: 'conv_xyz',
messageId: 'msg_001',
toolName: 'fetch_billing_info',
latencyMs: 143,
status: 'success', // 'success' | 'failure'
input: { userId: '123' }, // optional
output: { plan: 'pro' }, // optional
});track_agent_response — LLM output and token usage
await trodo.track_agent_response({
agentId: 'agt_abc12345',
conversationId: 'conv_xyz',
messageId: 'msg_001',
model: 'gpt-4o',
completionTokens: response.usage.completion_tokens,
promptTokens: response.usage.prompt_tokens,
totalTokens: response.usage.total_tokens,
finishReason: response.choices[0].finish_reason,
distinctId: userId,
});track_agent_error — errors and failures
await trodo.track_agent_error({
agentId: 'agt_abc12345',
conversationId: 'conv_xyz',
messageId: 'msg_001',
errorType: 'rate_limit', // 'timeout' | 'rate_limit' | 'guardrail_block' | ...
errorMessage: err.message,
failedTool: 'fetch_billing_info', // optional
});track_feedback — user thumbs up/down
await trodo.track_feedback({
agentId: 'agt_abc12345',
conversationId: 'conv_xyz',
messageId: 'msg_001', // same messageId as the response it refers to
feedback: 'positive', // 'positive' | 'negative' | 'unreact'
distinctId: userId,
});Full turn example
async function runAgentTurn(userId, conversationId, userMessage) {
const agentId = 'agt_abc12345';
const messageId = `msg_${Date.now()}`;
await trodo.track_agent_call({ agentId, conversationId, messageId, prompt: userMessage, distinctId: userId });
try {
await trodo.track_tool_use({ agentId, conversationId, messageId, toolName: 'search', status: 'success', latencyMs: 80 });
const response = await llm.complete(userMessage);
await trodo.track_agent_response({
agentId, conversationId, messageId,
model: response.model,
completionTokens: response.usage.completion_tokens,
promptTokens: response.usage.prompt_tokens,
totalTokens: response.usage.total_tokens,
distinctId: userId,
});
return response.text;
} catch (err) {
await trodo.track_agent_error({ agentId, conversationId, messageId, errorType: err.type, errorMessage: err.message, distinctId: userId });
throw err;
}
}Identity Merging (Cross-SDK)
Call identify() with the same value on the browser and server to merge all events under one user profile:
// Browser
Trodo.identify('[email protected]'); // → [email protected]
// Node.js (same value)
await user.identify('[email protected]'); // → [email protected]
// Events from both sides now appear together in the dashboardBatching
trodo.init({
siteId: 'your-site-id',
batchEnabled: true,
batchSize: 50,
batchFlushIntervalMs: 5000,
});
// Always flush before process exit
process.on('SIGTERM', async () => { await trodo.shutdown(); process.exit(0); });Auto Events
trodo.init({ siteId: 'your-site-id', autoEvents: true });
// Hooks process.on('uncaughtException') and process.on('unhandledRejection')
// Sends server_error events with distinct_id: 'server_global'
// Toggle at runtime
trodo.enableAutoEvents();
trodo.disableAutoEvents();TypeScript
Full type declarations bundled:
import trodo, { TrodoClient, UserContext } from 'trodo-node';
import type { AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps } from 'trodo-node';
// Multi-tenant
const client = new TrodoClient({ siteId: 'your-site-id' });License
ISC
