trodo-node 2.4.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +513 -513
- package/dist/cjs/otel/helpers.js.map +1 -1
- package/dist/cjs/otel/wrapAgent.js +91 -22
- package/dist/cjs/otel/wrapAgent.js.map +1 -1
- package/dist/esm/otel/helpers.js.map +1 -1
- package/dist/esm/otel/processor.d.ts +4 -4
- package/dist/esm/otel/processor.d.ts.map +1 -1
- package/dist/esm/otel/wrapAgent.d.ts +13 -7
- package/dist/esm/otel/wrapAgent.d.ts.map +1 -1
- package/dist/esm/otel/wrapAgent.js +91 -22
- package/dist/esm/otel/wrapAgent.js.map +1 -1
- package/package.json +21 -9
package/README.md
CHANGED
|
@@ -1,513 +1,513 @@
|
|
|
1
|
-
# trodo-node
|
|
2
|
-
|
|
3
|
-
Server-side Node.js SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same `siteId`.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install trodo-node
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Node 18+ uses native `fetch`. For Node 16/17, install the optional peer dependency:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install trodo-node node-fetch
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## OpenTelemetry / OTLP path (NEW in 2.4.0)
|
|
18
|
-
|
|
19
|
-
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# .env.local
|
|
23
|
-
OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
|
|
24
|
-
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${TRODO_SITE_ID}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The Bearer token is your **site_id** — same value you'd pass to `trodo.init({ siteId })`. Get it from the [Integration Manager](https://app.trodo.ai/dashboard/integrations).
|
|
28
|
-
|
|
29
|
-
Use this when:
|
|
30
|
-
- **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`.
|
|
31
|
-
- **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.
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
// instrumentation.ts (NextJS root or src/)
|
|
35
|
-
import { registerOTel } from 'trodo-node';
|
|
36
|
-
|
|
37
|
-
registerOTel({
|
|
38
|
-
siteId: process.env.TRODO_SITE_ID!,
|
|
39
|
-
mode: 'otlp',
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`mode: 'otlp'` requires these optional peer deps:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npm install @opentelemetry/api @opentelemetry/sdk-node \
|
|
47
|
-
@opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto \
|
|
48
|
-
@opentelemetry/resources
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The SDK throws a friendly install hint if you call `mode: 'otlp'` without them.
|
|
52
|
-
|
|
53
|
-
For richer Trodo features on top (`wrapAgent`, `feedback`, `trackMcp`), continue with the SDK quick start below.
|
|
54
|
-
|
|
55
|
-
## Quick Start
|
|
56
|
-
|
|
57
|
-
```javascript
|
|
58
|
-
const trodo = require('trodo-node');
|
|
59
|
-
|
|
60
|
-
trodo.init({ siteId: 'your-site-id' });
|
|
61
|
-
|
|
62
|
-
// User-bound context (recommended)
|
|
63
|
-
const user = trodo.forUser('user-123');
|
|
64
|
-
await user.track('purchase_completed', { amount: 99.99, plan: 'pro' });
|
|
65
|
-
await user.people.set({ plan: 'pro', company: 'Acme' });
|
|
66
|
-
|
|
67
|
-
// Flush before process exit if using batching
|
|
68
|
-
await trodo.shutdown();
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
// ESM
|
|
73
|
-
import trodo from 'trodo-node';
|
|
74
|
-
trodo.init({ siteId: 'your-site-id' });
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Core API
|
|
78
|
-
|
|
79
|
-
### `trodo.init(config)`
|
|
80
|
-
|
|
81
|
-
Call once at app startup.
|
|
82
|
-
|
|
83
|
-
| Option | Default | Description |
|
|
84
|
-
|--------|---------|-------------|
|
|
85
|
-
| `siteId` | required | Your Trodo site ID |
|
|
86
|
-
| `apiBase` | `https://sdkapi.trodo.ai` | API base URL |
|
|
87
|
-
| `timeout` | `10000` ms | HTTP request timeout |
|
|
88
|
-
| `retries` | `2` | Retries on network/5xx errors |
|
|
89
|
-
| `autoEvents` | `false` | Hook `uncaughtException` / `unhandledRejection` as `server_error` events |
|
|
90
|
-
| `batchEnabled` | `false` | Queue events and flush in batches |
|
|
91
|
-
| `batchSize` | `50` | Flush when this many events are queued |
|
|
92
|
-
| `batchFlushIntervalMs` | `5000` | Also flush every N milliseconds |
|
|
93
|
-
| `onError` | — | Callback for SDK errors (silent by default) |
|
|
94
|
-
| `debug` | `false` | Log API calls to stderr |
|
|
95
|
-
|
|
96
|
-
### `trodo.forUser(distinctId, options?)`
|
|
97
|
-
|
|
98
|
-
Returns a user-bound context. No API call is made until you track an event.
|
|
99
|
-
|
|
100
|
-
```javascript
|
|
101
|
-
const user = trodo.forUser('user-123', {
|
|
102
|
-
sessionId: req.cookies.trodo_session, // optional: correlate with browser session
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### `trodo.identify(identifyId, options?)`
|
|
107
|
-
|
|
108
|
-
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.
|
|
109
|
-
|
|
110
|
-
```javascript
|
|
111
|
-
const user = await trodo.identify('user@example.com', {
|
|
112
|
-
sessionId: req.cookies.trodo_session,
|
|
113
|
-
});
|
|
114
|
-
// distinctId is now id_user@example.com — merges with browser events
|
|
115
|
-
await user.track('login');
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### User context methods
|
|
119
|
-
|
|
120
|
-
```javascript
|
|
121
|
-
await user.track(eventName, properties?) // Custom event
|
|
122
|
-
await user.identify(identifyId) // Merge identity
|
|
123
|
-
await user.walletAddress(address) // Set wallet address
|
|
124
|
-
await user.reset() // Clear session
|
|
125
|
-
await user.captureError(err, severity?) // Track server_error ('critical' | 'error' | 'warning')
|
|
126
|
-
|
|
127
|
-
// People profile
|
|
128
|
-
await user.people.set(properties)
|
|
129
|
-
await user.people.setOnce(properties)
|
|
130
|
-
await user.people.unset(keys)
|
|
131
|
-
await user.people.increment(key, amount?)
|
|
132
|
-
await user.people.append(key, values)
|
|
133
|
-
await user.people.union(key, values)
|
|
134
|
-
await user.people.remove(key, values)
|
|
135
|
-
await user.people.trackCharge(amount, properties?)
|
|
136
|
-
await user.people.clearCharges()
|
|
137
|
-
await user.people.deleteUser()
|
|
138
|
-
|
|
139
|
-
// Groups
|
|
140
|
-
await user.set_group(groupKey, groupId)
|
|
141
|
-
await user.add_group(groupKey, groupId)
|
|
142
|
-
await user.remove_group(groupKey, groupId)
|
|
143
|
-
const group = user.get_group(groupKey, groupId)
|
|
144
|
-
await group.set(properties)
|
|
145
|
-
await group.set_once(properties)
|
|
146
|
-
await group.increment(key, amount?)
|
|
147
|
-
await group.append(key, values)
|
|
148
|
-
await group.union(key, values)
|
|
149
|
-
await group.remove(key, values)
|
|
150
|
-
await group.unset(keys)
|
|
151
|
-
await group.delete()
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Direct call pattern
|
|
155
|
-
|
|
156
|
-
```javascript
|
|
157
|
-
await trodo.track('user-123', 'event_name', { key: 'value' })
|
|
158
|
-
await trodo.people.set('user-123', { plan: 'pro' })
|
|
159
|
-
await trodo.set_group('user-123', 'company', 'acme')
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## AI Agent Tracing (recommended)
|
|
165
|
-
|
|
166
|
-
One wrap around your agent captures every LLM call, tool call, and
|
|
167
|
-
nested step as a tree of spans — token counts, costs, inputs, outputs,
|
|
168
|
-
errors. Works with any stack: OpenAI, Anthropic, LangChain, Vercel AI
|
|
169
|
-
SDK, raw HTTP, custom tools. Cost is derived server-side from
|
|
170
|
-
`(provider, model)` — the SDK only sends tokens.
|
|
171
|
-
|
|
172
|
-
### 30-second quickstart
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
import trodo from 'trodo-node';
|
|
176
|
-
trodo.init({ siteId: 'your-site-id' }); // autoInstrument on by default
|
|
177
|
-
|
|
178
|
-
const { result, runId } = await trodo.wrapAgent(
|
|
179
|
-
'customer-support',
|
|
180
|
-
async (run) => {
|
|
181
|
-
run.setInput({ query });
|
|
182
|
-
const answer = await agent.run(query); // OpenAI/Anthropic/LangChain auto-captured
|
|
183
|
-
run.setOutput(answer);
|
|
184
|
-
return answer;
|
|
185
|
-
},
|
|
186
|
-
{ distinctId: userId, conversationId: sessionId },
|
|
187
|
-
);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Open the Agent Runs dashboard — the row shows tokens in/out, cost,
|
|
191
|
-
span count, tool count, error count, plus the full trace tree.
|
|
192
|
-
|
|
193
|
-
### Auto-instrumentation
|
|
194
|
-
|
|
195
|
-
`trodo.init()` calls `enableAutoInstrument()` which registers every
|
|
196
|
-
installed OpenTelemetry instrumentor — no extra wiring.
|
|
197
|
-
|
|
198
|
-
| Framework | Install |
|
|
199
|
-
|-----------|---------|
|
|
200
|
-
| OpenAI | `npm i @opentelemetry/instrumentation-openai` |
|
|
201
|
-
| Anthropic | `npm i @opentelemetry/instrumentation-anthropic` |
|
|
202
|
-
| LangChain | `npm i @opentelemetry/instrumentation-langchain` |
|
|
203
|
-
| LlamaIndex | `npm i @opentelemetry/instrumentation-llamaindex` |
|
|
204
|
-
| Google Gemini | `npm i @opentelemetry/instrumentation-google-generativeai` |
|
|
205
|
-
| Vertex AI | `npm i @opentelemetry/instrumentation-vertexai` |
|
|
206
|
-
| Bedrock | `npm i @opentelemetry/instrumentation-bedrock` |
|
|
207
|
-
| Cohere | `npm i @opentelemetry/instrumentation-cohere` |
|
|
208
|
-
| Vercel AI SDK | emits OTel via `experimental_telemetry: { isEnabled: true }` |
|
|
209
|
-
| http / fetch | bundled — generic HTTP spans for raw-HTTP callers |
|
|
210
|
-
|
|
211
|
-
Opt out with `trodo.init({ siteId, autoInstrument: false })`.
|
|
212
|
-
|
|
213
|
-
### Span helpers
|
|
214
|
-
|
|
215
|
-
Typed function wrappers for custom code — every call becomes a span
|
|
216
|
-
with args auto-captured as `input`, return value as `output`,
|
|
217
|
-
exception as `error`.
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// trace — generic span
|
|
221
|
-
const prepared = trodo.trace('prepare', async (payload) => normalize(payload));
|
|
222
|
-
await prepared({ raw: true });
|
|
223
|
-
|
|
224
|
-
// tool — tool span (name-first OR fn-first)
|
|
225
|
-
const runFunnel = trodo.tool('run_funnel_query', async (teamId, preset) => {
|
|
226
|
-
return await db.funnel(teamId, preset);
|
|
227
|
-
});
|
|
228
|
-
await runFunnel(1, 'day7');
|
|
229
|
-
|
|
230
|
-
// llm — LLM span, auto-extracts OpenAI / Anthropic / Gemini usage
|
|
231
|
-
const answer = trodo.llm('answer', async (messages) => callOpenAI(messages), {
|
|
232
|
-
model: 'gpt-4o-mini',
|
|
233
|
-
provider: 'openai',
|
|
234
|
-
});
|
|
235
|
-
await answer([{ role: 'user', content: 'ping' }]);
|
|
236
|
-
// Records inputTokens / outputTokens from response.usage.
|
|
237
|
-
|
|
238
|
-
// retrieval — vector search / RAG retriever span
|
|
239
|
-
const search = trodo.retrieval('vector_search', async (q) => vecDb.query(q));
|
|
240
|
-
const docs = await search('users dropping off');
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Raw-HTTP escape hatches
|
|
244
|
-
|
|
245
|
-
If your LLM client isn't OTel-instrumented and you can't wrap it as a
|
|
246
|
-
function, record a span post-hoc:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
const resp = await fetch(url, { body: JSON.stringify(body) }).then(r => r.json());
|
|
250
|
-
await trodo.trackLlmCall({
|
|
251
|
-
model: 'gemini-2.5-flash',
|
|
252
|
-
provider: 'google',
|
|
253
|
-
inputTokens: resp.usageMetadata.promptTokenCount,
|
|
254
|
-
outputTokens: resp.usageMetadata.candidatesTokenCount,
|
|
255
|
-
prompt: body,
|
|
256
|
-
completion: resp,
|
|
257
|
-
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
For advanced cases, get a raw OTel tracer — the Trodo processor is
|
|
261
|
-
already subscribed:
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
const tracer = trodo.getTracer('my.module');
|
|
265
|
-
tracer.startActiveSpan('custom', (span) => {
|
|
266
|
-
span.setAttribute('gen_ai.system', 'my-llm');
|
|
267
|
-
span.end();
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Cross-service runs
|
|
272
|
-
|
|
273
|
-
When one service calls another, the downstream service **joins** the
|
|
274
|
-
caller's run instead of creating its own — all spans nest under a
|
|
275
|
-
single timeline.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
// Caller — outbound:
|
|
279
|
-
await fetch(url, {
|
|
280
|
-
method: 'POST',
|
|
281
|
-
headers: { ...trodo.propagationHeaders(), 'content-type': 'application/json' },
|
|
282
|
-
body: JSON.stringify(payload),
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// Downstream (Express):
|
|
286
|
-
import express from 'express';
|
|
287
|
-
const app = express();
|
|
288
|
-
app.use(trodo.expressMiddleware());
|
|
289
|
-
// Every LLM call / tool / trace helper inside handlers now nests under
|
|
290
|
-
// the caller's run.
|
|
291
|
-
|
|
292
|
-
// Or manually:
|
|
293
|
-
await trodo.joinRun(
|
|
294
|
-
req.headers['x-trodo-run-id'] as string,
|
|
295
|
-
req.headers['x-trodo-parent-span-id'] as string,
|
|
296
|
-
async () => { /* ... */ },
|
|
297
|
-
);
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Long-lived sessions across processes — `startRun` / `endRun`
|
|
301
|
-
|
|
302
|
-
`wrapAgent` is a single-callback block — it opens *and* closes the run in
|
|
303
|
-
one function call. For sessions that live across many HTTP requests (an
|
|
304
|
-
MCP server, a websocket-pinned chat, scheduled jobs that resume on
|
|
305
|
-
different workers), use `startRun` to open the run from one process and
|
|
306
|
-
`endRun` to finalise it later. Between the two, any process can use
|
|
307
|
-
`joinRun` to add child spans. Same `runId` threads through everything.
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
// Process A — open the run for an MCP session.
|
|
311
|
-
const runId = await trodo.startRun('external_mcp_session', {
|
|
312
|
-
distinctId: String(userId),
|
|
313
|
-
conversationId: mcpSessionId,
|
|
314
|
-
});
|
|
315
|
-
await redis.set(`mcp:run:${mcpSessionId}`, runId, 'EX', 3600);
|
|
316
|
-
|
|
317
|
-
// Process B (later, possibly a different worker) — append a tool span.
|
|
318
|
-
const runId = await redis.get(`mcp:run:${mcpSessionId}`);
|
|
319
|
-
await trodo.joinRun(runId, null, async (span) => {
|
|
320
|
-
span.setInput(args);
|
|
321
|
-
span.setOutput(result);
|
|
322
|
-
}, { name: 'tool.run_funnel_query', kind: 'tool' });
|
|
323
|
-
|
|
324
|
-
// When the session ends (timeout sweeper, explicit close):
|
|
325
|
-
await trodo.endRun(runId, { status: 'ok' });
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Conversation binding & feedback
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
const { runId } = await trodo.wrapAgent(
|
|
332
|
-
'chat',
|
|
333
|
-
async (run) => { /* ... */ },
|
|
334
|
-
{ distinctId: userId, conversationId: sessionId },
|
|
335
|
-
);
|
|
336
|
-
await trodo.feedback(runId, { satisfaction: 'positive', rating: 5 });
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
## Agent Analytics (legacy event-based API)
|
|
342
|
-
|
|
343
|
-
The older per-event API below is still supported but superseded by
|
|
344
|
-
`wrapAgent` + span helpers above. Use it only if you're already wired
|
|
345
|
-
into it; new integrations should prefer the tracing API.
|
|
346
|
-
|
|
347
|
-
**Before you start:** register your agent in **Integrations → AI Agents** in the dashboard to get an `agent_id` (`agt_xxxxxxxx`).
|
|
348
|
-
|
|
349
|
-
### `track_agent_call` — inbound message / LLM invocation
|
|
350
|
-
|
|
351
|
-
```javascript
|
|
352
|
-
await trodo.track_agent_call({
|
|
353
|
-
agentId: 'agt_abc12345',
|
|
354
|
-
conversationId: 'conv_xyz',
|
|
355
|
-
messageId: 'msg_001',
|
|
356
|
-
prompt: userMessage,
|
|
357
|
-
model: 'gpt-4o',
|
|
358
|
-
provider: 'openai',
|
|
359
|
-
systemPromptVersion: 'v2', // optional — track prompt iterations
|
|
360
|
-
distinctId: userId, // optional — link to a Trodo user
|
|
361
|
-
metadata: { threadSource: 'slack', locale: 'en' }, // optional — stored in agent_calls.metadata (JSONB)
|
|
362
|
-
});
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### `track_tool_use` — tool/function call within a turn
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
await trodo.track_tool_use({
|
|
369
|
-
agentId: 'agt_abc12345',
|
|
370
|
-
conversationId: 'conv_xyz',
|
|
371
|
-
messageId: 'msg_001',
|
|
372
|
-
toolName: 'fetch_billing_info',
|
|
373
|
-
latencyMs: 143,
|
|
374
|
-
status: 'success', // 'success' | 'failure'
|
|
375
|
-
input: { userId: '123' }, // optional
|
|
376
|
-
output: { plan: 'pro' }, // optional
|
|
377
|
-
});
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### `track_agent_response` — LLM output and token usage
|
|
381
|
-
|
|
382
|
-
```javascript
|
|
383
|
-
await trodo.track_agent_response({
|
|
384
|
-
agentId: 'agt_abc12345',
|
|
385
|
-
conversationId: 'conv_xyz',
|
|
386
|
-
messageId: 'msg_001',
|
|
387
|
-
model: 'gpt-4o',
|
|
388
|
-
completionTokens: response.usage.completion_tokens,
|
|
389
|
-
promptTokens: response.usage.prompt_tokens,
|
|
390
|
-
totalTokens: response.usage.total_tokens,
|
|
391
|
-
finishReason: response.choices[0].finish_reason,
|
|
392
|
-
distinctId: userId,
|
|
393
|
-
});
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### `track_agent_error` — errors and failures
|
|
397
|
-
|
|
398
|
-
```javascript
|
|
399
|
-
await trodo.track_agent_error({
|
|
400
|
-
agentId: 'agt_abc12345',
|
|
401
|
-
conversationId: 'conv_xyz',
|
|
402
|
-
messageId: 'msg_001',
|
|
403
|
-
errorType: 'rate_limit', // 'timeout' | 'rate_limit' | 'guardrail_block' | ...
|
|
404
|
-
errorMessage: err.message,
|
|
405
|
-
failedTool: 'fetch_billing_info', // optional
|
|
406
|
-
});
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### `track_feedback` — user thumbs up/down
|
|
410
|
-
|
|
411
|
-
```javascript
|
|
412
|
-
await trodo.track_feedback({
|
|
413
|
-
agentId: 'agt_abc12345',
|
|
414
|
-
conversationId: 'conv_xyz',
|
|
415
|
-
messageId: 'msg_001', // same messageId as the response it refers to
|
|
416
|
-
feedback: 'positive', // 'positive' | 'negative' | 'unreact'
|
|
417
|
-
distinctId: userId,
|
|
418
|
-
});
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Full turn example
|
|
422
|
-
|
|
423
|
-
```javascript
|
|
424
|
-
async function runAgentTurn(userId, conversationId, userMessage) {
|
|
425
|
-
const agentId = 'agt_abc12345';
|
|
426
|
-
const messageId = `msg_${Date.now()}`;
|
|
427
|
-
|
|
428
|
-
await trodo.track_agent_call({ agentId, conversationId, messageId, prompt: userMessage, distinctId: userId });
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
await trodo.track_tool_use({ agentId, conversationId, messageId, toolName: 'search', status: 'success', latencyMs: 80 });
|
|
432
|
-
|
|
433
|
-
const response = await llm.complete(userMessage);
|
|
434
|
-
|
|
435
|
-
await trodo.track_agent_response({
|
|
436
|
-
agentId, conversationId, messageId,
|
|
437
|
-
model: response.model,
|
|
438
|
-
completionTokens: response.usage.completion_tokens,
|
|
439
|
-
promptTokens: response.usage.prompt_tokens,
|
|
440
|
-
totalTokens: response.usage.total_tokens,
|
|
441
|
-
distinctId: userId,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
return response.text;
|
|
445
|
-
} catch (err) {
|
|
446
|
-
await trodo.track_agent_error({ agentId, conversationId, messageId, errorType: err.type, errorMessage: err.message, distinctId: userId });
|
|
447
|
-
throw err;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
---
|
|
453
|
-
|
|
454
|
-
## Identity Merging (Cross-SDK)
|
|
455
|
-
|
|
456
|
-
Call `identify()` with the **same value** on the browser and server to merge all events under one user profile:
|
|
457
|
-
|
|
458
|
-
```javascript
|
|
459
|
-
// Browser
|
|
460
|
-
Trodo.identify('user@example.com'); // → id_user@example.com
|
|
461
|
-
|
|
462
|
-
// Node.js (same value)
|
|
463
|
-
await user.identify('user@example.com'); // → id_user@example.com
|
|
464
|
-
// Events from both sides now appear together in the dashboard
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
## Batching
|
|
470
|
-
|
|
471
|
-
```javascript
|
|
472
|
-
trodo.init({
|
|
473
|
-
siteId: 'your-site-id',
|
|
474
|
-
batchEnabled: true,
|
|
475
|
-
batchSize: 50,
|
|
476
|
-
batchFlushIntervalMs: 5000,
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// Always flush before process exit
|
|
480
|
-
process.on('SIGTERM', async () => { await trodo.shutdown(); process.exit(0); });
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
## Auto Events
|
|
486
|
-
|
|
487
|
-
```javascript
|
|
488
|
-
trodo.init({ siteId: 'your-site-id', autoEvents: true });
|
|
489
|
-
// Hooks process.on('uncaughtException') and process.on('unhandledRejection')
|
|
490
|
-
// Sends server_error events with distinct_id: 'server_global'
|
|
491
|
-
|
|
492
|
-
// Toggle at runtime
|
|
493
|
-
trodo.enableAutoEvents();
|
|
494
|
-
trodo.disableAutoEvents();
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## TypeScript
|
|
500
|
-
|
|
501
|
-
Full type declarations bundled:
|
|
502
|
-
|
|
503
|
-
```typescript
|
|
504
|
-
import trodo, { TrodoClient, UserContext } from 'trodo-node';
|
|
505
|
-
import type { AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps } from 'trodo-node';
|
|
506
|
-
|
|
507
|
-
// Multi-tenant
|
|
508
|
-
const client = new TrodoClient({ siteId: 'your-site-id' });
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
## License
|
|
512
|
-
|
|
513
|
-
ISC
|
|
1
|
+
# trodo-node
|
|
2
|
+
|
|
3
|
+
Server-side Node.js SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same `siteId`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install trodo-node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+ uses native `fetch`. For Node 16/17, install the optional peer dependency:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install trodo-node node-fetch
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## OpenTelemetry / OTLP path (NEW in 2.4.0)
|
|
18
|
+
|
|
19
|
+
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# .env.local
|
|
23
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
|
|
24
|
+
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${TRODO_SITE_ID}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Bearer token is your **site_id** — same value you'd pass to `trodo.init({ siteId })`. Get it from the [Integration Manager](https://app.trodo.ai/dashboard/integrations).
|
|
28
|
+
|
|
29
|
+
Use this when:
|
|
30
|
+
- **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`.
|
|
31
|
+
- **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.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// instrumentation.ts (NextJS root or src/)
|
|
35
|
+
import { registerOTel } from 'trodo-node';
|
|
36
|
+
|
|
37
|
+
registerOTel({
|
|
38
|
+
siteId: process.env.TRODO_SITE_ID!,
|
|
39
|
+
mode: 'otlp',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`mode: 'otlp'` requires these optional peer deps:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install @opentelemetry/api @opentelemetry/sdk-node \
|
|
47
|
+
@opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto \
|
|
48
|
+
@opentelemetry/resources
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The SDK throws a friendly install hint if you call `mode: 'otlp'` without them.
|
|
52
|
+
|
|
53
|
+
For richer Trodo features on top (`wrapAgent`, `feedback`, `trackMcp`), continue with the SDK quick start below.
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const trodo = require('trodo-node');
|
|
59
|
+
|
|
60
|
+
trodo.init({ siteId: 'your-site-id' });
|
|
61
|
+
|
|
62
|
+
// User-bound context (recommended)
|
|
63
|
+
const user = trodo.forUser('user-123');
|
|
64
|
+
await user.track('purchase_completed', { amount: 99.99, plan: 'pro' });
|
|
65
|
+
await user.people.set({ plan: 'pro', company: 'Acme' });
|
|
66
|
+
|
|
67
|
+
// Flush before process exit if using batching
|
|
68
|
+
await trodo.shutdown();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// ESM
|
|
73
|
+
import trodo from 'trodo-node';
|
|
74
|
+
trodo.init({ siteId: 'your-site-id' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Core API
|
|
78
|
+
|
|
79
|
+
### `trodo.init(config)`
|
|
80
|
+
|
|
81
|
+
Call once at app startup.
|
|
82
|
+
|
|
83
|
+
| Option | Default | Description |
|
|
84
|
+
|--------|---------|-------------|
|
|
85
|
+
| `siteId` | required | Your Trodo site ID |
|
|
86
|
+
| `apiBase` | `https://sdkapi.trodo.ai` | API base URL |
|
|
87
|
+
| `timeout` | `10000` ms | HTTP request timeout |
|
|
88
|
+
| `retries` | `2` | Retries on network/5xx errors |
|
|
89
|
+
| `autoEvents` | `false` | Hook `uncaughtException` / `unhandledRejection` as `server_error` events |
|
|
90
|
+
| `batchEnabled` | `false` | Queue events and flush in batches |
|
|
91
|
+
| `batchSize` | `50` | Flush when this many events are queued |
|
|
92
|
+
| `batchFlushIntervalMs` | `5000` | Also flush every N milliseconds |
|
|
93
|
+
| `onError` | — | Callback for SDK errors (silent by default) |
|
|
94
|
+
| `debug` | `false` | Log API calls to stderr |
|
|
95
|
+
|
|
96
|
+
### `trodo.forUser(distinctId, options?)`
|
|
97
|
+
|
|
98
|
+
Returns a user-bound context. No API call is made until you track an event.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const user = trodo.forUser('user-123', {
|
|
102
|
+
sessionId: req.cookies.trodo_session, // optional: correlate with browser session
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `trodo.identify(identifyId, options?)`
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const user = await trodo.identify('user@example.com', {
|
|
112
|
+
sessionId: req.cookies.trodo_session,
|
|
113
|
+
});
|
|
114
|
+
// distinctId is now id_user@example.com — merges with browser events
|
|
115
|
+
await user.track('login');
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### User context methods
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
await user.track(eventName, properties?) // Custom event
|
|
122
|
+
await user.identify(identifyId) // Merge identity
|
|
123
|
+
await user.walletAddress(address) // Set wallet address
|
|
124
|
+
await user.reset() // Clear session
|
|
125
|
+
await user.captureError(err, severity?) // Track server_error ('critical' | 'error' | 'warning')
|
|
126
|
+
|
|
127
|
+
// People profile
|
|
128
|
+
await user.people.set(properties)
|
|
129
|
+
await user.people.setOnce(properties)
|
|
130
|
+
await user.people.unset(keys)
|
|
131
|
+
await user.people.increment(key, amount?)
|
|
132
|
+
await user.people.append(key, values)
|
|
133
|
+
await user.people.union(key, values)
|
|
134
|
+
await user.people.remove(key, values)
|
|
135
|
+
await user.people.trackCharge(amount, properties?)
|
|
136
|
+
await user.people.clearCharges()
|
|
137
|
+
await user.people.deleteUser()
|
|
138
|
+
|
|
139
|
+
// Groups
|
|
140
|
+
await user.set_group(groupKey, groupId)
|
|
141
|
+
await user.add_group(groupKey, groupId)
|
|
142
|
+
await user.remove_group(groupKey, groupId)
|
|
143
|
+
const group = user.get_group(groupKey, groupId)
|
|
144
|
+
await group.set(properties)
|
|
145
|
+
await group.set_once(properties)
|
|
146
|
+
await group.increment(key, amount?)
|
|
147
|
+
await group.append(key, values)
|
|
148
|
+
await group.union(key, values)
|
|
149
|
+
await group.remove(key, values)
|
|
150
|
+
await group.unset(keys)
|
|
151
|
+
await group.delete()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Direct call pattern
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
await trodo.track('user-123', 'event_name', { key: 'value' })
|
|
158
|
+
await trodo.people.set('user-123', { plan: 'pro' })
|
|
159
|
+
await trodo.set_group('user-123', 'company', 'acme')
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## AI Agent Tracing (recommended)
|
|
165
|
+
|
|
166
|
+
One wrap around your agent captures every LLM call, tool call, and
|
|
167
|
+
nested step as a tree of spans — token counts, costs, inputs, outputs,
|
|
168
|
+
errors. Works with any stack: OpenAI, Anthropic, LangChain, Vercel AI
|
|
169
|
+
SDK, raw HTTP, custom tools. Cost is derived server-side from
|
|
170
|
+
`(provider, model)` — the SDK only sends tokens.
|
|
171
|
+
|
|
172
|
+
### 30-second quickstart
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import trodo from 'trodo-node';
|
|
176
|
+
trodo.init({ siteId: 'your-site-id' }); // autoInstrument on by default
|
|
177
|
+
|
|
178
|
+
const { result, runId } = await trodo.wrapAgent(
|
|
179
|
+
'customer-support',
|
|
180
|
+
async (run) => {
|
|
181
|
+
run.setInput({ query });
|
|
182
|
+
const answer = await agent.run(query); // OpenAI/Anthropic/LangChain auto-captured
|
|
183
|
+
run.setOutput(answer);
|
|
184
|
+
return answer;
|
|
185
|
+
},
|
|
186
|
+
{ distinctId: userId, conversationId: sessionId },
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Open the Agent Runs dashboard — the row shows tokens in/out, cost,
|
|
191
|
+
span count, tool count, error count, plus the full trace tree.
|
|
192
|
+
|
|
193
|
+
### Auto-instrumentation
|
|
194
|
+
|
|
195
|
+
`trodo.init()` calls `enableAutoInstrument()` which registers every
|
|
196
|
+
installed OpenTelemetry instrumentor — no extra wiring.
|
|
197
|
+
|
|
198
|
+
| Framework | Install |
|
|
199
|
+
|-----------|---------|
|
|
200
|
+
| OpenAI | `npm i @opentelemetry/instrumentation-openai` |
|
|
201
|
+
| Anthropic | `npm i @opentelemetry/instrumentation-anthropic` |
|
|
202
|
+
| LangChain | `npm i @opentelemetry/instrumentation-langchain` |
|
|
203
|
+
| LlamaIndex | `npm i @opentelemetry/instrumentation-llamaindex` |
|
|
204
|
+
| Google Gemini | `npm i @opentelemetry/instrumentation-google-generativeai` |
|
|
205
|
+
| Vertex AI | `npm i @opentelemetry/instrumentation-vertexai` |
|
|
206
|
+
| Bedrock | `npm i @opentelemetry/instrumentation-bedrock` |
|
|
207
|
+
| Cohere | `npm i @opentelemetry/instrumentation-cohere` |
|
|
208
|
+
| Vercel AI SDK | emits OTel via `experimental_telemetry: { isEnabled: true }` |
|
|
209
|
+
| http / fetch | bundled — generic HTTP spans for raw-HTTP callers |
|
|
210
|
+
|
|
211
|
+
Opt out with `trodo.init({ siteId, autoInstrument: false })`.
|
|
212
|
+
|
|
213
|
+
### Span helpers
|
|
214
|
+
|
|
215
|
+
Typed function wrappers for custom code — every call becomes a span
|
|
216
|
+
with args auto-captured as `input`, return value as `output`,
|
|
217
|
+
exception as `error`.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// trace — generic span
|
|
221
|
+
const prepared = trodo.trace('prepare', async (payload) => normalize(payload));
|
|
222
|
+
await prepared({ raw: true });
|
|
223
|
+
|
|
224
|
+
// tool — tool span (name-first OR fn-first)
|
|
225
|
+
const runFunnel = trodo.tool('run_funnel_query', async (teamId, preset) => {
|
|
226
|
+
return await db.funnel(teamId, preset);
|
|
227
|
+
});
|
|
228
|
+
await runFunnel(1, 'day7');
|
|
229
|
+
|
|
230
|
+
// llm — LLM span, auto-extracts OpenAI / Anthropic / Gemini usage
|
|
231
|
+
const answer = trodo.llm('answer', async (messages) => callOpenAI(messages), {
|
|
232
|
+
model: 'gpt-4o-mini',
|
|
233
|
+
provider: 'openai',
|
|
234
|
+
});
|
|
235
|
+
await answer([{ role: 'user', content: 'ping' }]);
|
|
236
|
+
// Records inputTokens / outputTokens from response.usage.
|
|
237
|
+
|
|
238
|
+
// retrieval — vector search / RAG retriever span
|
|
239
|
+
const search = trodo.retrieval('vector_search', async (q) => vecDb.query(q));
|
|
240
|
+
const docs = await search('users dropping off');
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Raw-HTTP escape hatches
|
|
244
|
+
|
|
245
|
+
If your LLM client isn't OTel-instrumented and you can't wrap it as a
|
|
246
|
+
function, record a span post-hoc:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const resp = await fetch(url, { body: JSON.stringify(body) }).then(r => r.json());
|
|
250
|
+
await trodo.trackLlmCall({
|
|
251
|
+
model: 'gemini-2.5-flash',
|
|
252
|
+
provider: 'google',
|
|
253
|
+
inputTokens: resp.usageMetadata.promptTokenCount,
|
|
254
|
+
outputTokens: resp.usageMetadata.candidatesTokenCount,
|
|
255
|
+
prompt: body,
|
|
256
|
+
completion: resp,
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
For advanced cases, get a raw OTel tracer — the Trodo processor is
|
|
261
|
+
already subscribed:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const tracer = trodo.getTracer('my.module');
|
|
265
|
+
tracer.startActiveSpan('custom', (span) => {
|
|
266
|
+
span.setAttribute('gen_ai.system', 'my-llm');
|
|
267
|
+
span.end();
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Cross-service runs
|
|
272
|
+
|
|
273
|
+
When one service calls another, the downstream service **joins** the
|
|
274
|
+
caller's run instead of creating its own — all spans nest under a
|
|
275
|
+
single timeline.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Caller — outbound:
|
|
279
|
+
await fetch(url, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { ...trodo.propagationHeaders(), 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify(payload),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Downstream (Express):
|
|
286
|
+
import express from 'express';
|
|
287
|
+
const app = express();
|
|
288
|
+
app.use(trodo.expressMiddleware());
|
|
289
|
+
// Every LLM call / tool / trace helper inside handlers now nests under
|
|
290
|
+
// the caller's run.
|
|
291
|
+
|
|
292
|
+
// Or manually:
|
|
293
|
+
await trodo.joinRun(
|
|
294
|
+
req.headers['x-trodo-run-id'] as string,
|
|
295
|
+
req.headers['x-trodo-parent-span-id'] as string,
|
|
296
|
+
async () => { /* ... */ },
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Long-lived sessions across processes — `startRun` / `endRun`
|
|
301
|
+
|
|
302
|
+
`wrapAgent` is a single-callback block — it opens *and* closes the run in
|
|
303
|
+
one function call. For sessions that live across many HTTP requests (an
|
|
304
|
+
MCP server, a websocket-pinned chat, scheduled jobs that resume on
|
|
305
|
+
different workers), use `startRun` to open the run from one process and
|
|
306
|
+
`endRun` to finalise it later. Between the two, any process can use
|
|
307
|
+
`joinRun` to add child spans. Same `runId` threads through everything.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Process A — open the run for an MCP session.
|
|
311
|
+
const runId = await trodo.startRun('external_mcp_session', {
|
|
312
|
+
distinctId: String(userId),
|
|
313
|
+
conversationId: mcpSessionId,
|
|
314
|
+
});
|
|
315
|
+
await redis.set(`mcp:run:${mcpSessionId}`, runId, 'EX', 3600);
|
|
316
|
+
|
|
317
|
+
// Process B (later, possibly a different worker) — append a tool span.
|
|
318
|
+
const runId = await redis.get(`mcp:run:${mcpSessionId}`);
|
|
319
|
+
await trodo.joinRun(runId, null, async (span) => {
|
|
320
|
+
span.setInput(args);
|
|
321
|
+
span.setOutput(result);
|
|
322
|
+
}, { name: 'tool.run_funnel_query', kind: 'tool' });
|
|
323
|
+
|
|
324
|
+
// When the session ends (timeout sweeper, explicit close):
|
|
325
|
+
await trodo.endRun(runId, { status: 'ok' });
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Conversation binding & feedback
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const { runId } = await trodo.wrapAgent(
|
|
332
|
+
'chat',
|
|
333
|
+
async (run) => { /* ... */ },
|
|
334
|
+
{ distinctId: userId, conversationId: sessionId },
|
|
335
|
+
);
|
|
336
|
+
await trodo.feedback(runId, { satisfaction: 'positive', rating: 5 });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Agent Analytics (legacy event-based API)
|
|
342
|
+
|
|
343
|
+
The older per-event API below is still supported but superseded by
|
|
344
|
+
`wrapAgent` + span helpers above. Use it only if you're already wired
|
|
345
|
+
into it; new integrations should prefer the tracing API.
|
|
346
|
+
|
|
347
|
+
**Before you start:** register your agent in **Integrations → AI Agents** in the dashboard to get an `agent_id` (`agt_xxxxxxxx`).
|
|
348
|
+
|
|
349
|
+
### `track_agent_call` — inbound message / LLM invocation
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
await trodo.track_agent_call({
|
|
353
|
+
agentId: 'agt_abc12345',
|
|
354
|
+
conversationId: 'conv_xyz',
|
|
355
|
+
messageId: 'msg_001',
|
|
356
|
+
prompt: userMessage,
|
|
357
|
+
model: 'gpt-4o',
|
|
358
|
+
provider: 'openai',
|
|
359
|
+
systemPromptVersion: 'v2', // optional — track prompt iterations
|
|
360
|
+
distinctId: userId, // optional — link to a Trodo user
|
|
361
|
+
metadata: { threadSource: 'slack', locale: 'en' }, // optional — stored in agent_calls.metadata (JSONB)
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### `track_tool_use` — tool/function call within a turn
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
await trodo.track_tool_use({
|
|
369
|
+
agentId: 'agt_abc12345',
|
|
370
|
+
conversationId: 'conv_xyz',
|
|
371
|
+
messageId: 'msg_001',
|
|
372
|
+
toolName: 'fetch_billing_info',
|
|
373
|
+
latencyMs: 143,
|
|
374
|
+
status: 'success', // 'success' | 'failure'
|
|
375
|
+
input: { userId: '123' }, // optional
|
|
376
|
+
output: { plan: 'pro' }, // optional
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### `track_agent_response` — LLM output and token usage
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
await trodo.track_agent_response({
|
|
384
|
+
agentId: 'agt_abc12345',
|
|
385
|
+
conversationId: 'conv_xyz',
|
|
386
|
+
messageId: 'msg_001',
|
|
387
|
+
model: 'gpt-4o',
|
|
388
|
+
completionTokens: response.usage.completion_tokens,
|
|
389
|
+
promptTokens: response.usage.prompt_tokens,
|
|
390
|
+
totalTokens: response.usage.total_tokens,
|
|
391
|
+
finishReason: response.choices[0].finish_reason,
|
|
392
|
+
distinctId: userId,
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### `track_agent_error` — errors and failures
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
await trodo.track_agent_error({
|
|
400
|
+
agentId: 'agt_abc12345',
|
|
401
|
+
conversationId: 'conv_xyz',
|
|
402
|
+
messageId: 'msg_001',
|
|
403
|
+
errorType: 'rate_limit', // 'timeout' | 'rate_limit' | 'guardrail_block' | ...
|
|
404
|
+
errorMessage: err.message,
|
|
405
|
+
failedTool: 'fetch_billing_info', // optional
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `track_feedback` — user thumbs up/down
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
await trodo.track_feedback({
|
|
413
|
+
agentId: 'agt_abc12345',
|
|
414
|
+
conversationId: 'conv_xyz',
|
|
415
|
+
messageId: 'msg_001', // same messageId as the response it refers to
|
|
416
|
+
feedback: 'positive', // 'positive' | 'negative' | 'unreact'
|
|
417
|
+
distinctId: userId,
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Full turn example
|
|
422
|
+
|
|
423
|
+
```javascript
|
|
424
|
+
async function runAgentTurn(userId, conversationId, userMessage) {
|
|
425
|
+
const agentId = 'agt_abc12345';
|
|
426
|
+
const messageId = `msg_${Date.now()}`;
|
|
427
|
+
|
|
428
|
+
await trodo.track_agent_call({ agentId, conversationId, messageId, prompt: userMessage, distinctId: userId });
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
await trodo.track_tool_use({ agentId, conversationId, messageId, toolName: 'search', status: 'success', latencyMs: 80 });
|
|
432
|
+
|
|
433
|
+
const response = await llm.complete(userMessage);
|
|
434
|
+
|
|
435
|
+
await trodo.track_agent_response({
|
|
436
|
+
agentId, conversationId, messageId,
|
|
437
|
+
model: response.model,
|
|
438
|
+
completionTokens: response.usage.completion_tokens,
|
|
439
|
+
promptTokens: response.usage.prompt_tokens,
|
|
440
|
+
totalTokens: response.usage.total_tokens,
|
|
441
|
+
distinctId: userId,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return response.text;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
await trodo.track_agent_error({ agentId, conversationId, messageId, errorType: err.type, errorMessage: err.message, distinctId: userId });
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Identity Merging (Cross-SDK)
|
|
455
|
+
|
|
456
|
+
Call `identify()` with the **same value** on the browser and server to merge all events under one user profile:
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
// Browser
|
|
460
|
+
Trodo.identify('user@example.com'); // → id_user@example.com
|
|
461
|
+
|
|
462
|
+
// Node.js (same value)
|
|
463
|
+
await user.identify('user@example.com'); // → id_user@example.com
|
|
464
|
+
// Events from both sides now appear together in the dashboard
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Batching
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
trodo.init({
|
|
473
|
+
siteId: 'your-site-id',
|
|
474
|
+
batchEnabled: true,
|
|
475
|
+
batchSize: 50,
|
|
476
|
+
batchFlushIntervalMs: 5000,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Always flush before process exit
|
|
480
|
+
process.on('SIGTERM', async () => { await trodo.shutdown(); process.exit(0); });
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Auto Events
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
trodo.init({ siteId: 'your-site-id', autoEvents: true });
|
|
489
|
+
// Hooks process.on('uncaughtException') and process.on('unhandledRejection')
|
|
490
|
+
// Sends server_error events with distinct_id: 'server_global'
|
|
491
|
+
|
|
492
|
+
// Toggle at runtime
|
|
493
|
+
trodo.enableAutoEvents();
|
|
494
|
+
trodo.disableAutoEvents();
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## TypeScript
|
|
500
|
+
|
|
501
|
+
Full type declarations bundled:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import trodo, { TrodoClient, UserContext } from 'trodo-node';
|
|
505
|
+
import type { AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps } from 'trodo-node';
|
|
506
|
+
|
|
507
|
+
// Multi-tenant
|
|
508
|
+
const client = new TrodoClient({ siteId: 'your-site-id' });
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## License
|
|
512
|
+
|
|
513
|
+
ISC
|