@mastra/langfuse 0.0.5-alpha.0 → 0.0.5
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 +59 -0
- package/package.json +17 -4
- package/.turbo/turbo-build.log +0 -4
- package/eslint.config.js +0 -6
- package/src/ai-tracing.test.ts +0 -829
- package/src/ai-tracing.ts +0 -291
- package/src/index.ts +0 -9
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -17
- package/vitest.config.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# @mastra/langfuse
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- de3cbc6: Update the `package.json` file to include additional fields like `repository`, `homepage` or `files`.
|
|
8
|
+
- 979912c: "Updated langfuse exporter to handle Event spans"
|
|
9
|
+
- Updated dependencies [ab48c97]
|
|
10
|
+
- Updated dependencies [85ef90b]
|
|
11
|
+
- Updated dependencies [aedbbfa]
|
|
12
|
+
- Updated dependencies [ff89505]
|
|
13
|
+
- Updated dependencies [637f323]
|
|
14
|
+
- Updated dependencies [de3cbc6]
|
|
15
|
+
- Updated dependencies [c19bcf7]
|
|
16
|
+
- Updated dependencies [4474d04]
|
|
17
|
+
- Updated dependencies [183dc95]
|
|
18
|
+
- Updated dependencies [a1111e2]
|
|
19
|
+
- Updated dependencies [b42a961]
|
|
20
|
+
- Updated dependencies [61debef]
|
|
21
|
+
- Updated dependencies [9beaeff]
|
|
22
|
+
- Updated dependencies [29de0e1]
|
|
23
|
+
- Updated dependencies [f643c65]
|
|
24
|
+
- Updated dependencies [00c74e7]
|
|
25
|
+
- Updated dependencies [fef7375]
|
|
26
|
+
- Updated dependencies [e3d8fea]
|
|
27
|
+
- Updated dependencies [45e4d39]
|
|
28
|
+
- Updated dependencies [9eee594]
|
|
29
|
+
- Updated dependencies [7149d8d]
|
|
30
|
+
- Updated dependencies [822c2e8]
|
|
31
|
+
- Updated dependencies [979912c]
|
|
32
|
+
- Updated dependencies [7dcf4c0]
|
|
33
|
+
- Updated dependencies [4106a58]
|
|
34
|
+
- Updated dependencies [ad78bfc]
|
|
35
|
+
- Updated dependencies [0302f50]
|
|
36
|
+
- Updated dependencies [6ac697e]
|
|
37
|
+
- Updated dependencies [74db265]
|
|
38
|
+
- Updated dependencies [0ce418a]
|
|
39
|
+
- Updated dependencies [af90672]
|
|
40
|
+
- Updated dependencies [8387952]
|
|
41
|
+
- Updated dependencies [7f3b8da]
|
|
42
|
+
- Updated dependencies [905352b]
|
|
43
|
+
- Updated dependencies [599d04c]
|
|
44
|
+
- Updated dependencies [56041d0]
|
|
45
|
+
- Updated dependencies [3412597]
|
|
46
|
+
- Updated dependencies [5eca5d2]
|
|
47
|
+
- Updated dependencies [f2cda47]
|
|
48
|
+
- Updated dependencies [5de1555]
|
|
49
|
+
- Updated dependencies [cfd377a]
|
|
50
|
+
- Updated dependencies [1ed5a3e]
|
|
51
|
+
- @mastra/core@0.15.3
|
|
52
|
+
|
|
53
|
+
## 0.0.5-alpha.1
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- [#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`.
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [[`85ef90b`](https://github.com/mastra-ai/mastra/commit/85ef90bb2cd4ae4df855c7ac175f7d392c55c1bf), [`de3cbc6`](https://github.com/mastra-ai/mastra/commit/de3cbc61079211431bd30487982ea3653517278e)]:
|
|
60
|
+
- @mastra/core@0.15.3-alpha.5
|
|
61
|
+
|
|
3
62
|
## 0.0.5-alpha.0
|
|
4
63
|
|
|
5
64
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/langfuse",
|
|
3
|
-
"version": "0.0.5
|
|
3
|
+
"version": "0.0.5",
|
|
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",
|
|
32
|
-
"@internal/lint": "0.0.
|
|
33
|
-
"@internal/types-builder": "0.0.
|
|
34
|
-
"@mastra/core": "0.15.3
|
|
36
|
+
"@internal/lint": "0.0.35",
|
|
37
|
+
"@internal/types-builder": "0.0.10",
|
|
38
|
+
"@mastra/core": "0.15.3"
|
|
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",
|
package/.turbo/turbo-build.log
DELETED
package/eslint.config.js
DELETED
package/src/ai-tracing.test.ts
DELETED
|
@@ -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';
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
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
|
-
});
|