@traccia2/sdk 0.0.1

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 (130) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +503 -0
  3. package/dist/auto.d.ts +27 -0
  4. package/dist/auto.d.ts.map +1 -0
  5. package/dist/auto.js +171 -0
  6. package/dist/auto.js.map +1 -0
  7. package/dist/config/env-config.d.ts +21 -0
  8. package/dist/config/env-config.d.ts.map +1 -0
  9. package/dist/config/env-config.js +111 -0
  10. package/dist/config/env-config.js.map +1 -0
  11. package/dist/config/pricing-config.d.ts +27 -0
  12. package/dist/config/pricing-config.d.ts.map +1 -0
  13. package/dist/config/pricing-config.js +74 -0
  14. package/dist/config/pricing-config.js.map +1 -0
  15. package/dist/config/runtime-config.d.ts +65 -0
  16. package/dist/config/runtime-config.d.ts.map +1 -0
  17. package/dist/config/runtime-config.js +97 -0
  18. package/dist/config/runtime-config.js.map +1 -0
  19. package/dist/context/context.d.ts +29 -0
  20. package/dist/context/context.d.ts.map +1 -0
  21. package/dist/context/context.js +48 -0
  22. package/dist/context/context.js.map +1 -0
  23. package/dist/exporter/console-exporter.d.ts +18 -0
  24. package/dist/exporter/console-exporter.d.ts.map +1 -0
  25. package/dist/exporter/console-exporter.js +39 -0
  26. package/dist/exporter/console-exporter.js.map +1 -0
  27. package/dist/exporter/http-exporter.d.ts +57 -0
  28. package/dist/exporter/http-exporter.d.ts.map +1 -0
  29. package/dist/exporter/http-exporter.js +181 -0
  30. package/dist/exporter/http-exporter.js.map +1 -0
  31. package/dist/exporter/index.d.ts +7 -0
  32. package/dist/exporter/index.d.ts.map +1 -0
  33. package/dist/exporter/index.js +12 -0
  34. package/dist/exporter/index.js.map +1 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +32 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integrations/index.d.ts +9 -0
  40. package/dist/integrations/index.d.ts.map +1 -0
  41. package/dist/integrations/index.js +16 -0
  42. package/dist/integrations/index.js.map +1 -0
  43. package/dist/integrations/langchain-callback.d.ts +72 -0
  44. package/dist/integrations/langchain-callback.d.ts.map +1 -0
  45. package/dist/integrations/langchain-callback.js +201 -0
  46. package/dist/integrations/langchain-callback.js.map +1 -0
  47. package/dist/integrations/langgraph-instrumentation.d.ts +57 -0
  48. package/dist/integrations/langgraph-instrumentation.d.ts.map +1 -0
  49. package/dist/integrations/langgraph-instrumentation.js +162 -0
  50. package/dist/integrations/langgraph-instrumentation.js.map +1 -0
  51. package/dist/processor/batch-processor.d.ts +68 -0
  52. package/dist/processor/batch-processor.d.ts.map +1 -0
  53. package/dist/processor/batch-processor.js +150 -0
  54. package/dist/processor/batch-processor.js.map +1 -0
  55. package/dist/processor/cost-processor.d.ts +16 -0
  56. package/dist/processor/cost-processor.d.ts.map +1 -0
  57. package/dist/processor/cost-processor.js +50 -0
  58. package/dist/processor/cost-processor.js.map +1 -0
  59. package/dist/processor/index.d.ts +9 -0
  60. package/dist/processor/index.d.ts.map +1 -0
  61. package/dist/processor/index.js +18 -0
  62. package/dist/processor/index.js.map +1 -0
  63. package/dist/processor/logging-processor.d.ts +13 -0
  64. package/dist/processor/logging-processor.d.ts.map +1 -0
  65. package/dist/processor/logging-processor.js +26 -0
  66. package/dist/processor/logging-processor.js.map +1 -0
  67. package/dist/processor/sampler.d.ts +20 -0
  68. package/dist/processor/sampler.d.ts.map +1 -0
  69. package/dist/processor/sampler.js +33 -0
  70. package/dist/processor/sampler.js.map +1 -0
  71. package/dist/processor/token-counter.d.ts +13 -0
  72. package/dist/processor/token-counter.d.ts.map +1 -0
  73. package/dist/processor/token-counter.js +40 -0
  74. package/dist/processor/token-counter.js.map +1 -0
  75. package/dist/tracer/index.d.ts +8 -0
  76. package/dist/tracer/index.d.ts.map +1 -0
  77. package/dist/tracer/index.js +15 -0
  78. package/dist/tracer/index.js.map +1 -0
  79. package/dist/tracer/provider.d.ts +59 -0
  80. package/dist/tracer/provider.d.ts.map +1 -0
  81. package/dist/tracer/provider.js +114 -0
  82. package/dist/tracer/provider.js.map +1 -0
  83. package/dist/tracer/span-context.d.ts +23 -0
  84. package/dist/tracer/span-context.d.ts.map +1 -0
  85. package/dist/tracer/span-context.js +34 -0
  86. package/dist/tracer/span-context.js.map +1 -0
  87. package/dist/tracer/span.d.ts +49 -0
  88. package/dist/tracer/span.d.ts.map +1 -0
  89. package/dist/tracer/span.js +118 -0
  90. package/dist/tracer/span.js.map +1 -0
  91. package/dist/tracer/tracer.d.ts +28 -0
  92. package/dist/tracer/tracer.d.ts.map +1 -0
  93. package/dist/tracer/tracer.js +75 -0
  94. package/dist/tracer/tracer.js.map +1 -0
  95. package/dist/types.d.ts +135 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +16 -0
  98. package/dist/types.js.map +1 -0
  99. package/package.json +79 -0
  100. package/src/__tests__/exporter.test.ts +62 -0
  101. package/src/__tests__/integrations-langchain.test.ts +384 -0
  102. package/src/__tests__/integrations-langgraph.test.ts +479 -0
  103. package/src/__tests__/processor.test.ts +89 -0
  104. package/src/__tests__/span.test.ts +103 -0
  105. package/src/__tests__/tracer.test.ts +89 -0
  106. package/src/auto.ts +198 -0
  107. package/src/config/env-config.ts +93 -0
  108. package/src/config/pricing-config.ts +84 -0
  109. package/src/config/runtime-config.ts +108 -0
  110. package/src/context/context.ts +52 -0
  111. package/src/exporter/console-exporter.ts +38 -0
  112. package/src/exporter/http-exporter.ts +188 -0
  113. package/src/exporter/index.ts +7 -0
  114. package/src/index.ts +51 -0
  115. package/src/integrations/README.md +287 -0
  116. package/src/integrations/index.ts +13 -0
  117. package/src/integrations/langchain-callback.ts +229 -0
  118. package/src/integrations/langgraph-instrumentation.ts +174 -0
  119. package/src/processor/batch-processor.ts +180 -0
  120. package/src/processor/cost-processor.ts +57 -0
  121. package/src/processor/index.ts +9 -0
  122. package/src/processor/logging-processor.ts +26 -0
  123. package/src/processor/sampler.ts +35 -0
  124. package/src/processor/token-counter.ts +42 -0
  125. package/src/tracer/index.ts +8 -0
  126. package/src/tracer/provider.ts +130 -0
  127. package/src/tracer/span-context.ts +46 -0
  128. package/src/tracer/span.ts +145 -0
  129. package/src/tracer/tracer.ts +100 -0
  130. package/src/types.ts +155 -0
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Tests for LangGraph integration
3
+ */
4
+
5
+ import {
6
+ instrumentLangGraph,
7
+ createTracedNode,
8
+ createTracedConditional,
9
+ } from '../integrations/langgraph-instrumentation';
10
+ import { startTracing, stopTracing } from '../auto';
11
+
12
+ describe('LangGraph Integration', () => {
13
+ beforeEach(() => {
14
+ startTracing({
15
+ enableTokenCounting: false,
16
+ enableCostTracking: false,
17
+ });
18
+ });
19
+
20
+ afterEach(() => {
21
+ stopTracing();
22
+ });
23
+
24
+ describe('instrumentLangGraph', () => {
25
+ it('should instrument a graph and override compile', () => {
26
+ const mockGraph = {
27
+ graph_name: 'test-graph',
28
+ compile: jest.fn(() => ({
29
+ invoke: jest.fn(),
30
+ })),
31
+ };
32
+
33
+ const instrumented = instrumentLangGraph(mockGraph, { graphName: 'custom-graph' });
34
+
35
+ expect(instrumented).toBeDefined();
36
+ expect(instrumented.compile).toBeDefined();
37
+ });
38
+
39
+ it('should use provided graph name', () => {
40
+ const mockGraph = {
41
+ compile: jest.fn(() => ({
42
+ invoke: jest.fn(),
43
+ })),
44
+ };
45
+
46
+ const instrumented = instrumentLangGraph(mockGraph, { graphName: 'my-agent' });
47
+ expect(instrumented).toBeDefined();
48
+ });
49
+
50
+ it('should use graph_name if no option provided', () => {
51
+ const mockGraph = {
52
+ graph_name: 'default-graph',
53
+ compile: jest.fn(() => ({
54
+ invoke: jest.fn(),
55
+ })),
56
+ };
57
+
58
+ const instrumented = instrumentLangGraph(mockGraph);
59
+ expect(instrumented).toBeDefined();
60
+ });
61
+
62
+ it('should wrap invoke method', async () => {
63
+ const mockInvoke = jest.fn(async () => ({ result: 'success' }));
64
+ const mockGraph = {
65
+ compile: jest.fn(() => ({
66
+ invoke: mockInvoke,
67
+ })),
68
+ };
69
+
70
+ const instrumented = instrumentLangGraph(mockGraph);
71
+ const compiled = instrumented.compile();
72
+
73
+ expect(compiled.invoke).toBeDefined();
74
+ expect(typeof compiled.invoke).toBe('function');
75
+ });
76
+
77
+ it('should invoke original compile and invoke', async () => {
78
+ const mockInvoke = jest.fn(async () => ({ result: 'success' }));
79
+ const mockCompile = jest.fn(() => ({
80
+ invoke: mockInvoke,
81
+ }));
82
+
83
+ const mockGraph = { compile: mockCompile };
84
+
85
+ const instrumented = instrumentLangGraph(mockGraph);
86
+ const compiled = instrumented.compile();
87
+
88
+ await compiled.invoke({ test: 'input' });
89
+
90
+ expect(mockCompile).toHaveBeenCalled();
91
+ expect(mockInvoke).toHaveBeenCalled();
92
+ });
93
+
94
+ it('should pass through invoke result', async () => {
95
+ const expectedResult = { messages: [{ content: 'response' }] };
96
+ const mockInvoke = jest.fn(async () => expectedResult);
97
+
98
+ const mockGraph = {
99
+ compile: jest.fn(() => ({
100
+ invoke: mockInvoke,
101
+ })),
102
+ };
103
+
104
+ const instrumented = instrumentLangGraph(mockGraph);
105
+ const compiled = instrumented.compile();
106
+ const result = await compiled.invoke({ input: 'test' });
107
+
108
+ expect(result).toEqual(expectedResult);
109
+ });
110
+
111
+ it('should handle invoke errors', async () => {
112
+ const testError = new Error('Invoke failed');
113
+ const mockInvoke = jest.fn(async () => {
114
+ throw testError;
115
+ });
116
+
117
+ const mockGraph = {
118
+ compile: jest.fn(() => ({
119
+ invoke: mockInvoke,
120
+ })),
121
+ };
122
+
123
+ const instrumented = instrumentLangGraph(mockGraph);
124
+ const compiled = instrumented.compile();
125
+
126
+ await expect(compiled.invoke({ input: 'test' })).rejects.toThrow('Invoke failed');
127
+ });
128
+
129
+ it('should wrap stream method if available', async () => {
130
+ const mockStream = jest.fn(async function* () {
131
+ yield { node: 'agent', data: { step: 1 } };
132
+ });
133
+
134
+ const mockGraph = {
135
+ compile: jest.fn(() => ({
136
+ invoke: jest.fn(),
137
+ stream: mockStream,
138
+ })),
139
+ };
140
+
141
+ const instrumented = instrumentLangGraph(mockGraph);
142
+ const compiled = instrumented.compile();
143
+
144
+ expect(compiled.stream).toBeDefined();
145
+ });
146
+
147
+ it('should handle stream with thread_id config', async () => {
148
+ const mockStream = jest.fn(async function* () {
149
+ yield { node: 'agent' };
150
+ });
151
+
152
+ const mockGraph = {
153
+ compile: jest.fn(() => ({
154
+ invoke: jest.fn(),
155
+ stream: mockStream,
156
+ })),
157
+ };
158
+
159
+ const instrumented = instrumentLangGraph(mockGraph);
160
+ const compiled = instrumented.compile();
161
+
162
+ const config = { configurable: { thread_id: 'thread-123' } };
163
+
164
+ // Consume the async generator
165
+ let eventCount = 0;
166
+ for await (const _event of compiled.stream({ input: 'test' }, config)) {
167
+ eventCount++;
168
+ }
169
+
170
+ expect(mockStream).toHaveBeenCalledWith({ input: 'test' }, config);
171
+ expect(eventCount).toBe(1);
172
+ });
173
+
174
+ it('should count stream events', async () => {
175
+ const mockStream = jest.fn(async function* () {
176
+ yield { event: 1 };
177
+ yield { event: 2 };
178
+ yield { event: 3 };
179
+ });
180
+
181
+ const mockGraph = {
182
+ compile: jest.fn(() => ({
183
+ invoke: jest.fn(),
184
+ stream: mockStream,
185
+ })),
186
+ };
187
+
188
+ const instrumented = instrumentLangGraph(mockGraph);
189
+ const compiled = instrumented.compile();
190
+
191
+ let eventCount = 0;
192
+ for await (const _event of compiled.stream({ input: 'test' })) {
193
+ eventCount++;
194
+ }
195
+
196
+ expect(eventCount).toBe(3);
197
+ });
198
+
199
+ it('should handle stream errors', async () => {
200
+ const testError = new Error('Stream failed');
201
+ const mockStream = jest.fn(async function* () {
202
+ throw testError;
203
+ });
204
+
205
+ const mockGraph = {
206
+ compile: jest.fn(() => ({
207
+ invoke: jest.fn(),
208
+ stream: mockStream,
209
+ })),
210
+ };
211
+
212
+ const instrumented = instrumentLangGraph(mockGraph);
213
+ const compiled = instrumented.compile();
214
+
215
+ await expect(async () => {
216
+ for await (const _event of compiled.stream({ input: 'test' })) {
217
+ // Consume stream
218
+ }
219
+ }).rejects.toThrow('Stream failed');
220
+ });
221
+ });
222
+
223
+ describe('createTracedNode', () => {
224
+ it('should create a wrapped node function', () => {
225
+ const nodeFunc = jest.fn(async (_state) => ({ result: 'ok' }));
226
+ const tracedNode = createTracedNode('test-node', nodeFunc);
227
+
228
+ expect(typeof tracedNode).toBe('function');
229
+ });
230
+
231
+ it('should execute the original node function', async () => {
232
+ const nodeFunc = jest.fn(async (state) => ({ updated: state.count + 1 }));
233
+ const tracedNode = createTracedNode('counter', nodeFunc);
234
+
235
+ const state = { count: 5 };
236
+ const result = await tracedNode(state);
237
+
238
+ expect(nodeFunc).toHaveBeenCalledWith(state);
239
+ expect(result).toEqual({ updated: 6 });
240
+ });
241
+
242
+ it('should pass through node output', async () => {
243
+ const expectedOutput = { messages: [{ role: 'assistant', content: 'response' }] };
244
+ const nodeFunc = jest.fn(async () => expectedOutput);
245
+
246
+ const tracedNode = createTracedNode('agent', nodeFunc);
247
+ const result = await tracedNode({ input: 'test' });
248
+
249
+ expect(result).toEqual(expectedOutput);
250
+ });
251
+
252
+ it('should handle node function errors', async () => {
253
+ const nodeError = new Error('Node execution failed');
254
+ const nodeFunc = jest.fn(async () => {
255
+ throw nodeError;
256
+ });
257
+
258
+ const tracedNode = createTracedNode('failing-node', nodeFunc);
259
+
260
+ await expect(tracedNode({ data: 'test' })).rejects.toThrow('Node execution failed');
261
+ });
262
+
263
+ it('should support multiple traced nodes', async () => {
264
+ const agentFunc = jest.fn(async (state) => ({ ...state, agent_output: 'response' }));
265
+ const toolsFunc = jest.fn(async (state) => ({ ...state, tools_output: 'tool-result' }));
266
+
267
+ const agentNode = createTracedNode('agent', agentFunc);
268
+ const toolsNode = createTracedNode('tools', toolsFunc);
269
+
270
+ const state = { messages: [] };
271
+ const result1 = await agentNode(state);
272
+ const result2 = await toolsNode(result1);
273
+
274
+ expect(result2).toHaveProperty('agent_output');
275
+ expect(result2).toHaveProperty('tools_output');
276
+ });
277
+
278
+ it('should handle async node functions', async () => {
279
+ const asyncNode = jest.fn(async (_state) => {
280
+ return new Promise((resolve) =>
281
+ setTimeout(() => resolve({ delayed: true }), 10)
282
+ );
283
+ });
284
+
285
+ const tracedNode = createTracedNode('async-node', asyncNode);
286
+ const result = await tracedNode({ input: 'test' });
287
+
288
+ expect(result).toEqual({ delayed: true });
289
+ });
290
+ });
291
+
292
+ describe('createTracedConditional', () => {
293
+ it('should create a wrapped conditional function', () => {
294
+ const conditionalFunc = jest.fn((_state) => 'next-node');
295
+ const tracedConditional = createTracedConditional('router', conditionalFunc);
296
+
297
+ expect(typeof tracedConditional).toBe('function');
298
+ });
299
+
300
+ it('should execute conditional logic and return route', () => {
301
+ const conditionalFunc = jest.fn((state) => {
302
+ return state.continue ? 'continue' : 'end';
303
+ });
304
+
305
+ const tracedConditional = createTracedConditional('should-continue', conditionalFunc);
306
+
307
+ const result1 = tracedConditional({ continue: true });
308
+ const result2 = tracedConditional({ continue: false });
309
+
310
+ expect(result1).toBe('continue');
311
+ expect(result2).toBe('end');
312
+ });
313
+
314
+ it('should pass through conditional result', () => {
315
+ const route = 'tool-executor';
316
+ const conditionalFunc = jest.fn(() => route);
317
+
318
+ const tracedConditional = createTracedConditional('route', conditionalFunc);
319
+ const result = tracedConditional({ decision_data: 'test' });
320
+
321
+ expect(result).toBe(route);
322
+ });
323
+
324
+ it('should handle conditional errors', () => {
325
+ const conditionError = new Error('Routing failed');
326
+ const conditionalFunc = jest.fn(() => {
327
+ throw conditionError;
328
+ });
329
+
330
+ const tracedConditional = createTracedConditional('failing-route', conditionalFunc);
331
+
332
+ expect(() => tracedConditional({ data: 'test' })).toThrow('Routing failed');
333
+ });
334
+
335
+ it('should support complex routing logic', () => {
336
+ const conditionalFunc = jest.fn((state) => {
337
+ if (state.messages.length > 10) return 'end';
338
+ if (state.needs_tool) return 'tools';
339
+ return 'llm';
340
+ });
341
+
342
+ const tracedConditional = createTracedConditional('complex-route', conditionalFunc);
343
+
344
+ expect(tracedConditional({ messages: [], needs_tool: true })).toBe('tools');
345
+ expect(tracedConditional({ messages: [], needs_tool: false })).toBe('llm');
346
+ expect(tracedConditional({ messages: Array(15) })).toBe('end');
347
+ });
348
+
349
+ it('should handle different route values', () => {
350
+ const conditionalFunc = jest.fn((state) => {
351
+ const routes = ['node-a', 'node-b', 'node-c', 'end'];
352
+ return routes[state.selector % routes.length];
353
+ });
354
+
355
+ const tracedConditional = createTracedConditional('multi-route', conditionalFunc);
356
+
357
+ expect(tracedConditional({ selector: 0 })).toBe('node-a');
358
+ expect(tracedConditional({ selector: 1 })).toBe('node-b');
359
+ expect(tracedConditional({ selector: 2 })).toBe('node-c');
360
+ expect(tracedConditional({ selector: 3 })).toBe('end');
361
+ });
362
+ });
363
+
364
+ describe('Integration Scenarios', () => {
365
+ it('should support full graph instrumentation flow', async () => {
366
+ const agentNode = jest.fn(async (state) => ({ ...state, agent_done: true }));
367
+ const toolsNode = jest.fn(async (state) => ({ ...state, tools_done: true }));
368
+
369
+ const conditionalFunc = jest.fn((state) => (state.agent_done ? 'tools' : 'agent'));
370
+
371
+ const tracedAgent = createTracedNode('agent', agentNode);
372
+ const tracedTools = createTracedNode('tools', toolsNode);
373
+ const tracedRoute = createTracedConditional('route', conditionalFunc);
374
+
375
+ const mockGraph = {
376
+ compile: jest.fn(() => ({
377
+ invoke: jest.fn(),
378
+ })),
379
+ };
380
+
381
+ const instrumented = instrumentLangGraph(mockGraph);
382
+
383
+ // Verify all tracing functions work together
384
+ expect(tracedAgent).toBeDefined();
385
+ expect(tracedTools).toBeDefined();
386
+ expect(tracedRoute).toBeDefined();
387
+ expect(instrumented).toBeDefined();
388
+ });
389
+
390
+ it('should handle state flowing through traced nodes', async () => {
391
+ const state1 = { count: 1, steps: ['start'] };
392
+
393
+ const _nodeA = createTracedNode('node-a', async (s) => ({
394
+ ...s,
395
+ count: s.count + 1,
396
+ steps: [...s.steps, 'a'],
397
+ }));
398
+
399
+ const _nodeB = createTracedNode('node-b', async (s) => ({
400
+ ...s,
401
+ count: s.count * 2,
402
+ steps: [...s.steps, 'b'],
403
+ }));
404
+
405
+ const result = await _nodeB(await _nodeA(state1));
406
+
407
+ expect(result.count).toBe(4); // (1+1)*2
408
+ expect(result.steps).toEqual(['start', 'a', 'b']);
409
+ });
410
+
411
+ it('should handle conditional routing between traced nodes', async () => {
412
+ const router = createTracedConditional('route', (state) => {
413
+ return state.go_to_b ? 'b' : 'a';
414
+ });
415
+
416
+ const initialState = { data: 'test' };
417
+
418
+ const route1 = router(initialState);
419
+ const route2 = router({ ...initialState, go_to_b: true });
420
+
421
+ expect(route1).toBe('a');
422
+ expect(route2).toBe('b');
423
+ });
424
+ });
425
+
426
+ describe('Edge Cases', () => {
427
+ it('should handle null state in traced node', async () => {
428
+ const nodeFunc = jest.fn(async (state) => state || {});
429
+ const tracedNode = createTracedNode('nullable-node', nodeFunc);
430
+
431
+ const result = await tracedNode(null);
432
+ expect(result).toBeDefined();
433
+ });
434
+
435
+ it('should handle empty graph name', () => {
436
+ const mockGraph = {
437
+ compile: jest.fn(() => ({ invoke: jest.fn() })),
438
+ };
439
+
440
+ const instrumented = instrumentLangGraph(mockGraph, { graphName: '' });
441
+ expect(instrumented).toBeDefined();
442
+ });
443
+
444
+ it('should handle graph without stream method', async () => {
445
+ const mockGraph = {
446
+ compile: jest.fn(() => ({
447
+ invoke: jest.fn(async () => ({ result: 'ok' })),
448
+ // No stream method
449
+ })),
450
+ };
451
+
452
+ const instrumented = instrumentLangGraph(mockGraph);
453
+ const compiled = instrumented.compile();
454
+
455
+ expect(compiled.invoke).toBeDefined();
456
+ expect(compiled.stream).toBeUndefined();
457
+ });
458
+
459
+ it('should handle undefined config in stream', async () => {
460
+ const mockStream = jest.fn(async function* () {
461
+ yield { data: 'test' };
462
+ });
463
+
464
+ const mockGraph = {
465
+ compile: jest.fn(() => ({
466
+ invoke: jest.fn(),
467
+ stream: mockStream,
468
+ })),
469
+ };
470
+
471
+ const instrumented = instrumentLangGraph(mockGraph);
472
+ const compiled = instrumented.compile();
473
+
474
+ for await (const event of compiled.stream({ input: 'test' })) {
475
+ expect(event).toBeDefined();
476
+ }
477
+ });
478
+ });
479
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tests for processors.
3
+ */
4
+
5
+ import { Sampler } from '../processor/sampler';
6
+ import { TokenCountingProcessor } from '../processor/token-counter';
7
+ import { CostAnnotatingProcessor } from '../processor/cost-processor';
8
+ import { LoggingSpanProcessor } from '../processor/logging-processor';
9
+ import { TracerProvider } from '../tracer/provider';
10
+
11
+ describe('Sampler', () => {
12
+ it('should reject invalid sample rates', () => {
13
+ expect(() => new Sampler(-0.1)).toThrow();
14
+ expect(() => new Sampler(1.1)).toThrow();
15
+ });
16
+
17
+ it('should always sample at rate 1.0', () => {
18
+ const sampler = new Sampler(1.0);
19
+ for (let i = 0; i < 10; i++) {
20
+ expect(sampler.shouldSample().sampled).toBe(true);
21
+ }
22
+ });
23
+
24
+ it('should never sample at rate 0.0', () => {
25
+ const sampler = new Sampler(0.0);
26
+ for (let i = 0; i < 10; i++) {
27
+ expect(sampler.shouldSample().sampled).toBe(false);
28
+ }
29
+ });
30
+ });
31
+
32
+ describe('TokenCountingProcessor', () => {
33
+ it('should estimate tokens from text', () => {
34
+ const processor = new TokenCountingProcessor();
35
+ const provider = new TracerProvider();
36
+ const tracer = provider.getTracer('test');
37
+ const span = tracer.startSpan('test', {
38
+ attributes: {
39
+ prompt: 'a'.repeat(1000),
40
+ completion: 'b'.repeat(500),
41
+ },
42
+ });
43
+
44
+ span.end();
45
+ processor.onEnd(span);
46
+
47
+ expect(span.attributes.input_tokens).toBeDefined();
48
+ expect(span.attributes.output_tokens).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe('CostAnnotatingProcessor', () => {
53
+ it('should calculate costs', () => {
54
+ const processor = new CostAnnotatingProcessor({
55
+ 'gpt-3.5-turbo': {
56
+ inputCost: 0.0005,
57
+ outputCost: 0.0015,
58
+ },
59
+ });
60
+
61
+ const provider = new TracerProvider();
62
+ const tracer = provider.getTracer('test');
63
+ const span = tracer.startSpan('test', {
64
+ attributes: {
65
+ model: 'gpt-3.5-turbo',
66
+ input_tokens: 100,
67
+ output_tokens: 50,
68
+ },
69
+ });
70
+
71
+ span.end();
72
+ processor.onEnd(span);
73
+
74
+ expect(span.attributes.cost_usd).toBeDefined();
75
+ expect((span.attributes.cost_usd as number) > 0).toBe(true);
76
+ });
77
+ });
78
+
79
+ describe('LoggingSpanProcessor', () => {
80
+ it('should not throw on span end', () => {
81
+ const processor = new LoggingSpanProcessor();
82
+ const provider = new TracerProvider();
83
+ const tracer = provider.getTracer('test');
84
+ const span = tracer.startSpan('test');
85
+
86
+ span.end();
87
+ expect(() => processor.onEnd(span)).not.toThrow();
88
+ });
89
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tests for span and span context.
3
+ */
4
+
5
+ import { SpanContext } from '../tracer/span-context';
6
+ import { TracerProvider } from '../tracer/provider';
7
+ import { SpanStatus } from '../types';
8
+
9
+ describe('SpanContext', () => {
10
+ it('should create a valid span context', () => {
11
+ const ctx = new SpanContext('trace123', 'span456', 1, 'state=value');
12
+ expect(ctx.traceId).toBe('trace123');
13
+ expect(ctx.spanId).toBe('span456');
14
+ expect(ctx.traceFlags).toBe(1);
15
+ expect(ctx.traceState).toBe('state=value');
16
+ });
17
+
18
+ it('should validate context', () => {
19
+ const valid = new SpanContext('a'.repeat(32), 'b'.repeat(16));
20
+ expect(valid.isValid()).toBe(true);
21
+
22
+ const invalid = new SpanContext('0'.repeat(32), 'b'.repeat(16));
23
+ expect(invalid.isValid()).toBe(false);
24
+ });
25
+
26
+ it('should determine if sampled', () => {
27
+ const sampled = new SpanContext('trace', 'span', 1);
28
+ expect(sampled.isSampled()).toBe(true);
29
+
30
+ const notSampled = new SpanContext('trace', 'span', 0);
31
+ expect(notSampled.isSampled()).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe('Span', () => {
36
+ let provider: TracerProvider;
37
+
38
+ beforeEach(() => {
39
+ provider = new TracerProvider();
40
+ });
41
+
42
+ it('should create a span with attributes', () => {
43
+ const tracer = provider.getTracer('test');
44
+ const span = tracer.startSpan('test-span', {
45
+ attributes: { key: 'value' },
46
+ });
47
+
48
+ expect(span.name).toBe('test-span');
49
+ expect(span.attributes.key).toBe('value');
50
+ expect(span.isRecording()).toBe(true);
51
+ });
52
+
53
+ it('should set attributes on span', () => {
54
+ const tracer = provider.getTracer('test');
55
+ const span = tracer.startSpan('test-span');
56
+
57
+ span.setAttribute('foo', 'bar');
58
+ expect(span.attributes.foo).toBe('bar');
59
+ });
60
+
61
+ it('should add events to span', () => {
62
+ const tracer = provider.getTracer('test');
63
+ const span = tracer.startSpan('test-span');
64
+
65
+ span.addEvent('event1', { data: 'value' });
66
+ expect(span.events).toHaveLength(1);
67
+ expect(span.events[0].name).toBe('event1');
68
+ });
69
+
70
+ it('should record exceptions', () => {
71
+ const tracer = provider.getTracer('test');
72
+ const span = tracer.startSpan('test-span');
73
+
74
+ const error = new Error('Test error');
75
+ span.recordException(error);
76
+
77
+ expect(span.status).toBe(SpanStatus.ERROR);
78
+ expect(span.statusDescription).toBe('Test error');
79
+ });
80
+
81
+ it('should end span and calculate duration', () => {
82
+ const tracer = provider.getTracer('test');
83
+ const span = tracer.startSpan('test-span');
84
+
85
+ expect(span.endTimeNs).toBeUndefined();
86
+ span.end();
87
+ expect(span.endTimeNs).toBeDefined();
88
+ expect(span.durationNs).toBeGreaterThan(0);
89
+ expect(span.isRecording()).toBe(false);
90
+ });
91
+
92
+ it('should not allow modifications after end', () => {
93
+ const tracer = provider.getTracer('test');
94
+ const span = tracer.startSpan('test-span');
95
+
96
+ span.end();
97
+ span.setAttribute('foo', 'bar');
98
+ span.addEvent('event');
99
+
100
+ expect(span.attributes.foo).toBeUndefined();
101
+ expect(span.events).toHaveLength(0);
102
+ });
103
+ });