@mastra/langfuse 0.0.4 → 0.0.5-alpha.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.
@@ -1,663 +0,0 @@
1
- /**
2
- * Langfuse Exporter Tests
3
- *
4
- * These tests focus on Langfuse-specific functionality:
5
- * - Langfuse client interactions
6
- * - Mapping logic (spans -> traces/generations/spans)
7
- * - Type-specific metadata extraction
8
- * - Langfuse-specific error handling
9
- */
10
-
11
- import type { AITracingEvent, AnyAISpan, LLMGenerationAttributes, ToolCallAttributes } from '@mastra/core/ai-tracing';
12
- import { AISpanType, AITracingEventType } from '@mastra/core/ai-tracing';
13
- import { Langfuse } from 'langfuse';
14
- import { beforeEach, describe, expect, it, vi } from 'vitest';
15
- import { LangfuseExporter } from './ai-tracing';
16
- import type { LangfuseExporterConfig } from './ai-tracing';
17
-
18
- // Mock Langfuse constructor (must be at the top level)
19
- vi.mock('langfuse');
20
-
21
- describe('LangfuseExporter', () => {
22
- // Mock objects
23
- let mockGeneration: any;
24
- let mockSpan: any;
25
- let mockTrace: any;
26
- let mockLangfuseClient: any;
27
- let LangfuseMock: any;
28
-
29
- let exporter: LangfuseExporter;
30
- let config: LangfuseExporterConfig;
31
-
32
- beforeEach(() => {
33
- vi.clearAllMocks();
34
-
35
- // Set up mocks
36
- mockGeneration = {
37
- update: vi.fn(),
38
- end: vi.fn(),
39
- };
40
-
41
- mockSpan = {
42
- update: vi.fn(),
43
- end: vi.fn(),
44
- generation: vi.fn().mockReturnValue(mockGeneration),
45
- span: vi.fn(),
46
- };
47
-
48
- mockTrace = {
49
- generation: vi.fn().mockReturnValue(mockGeneration),
50
- span: vi.fn().mockReturnValue(mockSpan),
51
- update: vi.fn(),
52
- };
53
-
54
- // Set up circular reference
55
- mockSpan.span.mockReturnValue(mockSpan);
56
-
57
- mockLangfuseClient = {
58
- trace: vi.fn().mockReturnValue(mockTrace),
59
- shutdownAsync: vi.fn().mockResolvedValue(undefined),
60
- };
61
-
62
- // Get the mocked Langfuse constructor and configure it
63
- LangfuseMock = vi.mocked(Langfuse);
64
- LangfuseMock.mockImplementation(() => mockLangfuseClient);
65
-
66
- config = {
67
- publicKey: 'test-public-key',
68
- secretKey: 'test-secret-key',
69
- baseUrl: 'https://test-langfuse.com',
70
- options: {
71
- debug: false,
72
- flushAt: 1,
73
- flushInterval: 1000,
74
- },
75
- };
76
-
77
- exporter = new LangfuseExporter(config);
78
- });
79
-
80
- describe('Initialization', () => {
81
- it('should initialize with correct configuration', () => {
82
- expect(exporter.name).toBe('langfuse');
83
- // Verify Langfuse client was created with correct config
84
- expect(LangfuseMock).toHaveBeenCalledWith({
85
- publicKey: 'test-public-key',
86
- secretKey: 'test-secret-key',
87
- baseUrl: 'https://test-langfuse.com',
88
- debug: false,
89
- flushAt: 1,
90
- flushInterval: 1000,
91
- });
92
- });
93
- });
94
-
95
- describe('Trace Creation', () => {
96
- it('should create Langfuse trace for root spans', async () => {
97
- const rootSpan = createMockSpan({
98
- id: 'root-span-id',
99
- name: 'root-agent',
100
- type: AISpanType.AGENT_RUN,
101
- isRoot: true,
102
- attributes: {
103
- agentId: 'agent-123',
104
- instructions: 'Test agent',
105
- spanType: 'agent_run',
106
- },
107
- metadata: { userId: 'user-456', sessionId: 'session-789' },
108
- });
109
-
110
- const event: AITracingEvent = {
111
- type: AITracingEventType.SPAN_STARTED,
112
- span: rootSpan,
113
- };
114
-
115
- await exporter.exportEvent(event);
116
-
117
- // Should create Langfuse trace with correct parameters
118
- expect(mockLangfuseClient.trace).toHaveBeenCalledWith({
119
- id: 'root-span-id', // Uses span.trace.id
120
- name: 'root-agent',
121
- userId: 'user-456',
122
- sessionId: 'session-789',
123
- metadata: {
124
- agentId: 'agent-123',
125
- instructions: 'Test agent',
126
- spanType: 'agent_run',
127
- },
128
- });
129
- });
130
-
131
- it('should not create trace for child spans', async () => {
132
- const childSpan = createMockSpan({
133
- id: 'child-span-id',
134
- name: 'child-tool',
135
- type: AISpanType.TOOL_CALL,
136
- isRoot: false,
137
- attributes: { toolId: 'calculator' },
138
- });
139
-
140
- const event: AITracingEvent = {
141
- type: AITracingEventType.SPAN_STARTED,
142
- span: childSpan,
143
- };
144
-
145
- await exporter.exportEvent(event);
146
-
147
- // Should not create trace for child spans
148
- expect(mockLangfuseClient.trace).not.toHaveBeenCalled();
149
- });
150
- });
151
-
152
- describe('LLM Generation Mapping', () => {
153
- it('should create Langfuse generation for LLM_GENERATION spans', async () => {
154
- const llmSpan = createMockSpan({
155
- id: 'llm-span-id',
156
- name: 'gpt-4-call',
157
- type: AISpanType.LLM_GENERATION,
158
- isRoot: true,
159
- input: { messages: [{ role: 'user', content: 'Hello' }] },
160
- output: { content: 'Hi there!' },
161
- attributes: {
162
- model: 'gpt-4',
163
- provider: 'openai',
164
- usage: {
165
- promptTokens: 10,
166
- completionTokens: 5,
167
- totalTokens: 15,
168
- },
169
- parameters: {
170
- temperature: 0.7,
171
- maxTokens: 100,
172
- topP: 0.9,
173
- },
174
- streaming: false,
175
- resultType: 'response_generation',
176
- },
177
- });
178
-
179
- const event: AITracingEvent = {
180
- type: AITracingEventType.SPAN_STARTED,
181
- span: llmSpan,
182
- };
183
-
184
- await exporter.exportEvent(event);
185
-
186
- // Should create Langfuse generation with LLM-specific fields
187
- expect(mockTrace.generation).toHaveBeenCalledWith({
188
- id: 'llm-span-id',
189
- name: 'gpt-4-call',
190
- startTime: llmSpan.startTime,
191
- model: 'gpt-4',
192
- modelParameters: {
193
- temperature: 0.7,
194
- maxTokens: 100,
195
- topP: 0.9,
196
- },
197
- input: { messages: [{ role: 'user', content: 'Hello' }] },
198
- output: { content: 'Hi there!' },
199
- usage: {
200
- promptTokens: 10,
201
- completionTokens: 5,
202
- totalTokens: 15,
203
- },
204
- metadata: {
205
- provider: 'openai',
206
- resultType: 'response_generation',
207
- spanType: 'llm_generation',
208
- streaming: false,
209
- },
210
- });
211
- });
212
-
213
- it('should handle LLM spans without optional fields', async () => {
214
- const minimalLlmSpan = createMockSpan({
215
- id: 'minimal-llm',
216
- name: 'simple-llm',
217
- type: AISpanType.LLM_GENERATION,
218
- isRoot: true,
219
- attributes: {
220
- model: 'gpt-3.5-turbo',
221
- // No usage, parameters, input, output, etc.
222
- },
223
- });
224
-
225
- const event: AITracingEvent = {
226
- type: AITracingEventType.SPAN_STARTED,
227
- span: minimalLlmSpan,
228
- };
229
-
230
- await exporter.exportEvent(event);
231
-
232
- expect(mockTrace.generation).toHaveBeenCalledWith({
233
- id: 'minimal-llm',
234
- name: 'simple-llm',
235
- startTime: minimalLlmSpan.startTime,
236
- model: 'gpt-3.5-turbo',
237
- metadata: {
238
- spanType: 'llm_generation',
239
- },
240
- });
241
- });
242
- });
243
-
244
- describe('Regular Span Mapping', () => {
245
- it('should create Langfuse span for non-LLM span types', async () => {
246
- const toolSpan = createMockSpan({
247
- id: 'tool-span-id',
248
- name: 'calculator-tool',
249
- type: AISpanType.TOOL_CALL,
250
- isRoot: true,
251
- input: { operation: 'add', a: 2, b: 3 },
252
- output: { result: 5 },
253
- attributes: {
254
- toolId: 'calculator',
255
- success: true,
256
- },
257
- });
258
-
259
- const event: AITracingEvent = {
260
- type: AITracingEventType.SPAN_STARTED,
261
- span: toolSpan,
262
- };
263
-
264
- await exporter.exportEvent(event);
265
-
266
- expect(mockTrace.span).toHaveBeenCalledWith({
267
- id: 'tool-span-id',
268
- name: 'calculator-tool',
269
- startTime: toolSpan.startTime,
270
- input: { operation: 'add', a: 2, b: 3 },
271
- output: { result: 5 },
272
- metadata: {
273
- spanType: 'tool_call',
274
- toolId: 'calculator',
275
- success: true,
276
- },
277
- });
278
- });
279
- });
280
-
281
- describe('Type-Specific Metadata Extraction', () => {
282
- it('should extract agent-specific metadata', async () => {
283
- const agentSpan = createMockSpan({
284
- id: 'agent-span',
285
- name: 'customer-agent',
286
- type: AISpanType.AGENT_RUN,
287
- isRoot: true,
288
- attributes: {
289
- agentId: 'agent-456',
290
- availableTools: ['search', 'calculator'],
291
- maxSteps: 10,
292
- currentStep: 3,
293
- instructions: 'Help customers',
294
- },
295
- });
296
-
297
- const event: AITracingEvent = {
298
- type: AITracingEventType.SPAN_STARTED,
299
- span: agentSpan,
300
- };
301
-
302
- await exporter.exportEvent(event);
303
-
304
- expect(mockTrace.span).toHaveBeenCalledWith(
305
- expect.objectContaining({
306
- metadata: expect.objectContaining({
307
- spanType: 'agent_run',
308
- agentId: 'agent-456',
309
- availableTools: ['search', 'calculator'],
310
- maxSteps: 10,
311
- currentStep: 3,
312
- }),
313
- }),
314
- );
315
- });
316
-
317
- it('should extract MCP tool-specific metadata', async () => {
318
- const mcpSpan = createMockSpan({
319
- id: 'mcp-span',
320
- name: 'mcp-tool-call',
321
- type: AISpanType.MCP_TOOL_CALL,
322
- isRoot: true,
323
- attributes: {
324
- toolId: 'file-reader',
325
- mcpServer: 'filesystem-mcp',
326
- serverVersion: '1.0.0',
327
- success: true,
328
- },
329
- });
330
-
331
- const event: AITracingEvent = {
332
- type: AITracingEventType.SPAN_STARTED,
333
- span: mcpSpan,
334
- };
335
-
336
- await exporter.exportEvent(event);
337
-
338
- expect(mockTrace.span).toHaveBeenCalledWith(
339
- expect.objectContaining({
340
- metadata: expect.objectContaining({
341
- spanType: 'mcp_tool_call',
342
- toolId: 'file-reader',
343
- mcpServer: 'filesystem-mcp',
344
- serverVersion: '1.0.0',
345
- success: true,
346
- }),
347
- }),
348
- );
349
- });
350
-
351
- it('should extract workflow-specific metadata', async () => {
352
- const workflowSpan = createMockSpan({
353
- id: 'workflow-span',
354
- name: 'data-processing-workflow',
355
- type: AISpanType.WORKFLOW_RUN,
356
- isRoot: true,
357
- attributes: {
358
- workflowId: 'wf-123',
359
- status: 'running',
360
- },
361
- });
362
-
363
- const event: AITracingEvent = {
364
- type: AITracingEventType.SPAN_STARTED,
365
- span: workflowSpan,
366
- };
367
-
368
- await exporter.exportEvent(event);
369
-
370
- expect(mockTrace.span).toHaveBeenCalledWith(
371
- expect.objectContaining({
372
- metadata: expect.objectContaining({
373
- spanType: 'workflow_run',
374
- workflowId: 'wf-123',
375
- status: 'running',
376
- }),
377
- }),
378
- );
379
- });
380
- });
381
-
382
- describe('Span Updates', () => {
383
- it('should update LLM generation with new data', async () => {
384
- // First, start a span
385
- const llmSpan = createMockSpan({
386
- id: 'llm-span',
387
- name: 'gpt-4-call',
388
- type: AISpanType.LLM_GENERATION,
389
- isRoot: true,
390
- attributes: { model: 'gpt-4' },
391
- });
392
-
393
- await exporter.exportEvent({
394
- type: AITracingEventType.SPAN_STARTED,
395
- span: llmSpan,
396
- });
397
-
398
- // Then update it
399
- llmSpan.attributes = {
400
- ...llmSpan.attributes,
401
- usage: { totalTokens: 150 },
402
- } as LLMGenerationAttributes;
403
- llmSpan.output = { content: 'Updated response' };
404
-
405
- await exporter.exportEvent({
406
- type: AITracingEventType.SPAN_UPDATED,
407
- span: llmSpan,
408
- });
409
-
410
- expect(mockGeneration.update).toHaveBeenCalledWith({
411
- metadata: expect.objectContaining({
412
- spanType: 'llm_generation',
413
- }),
414
- model: 'gpt-4',
415
- output: { content: 'Updated response' },
416
- usage: {
417
- totalTokens: 150,
418
- },
419
- });
420
- });
421
-
422
- it('should update regular spans', async () => {
423
- const toolSpan = createMockSpan({
424
- id: 'tool-span',
425
- name: 'calculator',
426
- type: AISpanType.TOOL_CALL,
427
- isRoot: true,
428
- attributes: { toolId: 'calc', success: false },
429
- });
430
-
431
- await exporter.exportEvent({
432
- type: AITracingEventType.SPAN_STARTED,
433
- span: toolSpan,
434
- });
435
-
436
- // Update with success
437
- toolSpan.attributes = {
438
- ...toolSpan.attributes,
439
- success: true,
440
- } as ToolCallAttributes;
441
- toolSpan.output = { result: 42 };
442
-
443
- await exporter.exportEvent({
444
- type: AITracingEventType.SPAN_UPDATED,
445
- span: toolSpan,
446
- });
447
-
448
- expect(mockSpan.update).toHaveBeenCalledWith({
449
- metadata: expect.objectContaining({
450
- spanType: 'tool_call',
451
- success: true,
452
- }),
453
- output: { result: 42 },
454
- });
455
- });
456
- });
457
-
458
- describe('Span Ending', () => {
459
- it('should end span with success status', async () => {
460
- const span = createMockSpan({
461
- id: 'test-span',
462
- name: 'test',
463
- type: AISpanType.GENERIC,
464
- isRoot: true,
465
- attributes: {},
466
- });
467
-
468
- await exporter.exportEvent({
469
- type: AITracingEventType.SPAN_STARTED,
470
- span,
471
- });
472
-
473
- span.endTime = new Date();
474
-
475
- await exporter.exportEvent({
476
- type: AITracingEventType.SPAN_ENDED,
477
- span,
478
- });
479
-
480
- console.log('BOOP');
481
- console.log(span.metadata);
482
-
483
- expect(mockSpan.end).toHaveBeenCalledWith({
484
- endTime: span.endTime,
485
- metadata: expect.objectContaining({
486
- spanType: 'generic',
487
- }),
488
- });
489
- });
490
-
491
- it('should end span with error status', async () => {
492
- const errorSpan = createMockSpan({
493
- id: 'error-span',
494
- name: 'failing-operation',
495
- type: AISpanType.TOOL_CALL,
496
- isRoot: true,
497
- attributes: {
498
- toolId: 'failing-tool',
499
- },
500
- errorInfo: {
501
- message: 'Tool execution failed',
502
- id: 'TOOL_ERROR',
503
- category: 'EXECUTION',
504
- },
505
- });
506
-
507
- errorSpan.endTime = new Date();
508
-
509
- await exporter.exportEvent({
510
- type: AITracingEventType.SPAN_STARTED,
511
- span: errorSpan,
512
- });
513
-
514
- await exporter.exportEvent({
515
- type: AITracingEventType.SPAN_ENDED,
516
- span: errorSpan,
517
- });
518
-
519
- expect(mockSpan.end).toHaveBeenCalledWith({
520
- endTime: errorSpan.endTime,
521
- metadata: expect.objectContaining({
522
- spanType: 'tool_call',
523
- toolId: 'failing-tool',
524
- }),
525
- level: 'ERROR',
526
- statusMessage: 'Tool execution failed',
527
- });
528
- });
529
- });
530
-
531
- describe('Error Handling', () => {
532
- it('should handle missing traces gracefully', async () => {
533
- const orphanSpan = createMockSpan({
534
- id: 'orphan-span',
535
- name: 'orphan',
536
- type: AISpanType.TOOL_CALL,
537
- isRoot: false, // Child span without parent trace
538
- attributes: { toolId: 'orphan-tool' },
539
- });
540
-
541
- // Should not throw when trying to create child span without trace
542
- await expect(
543
- exporter.exportEvent({
544
- type: AITracingEventType.SPAN_STARTED,
545
- span: orphanSpan,
546
- }),
547
- ).resolves.not.toThrow();
548
-
549
- // Should not create Langfuse span
550
- expect(mockTrace.span).not.toHaveBeenCalled();
551
- expect(mockTrace.generation).not.toHaveBeenCalled();
552
- });
553
-
554
- it('should handle missing Langfuse objects gracefully', async () => {
555
- const span = createMockSpan({
556
- id: 'missing-span',
557
- name: 'missing',
558
- type: AISpanType.GENERIC,
559
- isRoot: true,
560
- attributes: {},
561
- });
562
-
563
- // Try to update non-existent span
564
- await expect(
565
- exporter.exportEvent({
566
- type: AITracingEventType.SPAN_UPDATED,
567
- span,
568
- }),
569
- ).resolves.not.toThrow();
570
-
571
- // Try to end non-existent span
572
- await expect(
573
- exporter.exportEvent({
574
- type: AITracingEventType.SPAN_ENDED,
575
- span,
576
- }),
577
- ).resolves.not.toThrow();
578
- });
579
- });
580
-
581
- describe('Shutdown', () => {
582
- it('should shutdown Langfuse client and clear maps', async () => {
583
- // Add some data to internal maps
584
- const span = createMockSpan({
585
- id: 'test-span',
586
- name: 'test',
587
- type: AISpanType.GENERIC,
588
- isRoot: true,
589
- attributes: {},
590
- });
591
-
592
- await exporter.exportEvent({
593
- type: AITracingEventType.SPAN_STARTED,
594
- span,
595
- });
596
-
597
- // Verify maps have data
598
- expect((exporter as any).traceMap.size).toBeGreaterThan(0);
599
- expect((exporter as any).traceMap.get('test-span').spans.size).toBeGreaterThan(0);
600
-
601
- // Shutdown
602
- await exporter.shutdown();
603
-
604
- // Verify Langfuse client shutdown was called
605
- expect(mockLangfuseClient.shutdownAsync).toHaveBeenCalled();
606
-
607
- // Verify maps were cleared
608
- expect((exporter as any).traceMap.size).toBe(0);
609
- });
610
- });
611
- });
612
-
613
- // Helper function to create mock spans
614
- function createMockSpan({
615
- id,
616
- name,
617
- type,
618
- isRoot,
619
- attributes,
620
- metadata,
621
- input,
622
- output,
623
- errorInfo,
624
- }: {
625
- id: string;
626
- name: string;
627
- type: AISpanType;
628
- isRoot: boolean;
629
- attributes: any;
630
- metadata?: Record<string, any>;
631
- input?: any;
632
- output?: any;
633
- errorInfo?: any;
634
- }): AnyAISpan {
635
- const mockSpan = {
636
- id,
637
- name,
638
- type,
639
- attributes,
640
- metadata,
641
- input,
642
- output,
643
- errorInfo,
644
- startTime: new Date(),
645
- endTime: undefined,
646
- traceId: isRoot ? id : 'parent-trace-id',
647
- get isRootSpan() {
648
- return isRoot;
649
- },
650
- trace: {
651
- id: isRoot ? id : 'parent-trace-id',
652
- traceId: isRoot ? id : 'parent-trace-id',
653
- } as AnyAISpan,
654
- parent: isRoot ? undefined : { id: 'parent-id' },
655
- aiTracing: {} as any,
656
- end: vi.fn(),
657
- error: vi.fn(),
658
- update: vi.fn(),
659
- createChildSpan: vi.fn(),
660
- } as AnyAISpan;
661
-
662
- return mockSpan;
663
- }