npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

trodo-node

v2.4.3

Published

Trodo Analytics SDK for Node.js — server-side event tracking

Downloads

1,509

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-node

Node 18+ uses native fetch. For Node 16/17, install the optional peer dependency:

npm install trodo-node node-fetch

OpenTelemetry / 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-instrumented generateText / streamText / tool calls flow into Trodo. Pass experimental_telemetry.metadata.{userId, sessionId, agentName, ...} and they map to distinct_id / conversation_id / agent_name / custom run.metadata.
  • You already run OTel (Datadog, Jaeger, Honeycomb) and want Trodo as an additional destination. Install trodo-node and call trodo.registerOTel({ siteId, mode: 'otlp' }) to attach our OTLP exporter without replacing your existing setup. wrapAgent / withSpan / tool then 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/resources

The 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 dashboard

Batching

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