trodo-node 2.4.3 → 2.5.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 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