@mastra/langfuse 0.0.5-alpha.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @mastra/langfuse
2
2
 
3
+ ## 0.0.5-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#7343](https://github.com/mastra-ai/mastra/pull/7343) [`de3cbc6`](https://github.com/mastra-ai/mastra/commit/de3cbc61079211431bd30487982ea3653517278e) Thanks [@LekoArts](https://github.com/LekoArts)! - Update the `package.json` file to include additional fields like `repository`, `homepage` or `files`.
8
+
9
+ - Updated dependencies [[`85ef90b`](https://github.com/mastra-ai/mastra/commit/85ef90bb2cd4ae4df855c7ac175f7d392c55c1bf), [`de3cbc6`](https://github.com/mastra-ai/mastra/commit/de3cbc61079211431bd30487982ea3653517278e)]:
10
+ - @mastra/core@0.15.3-alpha.5
11
+
3
12
  ## 0.0.5-alpha.0
4
13
 
5
14
  ### Patch Changes
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@mastra/langfuse",
3
- "version": "0.0.5-alpha.0",
3
+ "version": "0.0.5-alpha.1",
4
4
  "description": "Langfuse observability provider for Mastra - includes AI tracing and future observability features",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "CHANGELOG.md"
11
+ ],
8
12
  "exports": {
9
13
  ".": {
10
14
  "import": {
@@ -29,13 +33,22 @@
29
33
  "tsup": "^8.5.0",
30
34
  "typescript": "^5.8.3",
31
35
  "vitest": "^3.2.4",
36
+ "@mastra/core": "0.15.3-alpha.5",
32
37
  "@internal/lint": "0.0.34",
33
- "@internal/types-builder": "0.0.9",
34
- "@mastra/core": "0.15.3-alpha.4"
38
+ "@internal/types-builder": "0.0.9"
35
39
  },
36
40
  "peerDependencies": {
37
41
  "@mastra/core": ">=0.15.3-0 <0.16.0-0"
38
42
  },
43
+ "homepage": "https://mastra.ai",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/mastra-ai/mastra.git",
47
+ "directory": "observability/langfuse"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/mastra-ai/mastra/issues"
51
+ },
39
52
  "scripts": {
40
53
  "build": "tsup --silent --config tsup.config.ts",
41
54
  "build:watch": "pnpm build --watch",
@@ -1,4 +0,0 @@
1
-
2
- > @mastra/langfuse@0.0.5-alpha.0 build /home/runner/work/mastra/mastra/observability/langfuse
3
- > tsup --silent --config tsup.config.ts
4
-
package/eslint.config.js DELETED
@@ -1,6 +0,0 @@
1
- import { createConfig } from '@internal/lint/eslint';
2
-
3
- const config = await createConfig();
4
-
5
- /** @type {import("eslint").Linter.Config[]} */
6
- export default config;
@@ -1,829 +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
- event: vi.fn(),
39
- };
40
-
41
- mockSpan = {
42
- update: vi.fn(),
43
- generation: vi.fn().mockReturnValue(mockGeneration),
44
- span: vi.fn(),
45
- event: vi.fn(),
46
- };
47
-
48
- mockTrace = {
49
- generation: vi.fn().mockReturnValue(mockGeneration),
50
- span: vi.fn().mockReturnValue(mockSpan),
51
- update: vi.fn(),
52
- event: vi.fn(),
53
- };
54
-
55
- // Set up circular reference
56
- mockSpan.span.mockReturnValue(mockSpan);
57
-
58
- mockLangfuseClient = {
59
- trace: vi.fn().mockReturnValue(mockTrace),
60
- shutdownAsync: vi.fn().mockResolvedValue(undefined),
61
- };
62
-
63
- // Get the mocked Langfuse constructor and configure it
64
- LangfuseMock = vi.mocked(Langfuse);
65
- LangfuseMock.mockImplementation(() => mockLangfuseClient);
66
-
67
- config = {
68
- publicKey: 'test-public-key',
69
- secretKey: 'test-secret-key',
70
- baseUrl: 'https://test-langfuse.com',
71
- options: {
72
- debug: false,
73
- flushAt: 1,
74
- flushInterval: 1000,
75
- },
76
- };
77
-
78
- exporter = new LangfuseExporter(config);
79
- });
80
-
81
- describe('Initialization', () => {
82
- it('should initialize with correct configuration', () => {
83
- expect(exporter.name).toBe('langfuse');
84
- // Verify Langfuse client was created with correct config
85
- expect(LangfuseMock).toHaveBeenCalledWith({
86
- publicKey: 'test-public-key',
87
- secretKey: 'test-secret-key',
88
- baseUrl: 'https://test-langfuse.com',
89
- debug: false,
90
- flushAt: 1,
91
- flushInterval: 1000,
92
- });
93
- });
94
- });
95
-
96
- describe('Trace Creation', () => {
97
- it('should create Langfuse trace for root spans', async () => {
98
- const rootSpan = createMockSpan({
99
- id: 'root-span-id',
100
- name: 'root-agent',
101
- type: AISpanType.AGENT_RUN,
102
- isRoot: true,
103
- attributes: {
104
- agentId: 'agent-123',
105
- instructions: 'Test agent',
106
- spanType: 'agent_run',
107
- },
108
- metadata: { userId: 'user-456', sessionId: 'session-789' },
109
- });
110
-
111
- const event: AITracingEvent = {
112
- type: AITracingEventType.SPAN_STARTED,
113
- span: rootSpan,
114
- };
115
-
116
- await exporter.exportEvent(event);
117
-
118
- // Should create Langfuse trace with correct parameters
119
- expect(mockLangfuseClient.trace).toHaveBeenCalledWith({
120
- id: 'root-span-id', // Uses span.trace.id
121
- name: 'root-agent',
122
- userId: 'user-456',
123
- sessionId: 'session-789',
124
- metadata: {
125
- agentId: 'agent-123',
126
- instructions: 'Test agent',
127
- spanType: 'agent_run',
128
- },
129
- });
130
- });
131
-
132
- it('should not create trace for child spans', async () => {
133
- const childSpan = createMockSpan({
134
- id: 'child-span-id',
135
- name: 'child-tool',
136
- type: AISpanType.TOOL_CALL,
137
- isRoot: false,
138
- attributes: { toolId: 'calculator' },
139
- });
140
-
141
- const event: AITracingEvent = {
142
- type: AITracingEventType.SPAN_STARTED,
143
- span: childSpan,
144
- };
145
-
146
- await exporter.exportEvent(event);
147
-
148
- // Should not create trace for child spans
149
- expect(mockLangfuseClient.trace).not.toHaveBeenCalled();
150
- });
151
- });
152
-
153
- describe('LLM Generation Mapping', () => {
154
- it('should create Langfuse generation for LLM_GENERATION spans', async () => {
155
- const llmSpan = createMockSpan({
156
- id: 'llm-span-id',
157
- name: 'gpt-4-call',
158
- type: AISpanType.LLM_GENERATION,
159
- isRoot: true,
160
- input: { messages: [{ role: 'user', content: 'Hello' }] },
161
- output: { content: 'Hi there!' },
162
- attributes: {
163
- model: 'gpt-4',
164
- provider: 'openai',
165
- usage: {
166
- promptTokens: 10,
167
- completionTokens: 5,
168
- totalTokens: 15,
169
- },
170
- parameters: {
171
- temperature: 0.7,
172
- maxTokens: 100,
173
- topP: 0.9,
174
- },
175
- streaming: false,
176
- resultType: 'response_generation',
177
- },
178
- });
179
-
180
- const event: AITracingEvent = {
181
- type: AITracingEventType.SPAN_STARTED,
182
- span: llmSpan,
183
- };
184
-
185
- await exporter.exportEvent(event);
186
-
187
- // Should create Langfuse generation with LLM-specific fields
188
- expect(mockTrace.generation).toHaveBeenCalledWith({
189
- id: 'llm-span-id',
190
- name: 'gpt-4-call',
191
- startTime: llmSpan.startTime,
192
- model: 'gpt-4',
193
- modelParameters: {
194
- temperature: 0.7,
195
- maxTokens: 100,
196
- topP: 0.9,
197
- },
198
- input: { messages: [{ role: 'user', content: 'Hello' }] },
199
- output: { content: 'Hi there!' },
200
- usage: {
201
- promptTokens: 10,
202
- completionTokens: 5,
203
- totalTokens: 15,
204
- },
205
- metadata: {
206
- provider: 'openai',
207
- resultType: 'response_generation',
208
- spanType: 'llm_generation',
209
- streaming: false,
210
- },
211
- });
212
- });
213
-
214
- it('should handle LLM spans without optional fields', async () => {
215
- const minimalLlmSpan = createMockSpan({
216
- id: 'minimal-llm',
217
- name: 'simple-llm',
218
- type: AISpanType.LLM_GENERATION,
219
- isRoot: true,
220
- attributes: {
221
- model: 'gpt-3.5-turbo',
222
- // No usage, parameters, input, output, etc.
223
- },
224
- });
225
-
226
- const event: AITracingEvent = {
227
- type: AITracingEventType.SPAN_STARTED,
228
- span: minimalLlmSpan,
229
- };
230
-
231
- await exporter.exportEvent(event);
232
-
233
- expect(mockTrace.generation).toHaveBeenCalledWith({
234
- id: 'minimal-llm',
235
- name: 'simple-llm',
236
- startTime: minimalLlmSpan.startTime,
237
- model: 'gpt-3.5-turbo',
238
- metadata: {
239
- spanType: 'llm_generation',
240
- },
241
- });
242
- });
243
- });
244
-
245
- describe('Regular Span Mapping', () => {
246
- it('should create Langfuse span for non-LLM span types', async () => {
247
- const toolSpan = createMockSpan({
248
- id: 'tool-span-id',
249
- name: 'calculator-tool',
250
- type: AISpanType.TOOL_CALL,
251
- isRoot: true,
252
- input: { operation: 'add', a: 2, b: 3 },
253
- output: { result: 5 },
254
- attributes: {
255
- toolId: 'calculator',
256
- success: true,
257
- },
258
- });
259
-
260
- const event: AITracingEvent = {
261
- type: AITracingEventType.SPAN_STARTED,
262
- span: toolSpan,
263
- };
264
-
265
- await exporter.exportEvent(event);
266
-
267
- expect(mockTrace.span).toHaveBeenCalledWith({
268
- id: 'tool-span-id',
269
- name: 'calculator-tool',
270
- startTime: toolSpan.startTime,
271
- input: { operation: 'add', a: 2, b: 3 },
272
- output: { result: 5 },
273
- metadata: {
274
- spanType: 'tool_call',
275
- toolId: 'calculator',
276
- success: true,
277
- },
278
- });
279
- });
280
- });
281
-
282
- describe('Type-Specific Metadata Extraction', () => {
283
- it('should extract agent-specific metadata', async () => {
284
- const agentSpan = createMockSpan({
285
- id: 'agent-span',
286
- name: 'customer-agent',
287
- type: AISpanType.AGENT_RUN,
288
- isRoot: true,
289
- attributes: {
290
- agentId: 'agent-456',
291
- availableTools: ['search', 'calculator'],
292
- maxSteps: 10,
293
- currentStep: 3,
294
- instructions: 'Help customers',
295
- },
296
- });
297
-
298
- const event: AITracingEvent = {
299
- type: AITracingEventType.SPAN_STARTED,
300
- span: agentSpan,
301
- };
302
-
303
- await exporter.exportEvent(event);
304
-
305
- expect(mockTrace.span).toHaveBeenCalledWith(
306
- expect.objectContaining({
307
- metadata: expect.objectContaining({
308
- spanType: 'agent_run',
309
- agentId: 'agent-456',
310
- availableTools: ['search', 'calculator'],
311
- maxSteps: 10,
312
- currentStep: 3,
313
- }),
314
- }),
315
- );
316
- });
317
-
318
- it('should extract MCP tool-specific metadata', async () => {
319
- const mcpSpan = createMockSpan({
320
- id: 'mcp-span',
321
- name: 'mcp-tool-call',
322
- type: AISpanType.MCP_TOOL_CALL,
323
- isRoot: true,
324
- attributes: {
325
- toolId: 'file-reader',
326
- mcpServer: 'filesystem-mcp',
327
- serverVersion: '1.0.0',
328
- success: true,
329
- },
330
- });
331
-
332
- const event: AITracingEvent = {
333
- type: AITracingEventType.SPAN_STARTED,
334
- span: mcpSpan,
335
- };
336
-
337
- await exporter.exportEvent(event);
338
-
339
- expect(mockTrace.span).toHaveBeenCalledWith(
340
- expect.objectContaining({
341
- metadata: expect.objectContaining({
342
- spanType: 'mcp_tool_call',
343
- toolId: 'file-reader',
344
- mcpServer: 'filesystem-mcp',
345
- serverVersion: '1.0.0',
346
- success: true,
347
- }),
348
- }),
349
- );
350
- });
351
-
352
- it('should extract workflow-specific metadata', async () => {
353
- const workflowSpan = createMockSpan({
354
- id: 'workflow-span',
355
- name: 'data-processing-workflow',
356
- type: AISpanType.WORKFLOW_RUN,
357
- isRoot: true,
358
- attributes: {
359
- workflowId: 'wf-123',
360
- status: 'running',
361
- },
362
- });
363
-
364
- const event: AITracingEvent = {
365
- type: AITracingEventType.SPAN_STARTED,
366
- span: workflowSpan,
367
- };
368
-
369
- await exporter.exportEvent(event);
370
-
371
- expect(mockTrace.span).toHaveBeenCalledWith(
372
- expect.objectContaining({
373
- metadata: expect.objectContaining({
374
- spanType: 'workflow_run',
375
- workflowId: 'wf-123',
376
- status: 'running',
377
- }),
378
- }),
379
- );
380
- });
381
- });
382
-
383
- describe('Span Updates', () => {
384
- it('should update LLM generation with new data', async () => {
385
- // First, start a span
386
- const llmSpan = createMockSpan({
387
- id: 'llm-span',
388
- name: 'gpt-4-call',
389
- type: AISpanType.LLM_GENERATION,
390
- isRoot: true,
391
- attributes: { model: 'gpt-4' },
392
- });
393
-
394
- await exporter.exportEvent({
395
- type: AITracingEventType.SPAN_STARTED,
396
- span: llmSpan,
397
- });
398
-
399
- // Then update it
400
- llmSpan.attributes = {
401
- ...llmSpan.attributes,
402
- usage: { totalTokens: 150 },
403
- } as LLMGenerationAttributes;
404
- llmSpan.output = { content: 'Updated response' };
405
-
406
- await exporter.exportEvent({
407
- type: AITracingEventType.SPAN_UPDATED,
408
- span: llmSpan,
409
- });
410
-
411
- expect(mockGeneration.update).toHaveBeenCalledWith({
412
- metadata: expect.objectContaining({
413
- spanType: 'llm_generation',
414
- }),
415
- model: 'gpt-4',
416
- output: { content: 'Updated response' },
417
- usage: {
418
- totalTokens: 150,
419
- },
420
- });
421
- });
422
-
423
- it('should update regular spans', async () => {
424
- const toolSpan = createMockSpan({
425
- id: 'tool-span',
426
- name: 'calculator',
427
- type: AISpanType.TOOL_CALL,
428
- isRoot: true,
429
- attributes: { toolId: 'calc', success: false },
430
- });
431
-
432
- await exporter.exportEvent({
433
- type: AITracingEventType.SPAN_STARTED,
434
- span: toolSpan,
435
- });
436
-
437
- // Update with success
438
- toolSpan.attributes = {
439
- ...toolSpan.attributes,
440
- success: true,
441
- } as ToolCallAttributes;
442
- toolSpan.output = { result: 42 };
443
-
444
- await exporter.exportEvent({
445
- type: AITracingEventType.SPAN_UPDATED,
446
- span: toolSpan,
447
- });
448
-
449
- expect(mockSpan.update).toHaveBeenCalledWith({
450
- metadata: expect.objectContaining({
451
- spanType: 'tool_call',
452
- success: true,
453
- }),
454
- output: { result: 42 },
455
- });
456
- });
457
- });
458
-
459
- describe('Span Ending', () => {
460
- it('should update span with endTime on span end', async () => {
461
- const span = createMockSpan({
462
- id: 'test-span',
463
- name: 'test',
464
- type: AISpanType.GENERIC,
465
- isRoot: true,
466
- attributes: {},
467
- });
468
-
469
- await exporter.exportEvent({
470
- type: AITracingEventType.SPAN_STARTED,
471
- span,
472
- });
473
-
474
- span.endTime = new Date();
475
-
476
- await exporter.exportEvent({
477
- type: AITracingEventType.SPAN_ENDED,
478
- span,
479
- });
480
-
481
- expect(mockSpan.update).toHaveBeenCalledWith({
482
- endTime: span.endTime,
483
- metadata: expect.objectContaining({
484
- spanType: 'generic',
485
- }),
486
- });
487
- });
488
-
489
- it('should update span with error information on span end', async () => {
490
- const errorSpan = createMockSpan({
491
- id: 'error-span',
492
- name: 'failing-operation',
493
- type: AISpanType.TOOL_CALL,
494
- isRoot: true,
495
- attributes: {
496
- toolId: 'failing-tool',
497
- },
498
- errorInfo: {
499
- message: 'Tool execution failed',
500
- id: 'TOOL_ERROR',
501
- category: 'EXECUTION',
502
- },
503
- });
504
-
505
- errorSpan.endTime = new Date();
506
-
507
- await exporter.exportEvent({
508
- type: AITracingEventType.SPAN_STARTED,
509
- span: errorSpan,
510
- });
511
-
512
- await exporter.exportEvent({
513
- type: AITracingEventType.SPAN_ENDED,
514
- span: errorSpan,
515
- });
516
-
517
- expect(mockSpan.update).toHaveBeenCalledWith({
518
- endTime: errorSpan.endTime,
519
- metadata: expect.objectContaining({
520
- spanType: 'tool_call',
521
- toolId: 'failing-tool',
522
- }),
523
- level: 'ERROR',
524
- statusMessage: 'Tool execution failed',
525
- });
526
- });
527
-
528
- it('should update root trace and delete from traceMap when root span ends', async () => {
529
- const rootSpan = createMockSpan({
530
- id: 'root-span-id',
531
- name: 'root-span',
532
- type: AISpanType.AGENT_RUN,
533
- isRoot: true,
534
- attributes: {},
535
- });
536
-
537
- rootSpan.output = { result: 'success' };
538
- rootSpan.endTime = new Date();
539
-
540
- await exporter.exportEvent({
541
- type: AITracingEventType.SPAN_STARTED,
542
- span: rootSpan,
543
- });
544
-
545
- // Verify trace was created
546
- expect((exporter as any).traceMap.has('root-span-id')).toBe(true);
547
-
548
- await exporter.exportEvent({
549
- type: AITracingEventType.SPAN_ENDED,
550
- span: rootSpan,
551
- });
552
-
553
- // Should update trace with output
554
- expect(mockTrace.update).toHaveBeenCalledWith({
555
- output: { result: 'success' },
556
- });
557
-
558
- // Should remove trace from traceMap
559
- expect((exporter as any).traceMap.has('root-span-id')).toBe(false);
560
- });
561
- });
562
-
563
- describe('Error Handling', () => {
564
- it('should handle missing traces gracefully', async () => {
565
- const orphanSpan = createMockSpan({
566
- id: 'orphan-span',
567
- name: 'orphan',
568
- type: AISpanType.TOOL_CALL,
569
- isRoot: false, // Child span without parent trace
570
- attributes: { toolId: 'orphan-tool' },
571
- });
572
-
573
- // Should not throw when trying to create child span without trace
574
- await expect(
575
- exporter.exportEvent({
576
- type: AITracingEventType.SPAN_STARTED,
577
- span: orphanSpan,
578
- }),
579
- ).resolves.not.toThrow();
580
-
581
- // Should not create Langfuse span
582
- expect(mockTrace.span).not.toHaveBeenCalled();
583
- expect(mockTrace.generation).not.toHaveBeenCalled();
584
- });
585
-
586
- it('should handle missing Langfuse objects gracefully', async () => {
587
- const span = createMockSpan({
588
- id: 'missing-span',
589
- name: 'missing',
590
- type: AISpanType.GENERIC,
591
- isRoot: true,
592
- attributes: {},
593
- });
594
-
595
- // Try to update non-existent span
596
- await expect(
597
- exporter.exportEvent({
598
- type: AITracingEventType.SPAN_UPDATED,
599
- span,
600
- }),
601
- ).resolves.not.toThrow();
602
-
603
- // Try to end non-existent span
604
- await expect(
605
- exporter.exportEvent({
606
- type: AITracingEventType.SPAN_ENDED,
607
- span,
608
- }),
609
- ).resolves.not.toThrow();
610
- });
611
- });
612
-
613
- describe('Event Span Handling', () => {
614
- let mockEvent: any;
615
-
616
- beforeEach(() => {
617
- mockEvent = {
618
- update: vi.fn(),
619
- };
620
- mockTrace.event.mockReturnValue(mockEvent);
621
- mockSpan.event.mockReturnValue(mockEvent);
622
- mockGeneration.event.mockReturnValue(mockEvent);
623
- });
624
-
625
- it('should create Langfuse event for root event spans', async () => {
626
- const eventSpan = createMockSpan({
627
- id: 'event-span-id',
628
- name: 'user-feedback',
629
- type: AISpanType.GENERIC,
630
- isRoot: true,
631
- attributes: {
632
- eventType: 'user_feedback',
633
- rating: 5,
634
- },
635
- input: { message: 'Great response!' },
636
- });
637
- eventSpan.isEvent = true;
638
-
639
- await exporter.exportEvent({
640
- type: AITracingEventType.SPAN_STARTED,
641
- span: eventSpan,
642
- });
643
-
644
- // Should create trace for root event span
645
- expect(mockLangfuseClient.trace).toHaveBeenCalledWith({
646
- id: 'event-span-id',
647
- name: 'user-feedback',
648
- input: { message: 'Great response!' },
649
- metadata: {
650
- spanType: 'generic',
651
- eventType: 'user_feedback',
652
- rating: 5,
653
- },
654
- });
655
-
656
- // Should create Langfuse event
657
- expect(mockTrace.event).toHaveBeenCalledWith({
658
- id: 'event-span-id',
659
- name: 'user-feedback',
660
- startTime: eventSpan.startTime,
661
- input: { message: 'Great response!' },
662
- metadata: {
663
- spanType: 'generic',
664
- eventType: 'user_feedback',
665
- rating: 5,
666
- },
667
- });
668
- });
669
-
670
- it('should create Langfuse event for child event spans', async () => {
671
- // First create a root span
672
- const rootSpan = createMockSpan({
673
- id: 'root-span-id',
674
- name: 'root-agent',
675
- type: AISpanType.AGENT_RUN,
676
- isRoot: true,
677
- attributes: {},
678
- });
679
-
680
- await exporter.exportEvent({
681
- type: AITracingEventType.SPAN_STARTED,
682
- span: rootSpan,
683
- });
684
-
685
- // Then create a child event span
686
- const childEventSpan = createMockSpan({
687
- id: 'child-event-id',
688
- name: 'tool-result',
689
- type: AISpanType.GENERIC,
690
- isRoot: false,
691
- attributes: {
692
- toolName: 'calculator',
693
- success: true,
694
- },
695
- output: { result: 42 },
696
- });
697
- childEventSpan.isEvent = true;
698
- childEventSpan.traceId = 'root-span-id';
699
- childEventSpan.parent = { id: 'root-span-id' };
700
-
701
- await exporter.exportEvent({
702
- type: AITracingEventType.SPAN_STARTED,
703
- span: childEventSpan,
704
- });
705
-
706
- // Should create event under the parent span
707
- expect(mockSpan.event).toHaveBeenCalledWith({
708
- id: 'child-event-id',
709
- name: 'tool-result',
710
- startTime: childEventSpan.startTime,
711
- output: { result: 42 },
712
- metadata: {
713
- spanType: 'generic',
714
- toolName: 'calculator',
715
- success: true,
716
- },
717
- });
718
- });
719
-
720
- it('should handle event spans with missing parent gracefully', async () => {
721
- const orphanEventSpan = createMockSpan({
722
- id: 'orphan-event-id',
723
- name: 'orphan-event',
724
- type: AISpanType.GENERIC,
725
- isRoot: false,
726
- attributes: {},
727
- });
728
- orphanEventSpan.isEvent = true;
729
- orphanEventSpan.traceId = 'missing-trace-id';
730
-
731
- // Should not throw
732
- await expect(
733
- exporter.exportEvent({
734
- type: AITracingEventType.SPAN_STARTED,
735
- span: orphanEventSpan,
736
- }),
737
- ).resolves.not.toThrow();
738
-
739
- // Should not create any Langfuse objects
740
- expect(mockTrace.event).not.toHaveBeenCalled();
741
- expect(mockSpan.event).not.toHaveBeenCalled();
742
- });
743
- });
744
-
745
- describe('Shutdown', () => {
746
- it('should shutdown Langfuse client and clear maps', async () => {
747
- // Add some data to internal maps
748
- const span = createMockSpan({
749
- id: 'test-span',
750
- name: 'test',
751
- type: AISpanType.GENERIC,
752
- isRoot: true,
753
- attributes: {},
754
- });
755
-
756
- await exporter.exportEvent({
757
- type: AITracingEventType.SPAN_STARTED,
758
- span,
759
- });
760
-
761
- // Verify maps have data
762
- expect((exporter as any).traceMap.size).toBeGreaterThan(0);
763
- expect((exporter as any).traceMap.get('test-span').spans.size).toBeGreaterThan(0);
764
-
765
- // Shutdown
766
- await exporter.shutdown();
767
-
768
- // Verify Langfuse client shutdown was called
769
- expect(mockLangfuseClient.shutdownAsync).toHaveBeenCalled();
770
-
771
- // Verify maps were cleared
772
- expect((exporter as any).traceMap.size).toBe(0);
773
- });
774
- });
775
- });
776
-
777
- // Helper function to create mock spans
778
- function createMockSpan({
779
- id,
780
- name,
781
- type,
782
- isRoot,
783
- attributes,
784
- metadata,
785
- input,
786
- output,
787
- errorInfo,
788
- }: {
789
- id: string;
790
- name: string;
791
- type: AISpanType;
792
- isRoot: boolean;
793
- attributes: any;
794
- metadata?: Record<string, any>;
795
- input?: any;
796
- output?: any;
797
- errorInfo?: any;
798
- }): AnyAISpan {
799
- const mockSpan = {
800
- id,
801
- name,
802
- type,
803
- attributes,
804
- metadata,
805
- input,
806
- output,
807
- errorInfo,
808
- startTime: new Date(),
809
- endTime: undefined,
810
- traceId: isRoot ? id : 'parent-trace-id',
811
- get isRootSpan() {
812
- return isRoot;
813
- },
814
- trace: {
815
- id: isRoot ? id : 'parent-trace-id',
816
- traceId: isRoot ? id : 'parent-trace-id',
817
- } as AnyAISpan,
818
- parent: isRoot ? undefined : { id: 'parent-id' },
819
- aiTracing: {} as any,
820
- end: vi.fn(),
821
- error: vi.fn(),
822
- update: vi.fn(),
823
- createChildSpan: vi.fn(),
824
- createEventSpan: vi.fn(),
825
- isEvent: false,
826
- } as AnyAISpan;
827
-
828
- return mockSpan;
829
- }
package/src/ai-tracing.ts DELETED
@@ -1,291 +0,0 @@
1
- /**
2
- * Langfuse Exporter for Mastra AI Tracing
3
- *
4
- * This exporter sends tracing data to Langfuse for AI observability.
5
- * Root spans start traces in Langfuse.
6
- * LLM_GENERATION spans become Langfuse generations, all others become spans.
7
- */
8
-
9
- import type { AITracingExporter, AITracingEvent, AnyAISpan, LLMGenerationAttributes } from '@mastra/core/ai-tracing';
10
- import { AISpanType, omitKeys } from '@mastra/core/ai-tracing';
11
- import { ConsoleLogger } from '@mastra/core/logger';
12
- import { Langfuse } from 'langfuse';
13
- import type { LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient, LangfuseEventClient } from 'langfuse';
14
-
15
- export interface LangfuseExporterConfig {
16
- /** Langfuse API key */
17
- publicKey: string;
18
- /** Langfuse secret key */
19
- secretKey: string;
20
- /** Langfuse host URL */
21
- baseUrl: string;
22
- /** Enable realtime mode - flushes after each event for immediate visibility */
23
- realtime?: boolean;
24
- /** Logger level for diagnostic messages (default: 'warn') */
25
- logLevel?: 'debug' | 'info' | 'warn' | 'error';
26
- /** Additional options to pass to the Langfuse client */
27
- options?: any;
28
- }
29
-
30
- type TraceData = {
31
- trace: LangfuseTraceClient; // Langfuse trace object
32
- spans: Map<string, LangfuseSpanClient | LangfuseGenerationClient>; // Maps span.id to Langfuse span/generation
33
- events: Map<string, LangfuseEventClient>; // Maps span.id to Langfuse event
34
- };
35
-
36
- type LangfuseParent = LangfuseTraceClient | LangfuseSpanClient | LangfuseGenerationClient | LangfuseEventClient;
37
-
38
- export class LangfuseExporter implements AITracingExporter {
39
- name = 'langfuse';
40
- private client: Langfuse;
41
- private realtime: boolean;
42
- private traceMap = new Map<string, TraceData>();
43
- private logger: ConsoleLogger;
44
-
45
- constructor(config: LangfuseExporterConfig) {
46
- this.realtime = config.realtime ?? false;
47
- this.logger = new ConsoleLogger({ level: config.logLevel ?? 'warn' });
48
- this.client = new Langfuse({
49
- publicKey: config.publicKey,
50
- secretKey: config.secretKey,
51
- baseUrl: config.baseUrl,
52
- ...config.options,
53
- });
54
- }
55
-
56
- async exportEvent(event: AITracingEvent): Promise<void> {
57
- if (event.span.isEvent) {
58
- await this.handleEventSpan(event.span);
59
- return;
60
- }
61
-
62
- switch (event.type) {
63
- case 'span_started':
64
- await this.handleSpanStarted(event.span);
65
- break;
66
- case 'span_updated':
67
- await this.handleSpanUpdateOrEnd(event.span, false);
68
- break;
69
- case 'span_ended':
70
- await this.handleSpanUpdateOrEnd(event.span, true);
71
- break;
72
- }
73
-
74
- // Flush immediately in realtime mode for instant visibility
75
- if (this.realtime) {
76
- await this.client.flushAsync();
77
- }
78
- }
79
-
80
- private async handleSpanStarted(span: AnyAISpan): Promise<void> {
81
- if (span.isRootSpan) {
82
- this.initTrace(span);
83
- }
84
- const method = 'handleSpanStarted';
85
-
86
- const traceData = this.getTraceData({ span, method });
87
- if (!traceData) {
88
- return;
89
- }
90
-
91
- const langfuseParent = this.getLangfuseParent({ traceData, span, method });
92
- if (!langfuseParent) {
93
- return;
94
- }
95
-
96
- const payload = this.buildSpanPayload(span, true);
97
-
98
- const langfuseSpan =
99
- span.type === AISpanType.LLM_GENERATION ? langfuseParent.generation(payload) : langfuseParent.span(payload);
100
-
101
- traceData.spans.set(span.id, langfuseSpan);
102
- }
103
-
104
- private async handleSpanUpdateOrEnd(span: AnyAISpan, isEnd: boolean): Promise<void> {
105
- const method = isEnd ? 'handleSpanEnd' : 'handleSpanUpdate';
106
-
107
- const traceData = this.getTraceData({ span, method });
108
- if (!traceData) {
109
- return;
110
- }
111
-
112
- const langfuseSpan = traceData.spans.get(span.id);
113
- if (!langfuseSpan) {
114
- this.logger.warn('Langfuse exporter: No Langfuse span found for span update/end', {
115
- traceId: span.traceId,
116
- spanId: span.id,
117
- spanName: span.name,
118
- spanType: span.type,
119
- isRootSpan: span.isRootSpan,
120
- parentSpanId: span.parent?.id,
121
- availableSpanIds: Array.from(traceData.spans.keys()),
122
- method,
123
- });
124
- return;
125
- }
126
-
127
- // use update for both update & end, so that we can use the
128
- // end time we set when ending the span.
129
- langfuseSpan.update(this.buildSpanPayload(span, false));
130
-
131
- if (isEnd && span.isRootSpan) {
132
- traceData.trace.update({ output: span.output });
133
- this.traceMap.delete(span.traceId);
134
- }
135
- }
136
-
137
- private async handleEventSpan(span: AnyAISpan): Promise<void> {
138
- if (span.isRootSpan) {
139
- this.logger.debug('Langfuse exporter: Creating trace', {
140
- traceId: span.traceId,
141
- spanId: span.id,
142
- spanName: span.name,
143
- method: 'handleEventSpan',
144
- });
145
- this.initTrace(span);
146
- }
147
- const method = 'handleEventSpan';
148
-
149
- const traceData = this.getTraceData({ span, method });
150
- if (!traceData) {
151
- return;
152
- }
153
-
154
- const langfuseParent = this.getLangfuseParent({ traceData, span, method });
155
- if (!langfuseParent) {
156
- return;
157
- }
158
-
159
- const payload = this.buildSpanPayload(span, true);
160
-
161
- const langfuseEvent = langfuseParent.event(payload);
162
-
163
- traceData.events.set(span.id, langfuseEvent);
164
- }
165
-
166
- private initTrace(span: AnyAISpan): void {
167
- const trace = this.client.trace(this.buildTracePayload(span));
168
- this.traceMap.set(span.traceId, { trace, spans: new Map(), events: new Map() });
169
- }
170
-
171
- private getTraceData(options: { span: AnyAISpan; method: string }): TraceData | undefined {
172
- const { span, method } = options;
173
- if (this.traceMap.has(span.traceId)) {
174
- return this.traceMap.get(span.traceId);
175
- }
176
- this.logger.warn('Langfuse exporter: No trace data found for span', {
177
- traceId: span.traceId,
178
- spanId: span.id,
179
- spanName: span.name,
180
- spanType: span.type,
181
- isRootSpan: span.isRootSpan,
182
- parentSpanId: span.parent?.id,
183
- method,
184
- });
185
- }
186
-
187
- private getLangfuseParent(options: {
188
- traceData: TraceData;
189
- span: AnyAISpan;
190
- method: string;
191
- }): LangfuseParent | undefined {
192
- const { traceData, span, method } = options;
193
-
194
- const parentId = span.parent?.id;
195
- if (!parentId) {
196
- return traceData.trace;
197
- }
198
- if (traceData.spans.has(parentId)) {
199
- return traceData.spans.get(parentId);
200
- }
201
- if (traceData.events.has(parentId)) {
202
- return traceData.events.get(parentId);
203
- }
204
- this.logger.warn('Langfuse exporter: No parent data found for span', {
205
- traceId: span.traceId,
206
- spanId: span.id,
207
- spanName: span.name,
208
- spanType: span.type,
209
- isRootSpan: span.isRootSpan,
210
- parentSpanId: span.parent?.id,
211
- method,
212
- });
213
- }
214
-
215
- private buildTracePayload(span: AnyAISpan): Record<string, any> {
216
- const payload: Record<string, any> = {
217
- id: span.traceId,
218
- name: span.name,
219
- };
220
-
221
- const { userId, sessionId, ...remainingMetadata } = span.metadata ?? {};
222
-
223
- if (userId) payload.userId = userId;
224
- if (sessionId) payload.sessionId = sessionId;
225
- if (span.input) payload.input = span.input;
226
-
227
- payload.metadata = {
228
- spanType: span.type,
229
- ...span.attributes,
230
- ...remainingMetadata,
231
- };
232
-
233
- return payload;
234
- }
235
-
236
- private buildSpanPayload(span: AnyAISpan, isCreate: boolean): Record<string, any> {
237
- const payload: Record<string, any> = {};
238
-
239
- if (isCreate) {
240
- payload.id = span.id;
241
- payload.name = span.name;
242
- payload.startTime = span.startTime;
243
- if (span.input !== undefined) payload.input = span.input;
244
- }
245
-
246
- if (span.output !== undefined) payload.output = span.output;
247
- if (span.endTime !== undefined) payload.endTime = span.endTime;
248
-
249
- const attributes = (span.attributes ?? {}) as Record<string, any>;
250
-
251
- // Strip special fields from metadata if used in top-level keys
252
- const attributesToOmit: string[] = [];
253
-
254
- if (span.type === AISpanType.LLM_GENERATION) {
255
- const llmAttr = attributes as LLMGenerationAttributes;
256
-
257
- if (llmAttr.model !== undefined) {
258
- payload.model = llmAttr.model;
259
- attributesToOmit.push('model');
260
- }
261
-
262
- if (llmAttr.usage !== undefined) {
263
- payload.usage = llmAttr.usage;
264
- attributesToOmit.push('usage');
265
- }
266
-
267
- if (llmAttr.parameters !== undefined) {
268
- payload.modelParameters = llmAttr.parameters;
269
- attributesToOmit.push('parameters');
270
- }
271
- }
272
-
273
- payload.metadata = {
274
- spanType: span.type,
275
- ...omitKeys(attributes, attributesToOmit),
276
- ...span.metadata,
277
- };
278
-
279
- if (span.errorInfo) {
280
- payload.level = 'ERROR';
281
- payload.statusMessage = span.errorInfo.message;
282
- }
283
-
284
- return payload;
285
- }
286
-
287
- async shutdown(): Promise<void> {
288
- await this.client.shutdownAsync();
289
- this.traceMap.clear();
290
- }
291
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Langfuse Observability Provider for Mastra
3
- *
4
- * This package provides Langfuse-specific observability features for Mastra applications.
5
- * Currently includes AI tracing support with plans for additional observability features.
6
- */
7
-
8
- // AI Tracing
9
- export * from './ai-tracing';
@@ -1,9 +0,0 @@
1
- {
2
- "extends": ["./tsconfig.json", "../../tsconfig.build.json"],
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"],
8
- "exclude": ["node_modules", "**/*.test.ts", "src/**/*.mock.ts"]
9
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.node.json",
3
- "include": ["src/**/*", "tsup.config.ts"],
4
- "exclude": ["node_modules", "**/*.test.ts"]
5
- }
package/tsup.config.ts DELETED
@@ -1,17 +0,0 @@
1
- import { generateTypes } from '@internal/types-builder';
2
- import { defineConfig } from 'tsup';
3
-
4
- export default defineConfig({
5
- entry: ['src/index.ts'],
6
- format: ['esm', 'cjs'],
7
- clean: true,
8
- dts: false,
9
- splitting: true,
10
- treeshake: {
11
- preset: 'smallest',
12
- },
13
- sourcemap: true,
14
- onSuccess: async () => {
15
- await generateTypes(process.cwd());
16
- },
17
- });
package/vitest.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: 'node',
6
- include: ['src/**/*.test.ts'],
7
- coverage: {
8
- reporter: ['text', 'json', 'html'],
9
- },
10
- },
11
- });