trodo-node 1.1.0 → 2.1.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.
Files changed (68) hide show
  1. package/README.md +340 -103
  2. package/dist/cjs/TrodoClient.js +77 -19
  3. package/dist/cjs/TrodoClient.js.map +1 -1
  4. package/dist/cjs/api/ApiClient.js +21 -3
  5. package/dist/cjs/api/ApiClient.js.map +1 -1
  6. package/dist/cjs/api/endpoints.js +7 -1
  7. package/dist/cjs/api/endpoints.js.map +1 -1
  8. package/dist/cjs/index.js +104 -8
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/otel/autoInstrument.js +253 -0
  11. package/dist/cjs/otel/autoInstrument.js.map +1 -0
  12. package/dist/cjs/otel/context.js +49 -0
  13. package/dist/cjs/otel/context.js.map +1 -0
  14. package/dist/cjs/otel/helpers.js +254 -0
  15. package/dist/cjs/otel/helpers.js.map +1 -0
  16. package/dist/cjs/otel/processor.js +129 -0
  17. package/dist/cjs/otel/processor.js.map +1 -0
  18. package/dist/cjs/otel/uuid.js +24 -0
  19. package/dist/cjs/otel/uuid.js.map +1 -0
  20. package/dist/cjs/otel/wrapAgent.js +399 -0
  21. package/dist/cjs/otel/wrapAgent.js.map +1 -0
  22. package/dist/cjs/queue/BatchFlusher.js +1 -1
  23. package/dist/cjs/queue/BatchFlusher.js.map +1 -1
  24. package/dist/esm/TrodoClient.d.ts +34 -1
  25. package/dist/esm/TrodoClient.d.ts.map +1 -1
  26. package/dist/esm/TrodoClient.js +77 -19
  27. package/dist/esm/TrodoClient.js.map +1 -1
  28. package/dist/esm/api/ApiClient.d.ts +9 -1
  29. package/dist/esm/api/ApiClient.d.ts.map +1 -1
  30. package/dist/esm/api/ApiClient.js +21 -3
  31. package/dist/esm/api/ApiClient.js.map +1 -1
  32. package/dist/esm/api/endpoints.d.ts +6 -1
  33. package/dist/esm/api/endpoints.d.ts.map +1 -1
  34. package/dist/esm/api/endpoints.js +7 -1
  35. package/dist/esm/api/endpoints.js.map +1 -1
  36. package/dist/esm/index.d.ts +84 -8
  37. package/dist/esm/index.d.ts.map +1 -1
  38. package/dist/esm/index.js +89 -7
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/otel/autoInstrument.d.ts +61 -0
  41. package/dist/esm/otel/autoInstrument.d.ts.map +1 -0
  42. package/dist/esm/otel/autoInstrument.js +248 -0
  43. package/dist/esm/otel/autoInstrument.js.map +1 -0
  44. package/dist/esm/otel/context.d.ts +26 -0
  45. package/dist/esm/otel/context.d.ts.map +1 -0
  46. package/dist/esm/otel/context.js +44 -0
  47. package/dist/esm/otel/context.js.map +1 -0
  48. package/dist/esm/otel/helpers.d.ts +119 -0
  49. package/dist/esm/otel/helpers.d.ts.map +1 -0
  50. package/dist/esm/otel/helpers.js +244 -0
  51. package/dist/esm/otel/helpers.js.map +1 -0
  52. package/dist/esm/otel/processor.d.ts +94 -0
  53. package/dist/esm/otel/processor.d.ts.map +1 -0
  54. package/dist/esm/otel/processor.js +125 -0
  55. package/dist/esm/otel/processor.js.map +1 -0
  56. package/dist/esm/otel/uuid.d.ts +7 -0
  57. package/dist/esm/otel/uuid.d.ts.map +1 -0
  58. package/dist/esm/otel/uuid.js +21 -0
  59. package/dist/esm/otel/uuid.js.map +1 -0
  60. package/dist/esm/otel/wrapAgent.d.ts +100 -0
  61. package/dist/esm/otel/wrapAgent.d.ts.map +1 -0
  62. package/dist/esm/otel/wrapAgent.js +389 -0
  63. package/dist/esm/otel/wrapAgent.js.map +1 -0
  64. package/dist/esm/queue/BatchFlusher.js +1 -1
  65. package/dist/esm/queue/BatchFlusher.js.map +1 -1
  66. package/dist/esm/types/index.d.ts +26 -0
  67. package/dist/esm/types/index.d.ts.map +1 -1
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # trodo-node
2
2
 
3
- Server-side Node.js SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, and manage people/groups — all merging seamlessly with your frontend Trodo data under the same `siteId`.
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
4
 
5
5
  ## Installation
6
6
 
@@ -19,117 +19,81 @@ npm install trodo-node node-fetch
19
19
  ```javascript
20
20
  const trodo = require('trodo-node');
21
21
 
22
- // Initialize once at app startup
23
- trodo.init({
24
- siteId: 'your-site-id',
25
- debug: false, // optional: log API calls
26
- autoEvents: true, // optional: capture uncaughtException / unhandledRejection
27
- });
28
-
29
- // For identified users — one call creates the session AND fires identify
30
- const user = await trodo.identify('user@example.com');
31
- // distinctId is now id_user@example.com; subsequent calls return cached context
32
-
33
- // For anonymous/backend-only users
34
- const anonUser = trodo.forUser('user-123');
22
+ trodo.init({ siteId: 'your-site-id' });
35
23
 
36
- // Track a custom event
24
+ // User-bound context (recommended)
25
+ const user = trodo.forUser('user-123');
37
26
  await user.track('purchase_completed', { amount: 99.99, plan: 'pro' });
38
-
39
- // Update people profile
40
27
  await user.people.set({ plan: 'pro', company: 'Acme' });
41
28
 
42
- // Track a server-side error
43
- await user.captureError(new Error('payment failed'));
44
-
45
- // Flush queued events before process exit
29
+ // Flush before process exit if using batching
46
30
  await trodo.shutdown();
47
31
  ```
48
32
 
49
- ## ESM
50
-
51
33
  ```javascript
34
+ // ESM
52
35
  import trodo from 'trodo-node';
53
36
  trodo.init({ siteId: 'your-site-id' });
54
37
  ```
55
38
 
56
- ## Cross-SDK Identity Merging
57
-
58
- Frontend and backend events merge when both sides call `identify()` with the same value:
59
-
60
- ```javascript
61
- // Browser SDK
62
- Trodo.identify('user@example.com'); // → id_user@example.com
63
-
64
- // Node.js SDK (same value)
65
- await user.identify('user@example.com'); // → id_user@example.com
66
-
67
- // Both event streams now appear together in the Trodo dashboard
68
- ```
69
-
70
- ## API Reference
39
+ ## Core API
71
40
 
72
41
  ### `trodo.init(config)`
73
42
 
74
- | Option | Type | Default | Description |
75
- |--------|------|---------|-------------|
76
- | `siteId` | `string` | required | Your Trodo site ID |
77
- | `apiBase` | `string` | `https://sdkapi.trodo.ai` | API base URL |
78
- | `debug` | `boolean` | `false` | Log API requests/responses |
79
- | `autoEvents` | `boolean` | `false` | Hook `uncaughtException` + `unhandledRejection` |
80
- | `retries` | `number` | `3` | HTTP retry attempts on 5xx errors |
81
- | `timeout` | `number` | `10000` | HTTP timeout in milliseconds |
82
- | `batchEnabled` | `boolean` | `false` | Queue events and flush in bulk |
83
- | `batchSize` | `number` | `50` | Max events per batch flush |
84
- | `batchFlushIntervalMs` | `number` | `5000` | Flush interval in milliseconds |
85
- | `onError` | `function` | `undefined` | Callback for SDK errors |
86
-
87
- ### `trodo.identify(identifyId, options?)` primary entry point
43
+ Call once at app startup.
44
+
45
+ | Option | Default | Description |
46
+ |--------|---------|-------------|
47
+ | `siteId` | required | Your Trodo site ID |
48
+ | `apiBase` | `https://sdkapi.trodo.ai` | API base URL |
49
+ | `timeout` | `10000` ms | HTTP request timeout |
50
+ | `retries` | `2` | Retries on network/5xx errors |
51
+ | `autoEvents` | `false` | Hook `uncaughtException` / `unhandledRejection` as `server_error` events |
52
+ | `batchEnabled` | `false` | Queue events and flush in batches |
53
+ | `batchSize` | `50` | Flush when this many events are queued |
54
+ | `batchFlushIntervalMs` | `5000` | Also flush every N milliseconds |
55
+ | `onError` | — | Callback for SDK errors (silent by default) |
56
+ | `debug` | `false` | Log API calls to stderr |
88
57
 
89
- Creates a session, fires `POST /api/sdk/identify` (first call only), caches the context, and returns it. Subsequent calls with the same `identifyId` return the cached context instantly with no API call.
90
-
91
- ```javascript
92
- const user = await trodo.identify('user@example.com');
93
- // distinctId = id_user@example.com — merges with frontend events
94
- await user.track('purchase_completed', { amount: 99.99 });
95
- ```
58
+ ### `trodo.forUser(distinctId, options?)`
96
59
 
97
- Pass `sessionId` to correlate with an active browser session:
60
+ Returns a user-bound context. No API call is made until you track an event.
98
61
 
99
62
  ```javascript
100
- const user = await trodo.identify('user@example.com', {
101
- sessionId: req.cookies.trodo_session,
63
+ const user = trodo.forUser('user-123', {
64
+ sessionId: req.cookies.trodo_session, // optional: correlate with browser session
102
65
  });
103
66
  ```
104
67
 
105
- ### `trodo.forUser(distinctId, options?)`
68
+ ### `trodo.identify(identifyId, options?)`
106
69
 
107
- Returns a user-bound context synchronously with no API call. Use this for pure backend tracking where identity is already known and you don't need to merge with frontend events.
70
+ 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.
108
71
 
109
72
  ```javascript
110
- const user = trodo.forUser('internal-worker-id', {
111
- sessionId: req.cookies.trodo_session, // optional: correlate with browser session
73
+ const user = await trodo.identify('user@example.com', {
74
+ sessionId: req.cookies.trodo_session,
112
75
  });
76
+ // distinctId is now id_user@example.com — merges with browser events
77
+ await user.track('login');
113
78
  ```
114
79
 
115
- ### User Context Methods
80
+ ### User context methods
116
81
 
117
82
  ```javascript
118
- await user.track(eventName, properties?) // Track custom event
119
- await user.trackEvent(eventName, properties?) // Alias for track()
120
- await user.identify(identifyId) // Merge identity
121
- await user.walletAddress(address) // Set crypto wallet address
122
- await user.reset() // Clear session context
123
- await user.captureError(error) // Track server_error event
83
+ await user.track(eventName, properties?) // Custom event
84
+ await user.identify(identifyId) // Merge identity
85
+ await user.walletAddress(address) // Set wallet address
86
+ await user.reset() // Clear session
87
+ await user.captureError(err, severity?) // Track server_error ('critical' | 'error' | 'warning')
124
88
 
125
89
  // People profile
126
90
  await user.people.set(properties)
127
91
  await user.people.setOnce(properties)
128
92
  await user.people.unset(keys)
129
- await user.people.increment(properties)
130
- await user.people.append(properties)
131
- await user.people.union(properties)
132
- await user.people.remove(properties)
93
+ await user.people.increment(key, amount?)
94
+ await user.people.append(key, values)
95
+ await user.people.union(key, values)
96
+ await user.people.remove(key, values)
133
97
  await user.people.trackCharge(amount, properties?)
134
98
  await user.people.clearCharges()
135
99
  await user.people.deleteUser()
@@ -141,15 +105,15 @@ await user.remove_group(groupKey, groupId)
141
105
  const group = user.get_group(groupKey, groupId)
142
106
  await group.set(properties)
143
107
  await group.set_once(properties)
144
- await group.union(properties)
145
- await group.remove(properties)
108
+ await group.increment(key, amount?)
109
+ await group.append(key, values)
110
+ await group.union(key, values)
111
+ await group.remove(key, values)
146
112
  await group.unset(keys)
147
- await group.increment(properties)
148
- await group.append(properties)
149
113
  await group.delete()
150
114
  ```
151
115
 
152
- ### Direct Call Pattern (for pipelines)
116
+ ### Direct call pattern
153
117
 
154
118
  ```javascript
155
119
  await trodo.track('user-123', 'event_name', { key: 'value' })
@@ -157,25 +121,284 @@ await trodo.people.set('user-123', { plan: 'pro' })
157
121
  await trodo.set_group('user-123', 'company', 'acme')
158
122
  ```
159
123
 
160
- ### Global Methods
124
+ ---
125
+
126
+ ## AI Agent Tracing (recommended)
127
+
128
+ One wrap around your agent captures every LLM call, tool call, and
129
+ nested step as a tree of spans — token counts, costs, inputs, outputs,
130
+ errors. Works with any stack: OpenAI, Anthropic, LangChain, Vercel AI
131
+ SDK, raw HTTP, custom tools. Cost is derived server-side from
132
+ `(provider, model)` — the SDK only sends tokens.
133
+
134
+ ### 30-second quickstart
135
+
136
+ ```typescript
137
+ import trodo from 'trodo-node';
138
+ trodo.init({ siteId: 'your-site-id' }); // autoInstrument on by default
139
+
140
+ const { result, runId } = await trodo.wrapAgent(
141
+ 'customer-support',
142
+ async (run) => {
143
+ run.setInput({ query });
144
+ const answer = await agent.run(query); // OpenAI/Anthropic/LangChain auto-captured
145
+ run.setOutput(answer);
146
+ return answer;
147
+ },
148
+ { distinctId: userId, conversationId: sessionId },
149
+ );
150
+ ```
151
+
152
+ Open the Agent Runs dashboard — the row shows tokens in/out, cost,
153
+ span count, tool count, error count, plus the full trace tree.
154
+
155
+ ### Auto-instrumentation
156
+
157
+ `trodo.init()` calls `enableAutoInstrument()` which registers every
158
+ installed OpenTelemetry instrumentor — no extra wiring.
159
+
160
+ | Framework | Install |
161
+ |-----------|---------|
162
+ | OpenAI | `npm i @opentelemetry/instrumentation-openai` |
163
+ | Anthropic | `npm i @opentelemetry/instrumentation-anthropic` |
164
+ | LangChain | `npm i @opentelemetry/instrumentation-langchain` |
165
+ | LlamaIndex | `npm i @opentelemetry/instrumentation-llamaindex` |
166
+ | Google Gemini | `npm i @opentelemetry/instrumentation-google-generativeai` |
167
+ | Vertex AI | `npm i @opentelemetry/instrumentation-vertexai` |
168
+ | Bedrock | `npm i @opentelemetry/instrumentation-bedrock` |
169
+ | Cohere | `npm i @opentelemetry/instrumentation-cohere` |
170
+ | Vercel AI SDK | emits OTel via `experimental_telemetry: { isEnabled: true }` |
171
+ | http / fetch | bundled — generic HTTP spans for raw-HTTP callers |
172
+
173
+ Opt out with `trodo.init({ siteId, autoInstrument: false })`.
174
+
175
+ ### Span helpers
176
+
177
+ Typed function wrappers for custom code — every call becomes a span
178
+ with args auto-captured as `input`, return value as `output`,
179
+ exception as `error`.
180
+
181
+ ```typescript
182
+ // trace — generic span
183
+ const prepared = trodo.trace('prepare', async (payload) => normalize(payload));
184
+ await prepared({ raw: true });
185
+
186
+ // tool — tool span (name-first OR fn-first)
187
+ const runFunnel = trodo.tool('run_funnel_query', async (teamId, preset) => {
188
+ return await db.funnel(teamId, preset);
189
+ });
190
+ await runFunnel(1, 'day7');
191
+
192
+ // llm — LLM span, auto-extracts OpenAI / Anthropic / Gemini usage
193
+ const answer = trodo.llm('answer', async (messages) => callOpenAI(messages), {
194
+ model: 'gpt-4o-mini',
195
+ provider: 'openai',
196
+ });
197
+ await answer([{ role: 'user', content: 'ping' }]);
198
+ // Records inputTokens / outputTokens from response.usage.
199
+
200
+ // retrieval — vector search / RAG retriever span
201
+ const search = trodo.retrieval('vector_search', async (q) => vecDb.query(q));
202
+ const docs = await search('users dropping off');
203
+ ```
204
+
205
+ ### Raw-HTTP escape hatches
206
+
207
+ If your LLM client isn't OTel-instrumented and you can't wrap it as a
208
+ function, record a span post-hoc:
209
+
210
+ ```typescript
211
+ const resp = await fetch(url, { body: JSON.stringify(body) }).then(r => r.json());
212
+ await trodo.trackLlmCall({
213
+ model: 'gemini-2.5-flash',
214
+ provider: 'google',
215
+ inputTokens: resp.usageMetadata.promptTokenCount,
216
+ outputTokens: resp.usageMetadata.candidatesTokenCount,
217
+ prompt: body,
218
+ completion: resp,
219
+ });
220
+ ```
221
+
222
+ For advanced cases, get a raw OTel tracer — the Trodo processor is
223
+ already subscribed:
224
+
225
+ ```typescript
226
+ const tracer = trodo.getTracer('my.module');
227
+ tracer.startActiveSpan('custom', (span) => {
228
+ span.setAttribute('gen_ai.system', 'my-llm');
229
+ span.end();
230
+ });
231
+ ```
232
+
233
+ ### Cross-service runs
234
+
235
+ When one service calls another, the downstream service **joins** the
236
+ caller's run instead of creating its own — all spans nest under a
237
+ single timeline.
238
+
239
+ ```typescript
240
+ // Caller — outbound:
241
+ await fetch(url, {
242
+ method: 'POST',
243
+ headers: { ...trodo.propagationHeaders(), 'content-type': 'application/json' },
244
+ body: JSON.stringify(payload),
245
+ });
246
+
247
+ // Downstream (Express):
248
+ import express from 'express';
249
+ const app = express();
250
+ app.use(trodo.expressMiddleware());
251
+ // Every LLM call / tool / trace helper inside handlers now nests under
252
+ // the caller's run.
253
+
254
+ // Or manually:
255
+ await trodo.joinRun(
256
+ req.headers['x-trodo-run-id'] as string,
257
+ req.headers['x-trodo-parent-span-id'] as string,
258
+ async () => { /* ... */ },
259
+ );
260
+ ```
261
+
262
+ ### Conversation binding & feedback
263
+
264
+ ```typescript
265
+ const { runId } = await trodo.wrapAgent(
266
+ 'chat',
267
+ async (run) => { /* ... */ },
268
+ { distinctId: userId, conversationId: sessionId },
269
+ );
270
+ await trodo.feedback(runId, { satisfaction: 'positive', rating: 5 });
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Agent Analytics (legacy event-based API)
276
+
277
+ The older per-event API below is still supported but superseded by
278
+ `wrapAgent` + span helpers above. Use it only if you're already wired
279
+ into it; new integrations should prefer the tracing API.
280
+
281
+ **Before you start:** register your agent in **Integrations → AI Agents** in the dashboard to get an `agent_id` (`agt_xxxxxxxx`).
282
+
283
+ ### `track_agent_call` — inbound message / LLM invocation
161
284
 
162
285
  ```javascript
163
- trodo.enableAutoEvents() // Enable uncaughtException hooks
164
- trodo.disableAutoEvents() // Disable hooks
165
- await trodo.flush() // Flush pending batch queue
166
- await trodo.shutdown() // Flush + stop background timers
286
+ await trodo.track_agent_call({
287
+ agentId: 'agt_abc12345',
288
+ conversationId: 'conv_xyz',
289
+ messageId: 'msg_001',
290
+ prompt: userMessage,
291
+ model: 'gpt-4o',
292
+ provider: 'openai',
293
+ systemPromptVersion: 'v2', // optional — track prompt iterations
294
+ distinctId: userId, // optional — link to a Trodo user
295
+ metadata: { threadSource: 'slack', locale: 'en' }, // optional — stored in agent_calls.metadata (JSONB)
296
+ });
167
297
  ```
168
298
 
169
- ## Auto Events
299
+ ### `track_tool_use` — tool/function call within a turn
170
300
 
171
- When `autoEvents: true`, the SDK hooks Node.js process error events and sends `server_error` events to Trodo:
301
+ ```javascript
302
+ await trodo.track_tool_use({
303
+ agentId: 'agt_abc12345',
304
+ conversationId: 'conv_xyz',
305
+ messageId: 'msg_001',
306
+ toolName: 'fetch_billing_info',
307
+ latencyMs: 143,
308
+ status: 'success', // 'success' | 'failure'
309
+ input: { userId: '123' }, // optional
310
+ output: { plan: 'pro' }, // optional
311
+ });
312
+ ```
172
313
 
173
- - `process.on('uncaughtException')`
174
- - `process.on('unhandledRejection')`
314
+ ### `track_agent_response` — LLM output and token usage
175
315
 
176
- These events use `distinct_id: 'server_global'` in the dashboard.
316
+ ```javascript
317
+ await trodo.track_agent_response({
318
+ agentId: 'agt_abc12345',
319
+ conversationId: 'conv_xyz',
320
+ messageId: 'msg_001',
321
+ model: 'gpt-4o',
322
+ completionTokens: response.usage.completion_tokens,
323
+ promptTokens: response.usage.prompt_tokens,
324
+ totalTokens: response.usage.total_tokens,
325
+ finishReason: response.choices[0].finish_reason,
326
+ distinctId: userId,
327
+ });
328
+ ```
177
329
 
178
- Per-user error capture: `user.captureError(error)` uses the user's own `distinctId`.
330
+ ### `track_agent_error` errors and failures
331
+
332
+ ```javascript
333
+ await trodo.track_agent_error({
334
+ agentId: 'agt_abc12345',
335
+ conversationId: 'conv_xyz',
336
+ messageId: 'msg_001',
337
+ errorType: 'rate_limit', // 'timeout' | 'rate_limit' | 'guardrail_block' | ...
338
+ errorMessage: err.message,
339
+ failedTool: 'fetch_billing_info', // optional
340
+ });
341
+ ```
342
+
343
+ ### `track_feedback` — user thumbs up/down
344
+
345
+ ```javascript
346
+ await trodo.track_feedback({
347
+ agentId: 'agt_abc12345',
348
+ conversationId: 'conv_xyz',
349
+ messageId: 'msg_001', // same messageId as the response it refers to
350
+ feedback: 'positive', // 'positive' | 'negative' | 'unreact'
351
+ distinctId: userId,
352
+ });
353
+ ```
354
+
355
+ ### Full turn example
356
+
357
+ ```javascript
358
+ async function runAgentTurn(userId, conversationId, userMessage) {
359
+ const agentId = 'agt_abc12345';
360
+ const messageId = `msg_${Date.now()}`;
361
+
362
+ await trodo.track_agent_call({ agentId, conversationId, messageId, prompt: userMessage, distinctId: userId });
363
+
364
+ try {
365
+ await trodo.track_tool_use({ agentId, conversationId, messageId, toolName: 'search', status: 'success', latencyMs: 80 });
366
+
367
+ const response = await llm.complete(userMessage);
368
+
369
+ await trodo.track_agent_response({
370
+ agentId, conversationId, messageId,
371
+ model: response.model,
372
+ completionTokens: response.usage.completion_tokens,
373
+ promptTokens: response.usage.prompt_tokens,
374
+ totalTokens: response.usage.total_tokens,
375
+ distinctId: userId,
376
+ });
377
+
378
+ return response.text;
379
+ } catch (err) {
380
+ await trodo.track_agent_error({ agentId, conversationId, messageId, errorType: err.type, errorMessage: err.message, distinctId: userId });
381
+ throw err;
382
+ }
383
+ }
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Identity Merging (Cross-SDK)
389
+
390
+ Call `identify()` with the **same value** on the browser and server to merge all events under one user profile:
391
+
392
+ ```javascript
393
+ // Browser
394
+ Trodo.identify('user@example.com'); // → id_user@example.com
395
+
396
+ // Node.js (same value)
397
+ await user.identify('user@example.com'); // → id_user@example.com
398
+ // Events from both sides now appear together in the dashboard
399
+ ```
400
+
401
+ ---
179
402
 
180
403
  ## Batching
181
404
 
@@ -183,26 +406,40 @@ Per-user error capture: `user.captureError(error)` uses the user's own `distinct
183
406
  trodo.init({
184
407
  siteId: 'your-site-id',
185
408
  batchEnabled: true,
186
- batchSize: 100,
187
- batchFlushIntervalMs: 3000,
409
+ batchSize: 50,
410
+ batchFlushIntervalMs: 5000,
188
411
  });
189
412
 
190
- // Events are queued and flushed every 3s or when 100 events accumulate
191
- await user.track('page_view');
192
-
193
413
  // Always flush before process exit
194
- process.on('SIGTERM', () => trodo.shutdown());
414
+ process.on('SIGTERM', async () => { await trodo.shutdown(); process.exit(0); });
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Auto Events
420
+
421
+ ```javascript
422
+ trodo.init({ siteId: 'your-site-id', autoEvents: true });
423
+ // Hooks process.on('uncaughtException') and process.on('unhandledRejection')
424
+ // Sends server_error events with distinct_id: 'server_global'
425
+
426
+ // Toggle at runtime
427
+ trodo.enableAutoEvents();
428
+ trodo.disableAutoEvents();
195
429
  ```
196
430
 
431
+ ---
432
+
197
433
  ## TypeScript
198
434
 
199
- Full TypeScript support with bundled type declarations:
435
+ Full type declarations bundled:
200
436
 
201
437
  ```typescript
202
- import trodo, { TrodoConfig, ForUserOptions } from 'trodo-node';
438
+ import trodo, { TrodoClient, UserContext } from 'trodo-node';
439
+ import type { AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps } from 'trodo-node';
203
440
 
204
- const config: TrodoConfig = { siteId: 'your-site-id' };
205
- trodo.init(config);
441
+ // Multi-tenant
442
+ const client = new TrodoClient({ siteId: 'your-site-id' });
206
443
  ```
207
444
 
208
445
  ## License
@@ -7,6 +7,10 @@ const EventQueue_js_1 = require("./queue/EventQueue.js");
7
7
  const BatchFlusher_js_1 = require("./queue/BatchFlusher.js");
8
8
  const AutoEventManager_js_1 = require("./auto/AutoEventManager.js");
9
9
  const UserContext_js_1 = require("./UserContext.js");
10
+ const processor_js_1 = require("./otel/processor.js");
11
+ const wrapAgent_js_1 = require("./otel/wrapAgent.js");
12
+ const helpers_js_1 = require("./otel/helpers.js");
13
+ const autoInstrument_js_1 = require("./otel/autoInstrument.js");
10
14
  const DEFAULT_API_BASE = 'https://sdkapi.trodo.ai';
11
15
  class TrodoClient {
12
16
  constructor(config) {
@@ -30,7 +34,11 @@ class TrodoClient {
30
34
  if (config.autoEvents) {
31
35
  this.autoEventManager.enable();
32
36
  }
33
- // Wire up direct-call-pattern people helpers
37
+ this.spanProcessor = new processor_js_1.TrodoSpanProcessor({ apiClient: this.apiClient });
38
+ // Default ON — matches Python SDK. Opt out with autoInstrument: false.
39
+ if (config.autoInstrument !== false) {
40
+ (0, autoInstrument_js_1.enableAutoInstrument)({ processor: this.spanProcessor });
41
+ }
34
42
  const client = this;
35
43
  this.people = {
36
44
  set: (d, p) => client.forUser(d).people.set(p),
@@ -49,17 +57,13 @@ class TrodoClient {
49
57
  // Primary pattern: forUser()
50
58
  // --------------------------------------------------------------------------
51
59
  forUser(distinctId, options) {
52
- const key = distinctId;
53
- const cached = this.userContextCache.get(key);
60
+ const cached = this.userContextCache.get(distinctId);
54
61
  if (cached)
55
62
  return cached;
56
63
  const ctx = new UserContext_js_1.UserContext(distinctId, this.siteId, this.apiClient, this.sessionManager, this.eventQueue, this.batchFlusher, this.autoEventManager, options);
57
- this.userContextCache.set(key, ctx);
64
+ this.userContextCache.set(distinctId, ctx);
58
65
  return ctx;
59
66
  }
60
- // --------------------------------------------------------------------------
61
- // Direct-call pattern (distinctId as first param)
62
- // --------------------------------------------------------------------------
63
67
  async track(distinctId, eventName, properties, options) {
64
68
  return this.forUser(distinctId).track(eventName, properties, options);
65
69
  }
@@ -90,21 +94,12 @@ class TrodoClient {
90
94
  get_group(distinctId, groupKey, groupId) {
91
95
  return this.forUser(distinctId).get_group(groupKey, groupId);
92
96
  }
93
- // --------------------------------------------------------------------------
94
- // Auto events
95
- // --------------------------------------------------------------------------
96
- enableAutoEvents() {
97
- this.autoEventManager.enable();
98
- }
99
- disableAutoEvents() {
100
- this.autoEventManager.disable();
101
- }
102
- // --------------------------------------------------------------------------
103
- // Lifecycle
104
- // --------------------------------------------------------------------------
97
+ enableAutoEvents() { this.autoEventManager.enable(); }
98
+ disableAutoEvents() { this.autoEventManager.disable(); }
105
99
  async flush() {
106
100
  if (this.batchFlusher)
107
101
  await this.batchFlusher.flush();
102
+ await this.spanProcessor.forceFlush();
108
103
  }
109
104
  async shutdown() {
110
105
  this.autoEventManager.disable();
@@ -112,6 +107,69 @@ class TrodoClient {
112
107
  this.batchFlusher.stop();
113
108
  await this.batchFlusher.flush();
114
109
  }
110
+ await this.spanProcessor.shutdown();
111
+ }
112
+ // --------------------------------------------------------------------------
113
+ // Agent Runs — unified surface
114
+ // --------------------------------------------------------------------------
115
+ /**
116
+ * Wrap an async function as an agent run. Callback receives a RunHandle
117
+ * — call `handle.setInput(...)` / `handle.setOutput(...)` to populate the
118
+ * run's input/output fields. Every OTel-auto-instrumented LLM call or
119
+ * nested `withSpan` inside `fn` is captured as a child span.
120
+ * Returns { result, runId } — runId is used to attach feedback later.
121
+ */
122
+ wrapAgent(agentName, fn, options = {}) {
123
+ return (0, wrapAgent_js_1.wrapAgent)(this.spanProcessor, this.siteId, agentName, fn, options);
124
+ }
125
+ /** Create a nested span inside the current run. */
126
+ withSpan(name, fn, options = {}) {
127
+ return (0, wrapAgent_js_1.withSpan)(this.spanProcessor, name, fn, options);
128
+ }
129
+ /**
130
+ * Join an existing run owned by a remote service — opens a span (not a new
131
+ * run) in that run's context. Spans inside `fn` are streamed to the backend
132
+ * via append_spans. Used by the express middleware and for manual cross-
133
+ * service join on any request shape.
134
+ */
135
+ joinRun(runId, parentSpanId, fn, options = {}) {
136
+ return (0, wrapAgent_js_1.joinRun)(this.spanProcessor, this.siteId, runId, parentSpanId, fn, options);
137
+ }
138
+ /** Higher-order wrapper: decorate a tool-like function so each call becomes a span. */
139
+ tool(fn, options = {}) {
140
+ return (0, helpers_js_1.tool)(fn, options);
141
+ }
142
+ /** Record a one-shot LLM span for a raw-HTTP caller. */
143
+ trackLlmCall(params) {
144
+ return (0, helpers_js_1.trackLlmCall)(params);
145
+ }
146
+ /** Return an Express/Connect middleware that auto-joins inbound runs. */
147
+ expressMiddleware() {
148
+ return (0, helpers_js_1.expressMiddleware)({
149
+ processor: this.spanProcessor,
150
+ teamSiteId: this.siteId,
151
+ });
152
+ }
153
+ /** Outbound HTTP headers carrying the current run/span context. */
154
+ propagationHeaders() {
155
+ return (0, helpers_js_1.propagationHeaders)();
156
+ }
157
+ /** Expose current run/span ids for bespoke propagation needs. */
158
+ currentRunId() {
159
+ return (0, wrapAgent_js_1.currentRunId)();
160
+ }
161
+ currentSpanId() {
162
+ return (0, wrapAgent_js_1.currentSpanId)();
163
+ }
164
+ /** Attach feedback to a completed run. Call with the runId returned from wrapAgent. */
165
+ async feedback(runId, props) {
166
+ return this.apiClient.postRunFeedback(runId, {
167
+ satisfaction: props.satisfaction ?? null,
168
+ rating: props.rating ?? null,
169
+ comment: props.comment ?? props.feedback ?? null,
170
+ distinct_id: props.distinctId ?? null,
171
+ attributes: props.metadata ?? {},
172
+ });
115
173
  }
116
174
  }
117
175
  exports.TrodoClient = TrodoClient;