@mastra/langfuse 0.0.0-trace-timeline-update-20251121092347 → 0.0.0-type-testing-20260120105120

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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Langfuse Tracing Options Helpers
3
+ *
4
+ * These helpers integrate with the `buildTracingOptions` pattern from
5
+ * `@mastra/observability` to add Langfuse-specific tracing features.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { buildTracingOptions } from '@mastra/observability';
10
+ * import { withLangfusePrompt } from '@mastra/langfuse';
11
+ *
12
+ * const prompt = await langfuse.getPrompt('my-prompt');
13
+ *
14
+ * const agent = new Agent({
15
+ * defaultGenerateOptions: {
16
+ * tracingOptions: buildTracingOptions(withLangfusePrompt(prompt)),
17
+ * },
18
+ * });
19
+ * ```
20
+ */
21
+ import type { TracingOptionsUpdater } from '@mastra/observability';
22
+ /**
23
+ * Langfuse prompt input - accepts either a Langfuse SDK prompt object
24
+ * or manual fields.
25
+ */
26
+ export interface LangfusePromptInput {
27
+ /** Prompt name */
28
+ name?: string;
29
+ /** Prompt version */
30
+ version?: number;
31
+ /** Prompt UUID */
32
+ id?: string;
33
+ }
34
+ /**
35
+ * Adds Langfuse prompt metadata to the tracing options
36
+ * to enable Langfuse Prompt Tracing.
37
+ *
38
+ * The metadata is added under `metadata.langfuse.prompt` and includes:
39
+ * - `name` - Prompt name
40
+ * - `version` - Prompt version
41
+ * - `id` - Prompt UUID
42
+ *
43
+ * All fields are deeply merged with any existing metadata.
44
+ *
45
+ * @param prompt - A Langfuse prompt object (from `langfuse.getPrompt()`) or manual fields
46
+ * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { buildTracingOptions } from '@mastra/observability';
51
+ * import { withLangfusePrompt } from '@mastra/langfuse';
52
+ * import { Langfuse } from 'langfuse';
53
+ *
54
+ * const langfuse = new Langfuse();
55
+ * const prompt = await langfuse.getPrompt('customer-support');
56
+ *
57
+ * // Use with buildTracingOptions
58
+ * const tracingOptions = buildTracingOptions(
59
+ * withLangfusePrompt(prompt),
60
+ * );
61
+ *
62
+ * // Or directly in agent config
63
+ * const agent = new Agent({
64
+ * name: 'support-agent',
65
+ * instructions: prompt.prompt,
66
+ * model: openai('gpt-4o'),
67
+ * defaultGenerateOptions: {
68
+ * tracingOptions: buildTracingOptions(withLangfusePrompt(prompt)),
69
+ * },
70
+ * });
71
+ *
72
+ * // Manual fields also work
73
+ * const tracingOptions = buildTracingOptions(
74
+ * withLangfusePrompt({ name: 'my-prompt', version: 1 }),
75
+ * );
76
+ * ```
77
+ */
78
+ export declare function withLangfusePrompt(prompt: LangfusePromptInput): TracingOptionsUpdater;
79
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,kBAAkB;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,GAAG,qBAAqB,CAerF"}
package/dist/index.cjs CHANGED
@@ -6,174 +6,127 @@ var observability = require('@mastra/observability');
6
6
  var langfuse = require('langfuse');
7
7
 
8
8
  // src/tracing.ts
9
- var LangfuseExporter = class extends observability.BaseExporter {
9
+
10
+ // src/metrics.ts
11
+ function formatUsageMetrics(usage) {
12
+ if (!usage) return {};
13
+ const metrics = {};
14
+ if (usage.inputTokens !== void 0) {
15
+ metrics.input = usage.inputTokens;
16
+ if (usage.inputDetails?.cacheWrite !== void 0) {
17
+ metrics.cache_write_input_tokens = usage.inputDetails.cacheWrite;
18
+ metrics.input -= metrics.cache_write_input_tokens;
19
+ }
20
+ }
21
+ if (usage.inputDetails?.cacheRead !== void 0) {
22
+ metrics.cache_read_input_tokens = usage.inputDetails.cacheRead;
23
+ }
24
+ if (usage.outputTokens !== void 0) {
25
+ metrics.output = usage.outputTokens;
26
+ }
27
+ if (usage.outputDetails?.reasoning !== void 0) {
28
+ metrics.reasoning = usage.outputDetails.reasoning;
29
+ }
30
+ if (metrics.input != null && metrics.output != null) {
31
+ metrics.total = metrics.input + metrics.output;
32
+ if (metrics.cache_write_input_tokens != null) {
33
+ metrics.total += metrics.cache_write_input_tokens;
34
+ }
35
+ }
36
+ return metrics;
37
+ }
38
+
39
+ // src/tracing.ts
40
+ var LangfuseExporter = class extends observability.TrackingExporter {
10
41
  name = "langfuse";
11
- client;
12
- realtime;
13
- traceMap = /* @__PURE__ */ new Map();
14
- constructor(config) {
15
- super(config);
16
- this.realtime = config.realtime ?? false;
17
- if (!config.publicKey || !config.secretKey) {
42
+ #client;
43
+ #realtime;
44
+ constructor(config = {}) {
45
+ const publicKey = config.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;
46
+ const secretKey = config.secretKey ?? process.env.LANGFUSE_SECRET_KEY;
47
+ const baseUrl = config.baseUrl ?? process.env.LANGFUSE_BASE_URL;
48
+ super({
49
+ ...config,
50
+ publicKey,
51
+ secretKey,
52
+ baseUrl
53
+ });
54
+ this.#realtime = config.realtime ?? false;
55
+ if (!publicKey || !secretKey) {
56
+ const publicKeySource = config.publicKey ? "from config" : process.env.LANGFUSE_PUBLIC_KEY ? "from env" : "missing";
57
+ const secretKeySource = config.secretKey ? "from config" : process.env.LANGFUSE_SECRET_KEY ? "from env" : "missing";
18
58
  this.setDisabled(
19
- `Missing required credentials (publicKey: ${!!config.publicKey}, secretKey: ${!!config.secretKey})`
59
+ `Missing required credentials (publicKey: ${publicKeySource}, secretKey: ${secretKeySource}). Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment variables or pass them in config.`
20
60
  );
21
- this.client = null;
22
61
  return;
23
62
  }
24
- this.client = new langfuse.Langfuse({
25
- publicKey: config.publicKey,
26
- secretKey: config.secretKey,
27
- baseUrl: config.baseUrl,
63
+ this.#client = new langfuse.Langfuse({
64
+ publicKey,
65
+ secretKey,
66
+ baseUrl,
28
67
  ...config.options
29
68
  });
30
69
  }
31
- async _exportTracingEvent(event) {
32
- if (event.exportedSpan.isEvent) {
33
- await this.handleEventSpan(event.exportedSpan);
34
- return;
35
- }
36
- switch (event.type) {
37
- case "span_started":
38
- await this.handleSpanStarted(event.exportedSpan);
39
- break;
40
- case "span_updated":
41
- await this.handleSpanUpdateOrEnd(event.exportedSpan, false);
42
- break;
43
- case "span_ended":
44
- await this.handleSpanUpdateOrEnd(event.exportedSpan, true);
45
- break;
46
- }
47
- if (this.realtime) {
48
- await this.client.flushAsync();
70
+ async _postExportTracingEvent() {
71
+ if (this.#realtime) {
72
+ await this.#client?.flushAsync();
49
73
  }
50
74
  }
51
- async handleSpanStarted(span) {
52
- if (span.isRootSpan) {
53
- this.initTrace(span);
54
- }
55
- const method = "handleSpanStarted";
56
- const traceData = this.getTraceData({ span, method });
57
- if (!traceData) {
75
+ async _buildRoot(args) {
76
+ const { span } = args;
77
+ return this.#client?.trace(this.buildTracePayload(span));
78
+ }
79
+ async _buildEvent(args) {
80
+ const { span, traceData } = args;
81
+ const langfuseParent = traceData.getParentOrRoot({ span });
82
+ if (!langfuseParent) {
58
83
  return;
59
84
  }
60
- const langfuseParent = this.getLangfuseParent({ traceData, span, method });
85
+ const payload = this.buildSpanPayload(span, true, traceData);
86
+ return langfuseParent.event(payload);
87
+ }
88
+ async _buildSpan(args) {
89
+ const { span, traceData } = args;
90
+ const langfuseParent = traceData.getParentOrRoot({ span });
61
91
  if (!langfuseParent) {
62
92
  return;
63
93
  }
64
- const payload = this.buildSpanPayload(span, true);
94
+ const payload = this.buildSpanPayload(span, true, traceData);
65
95
  const langfuseSpan = span.type === observability$1.SpanType.MODEL_GENERATION ? langfuseParent.generation(payload) : langfuseParent.span(payload);
66
- traceData.spans.set(span.id, langfuseSpan);
67
- traceData.activeSpans.add(span.id);
96
+ this.logger.debug(`${this.name}: built span`, {
97
+ traceId: span.traceId,
98
+ spanId: payload.id,
99
+ method: "_buildSpan"
100
+ });
101
+ return langfuseSpan;
68
102
  }
69
- async handleSpanUpdateOrEnd(span, isEnd) {
70
- const method = isEnd ? "handleSpanEnd" : "handleSpanUpdate";
71
- const traceData = this.getTraceData({ span, method });
72
- if (!traceData) {
73
- return;
74
- }
75
- const langfuseSpan = traceData.spans.get(span.id);
76
- if (!langfuseSpan) {
77
- if (isEnd && span.isEvent) {
78
- traceData.activeSpans.delete(span.id);
79
- if (traceData.activeSpans.size === 0) {
80
- this.traceMap.delete(span.traceId);
81
- }
82
- return;
83
- }
84
- this.logger.warn("Langfuse exporter: No Langfuse span found for span update/end", {
103
+ async _updateSpan(args) {
104
+ const { span, traceData } = args;
105
+ const langfuseSpan = traceData.getSpan({ spanId: span.id });
106
+ if (langfuseSpan) {
107
+ this.logger.debug(`${this.name}: found span for update`, {
85
108
  traceId: span.traceId,
86
- spanId: span.id,
87
- spanName: span.name,
88
- spanType: span.type,
89
- isRootSpan: span.isRootSpan,
90
- parentSpanId: span.parentSpanId,
91
- method
109
+ spanId: langfuseSpan.id,
110
+ method: "_updateSpan"
92
111
  });
93
- return;
94
- }
95
- langfuseSpan.update(this.buildSpanPayload(span, false));
96
- if (isEnd) {
97
- traceData.activeSpans.delete(span.id);
98
- if (span.isRootSpan) {
99
- traceData.trace.update({ output: span.output });
100
- }
101
- if (traceData.activeSpans.size === 0) {
102
- this.traceMap.delete(span.traceId);
103
- }
112
+ const updatePayload = this.buildSpanPayload(span, false, traceData);
113
+ langfuseSpan.update(updatePayload);
104
114
  }
105
115
  }
106
- async handleEventSpan(span) {
116
+ async _finishSpan(args) {
117
+ const { span, traceData } = args;
118
+ const langfuseSpan = traceData.getSpan({ spanId: span.id });
119
+ langfuseSpan?.update(this.buildSpanPayload(span, false, traceData));
107
120
  if (span.isRootSpan) {
108
- this.logger.debug("Langfuse exporter: Creating trace", {
109
- traceId: span.traceId,
110
- spanId: span.id,
111
- spanName: span.name,
112
- method: "handleEventSpan"
113
- });
114
- this.initTrace(span);
115
- }
116
- const method = "handleEventSpan";
117
- const traceData = this.getTraceData({ span, method });
118
- if (!traceData) {
119
- return;
120
- }
121
- const langfuseParent = this.getLangfuseParent({ traceData, span, method });
122
- if (!langfuseParent) {
123
- return;
124
- }
125
- const payload = this.buildSpanPayload(span, true);
126
- const langfuseEvent = langfuseParent.event(payload);
127
- traceData.events.set(span.id, langfuseEvent);
128
- if (!span.endTime) {
129
- traceData.activeSpans.add(span.id);
121
+ const langfuseRoot = traceData.getRoot();
122
+ langfuseRoot?.update({ output: span.output });
130
123
  }
131
124
  }
132
- initTrace(span) {
133
- const trace = this.client.trace(this.buildTracePayload(span));
134
- this.traceMap.set(span.traceId, {
135
- trace,
136
- spans: /* @__PURE__ */ new Map(),
137
- events: /* @__PURE__ */ new Map(),
138
- activeSpans: /* @__PURE__ */ new Set(),
139
- rootSpanId: span.id
140
- });
141
- }
142
- getTraceData(options) {
143
- const { span, method } = options;
144
- if (this.traceMap.has(span.traceId)) {
145
- return this.traceMap.get(span.traceId);
146
- }
147
- this.logger.warn("Langfuse exporter: No trace data found for span", {
148
- traceId: span.traceId,
149
- spanId: span.id,
150
- spanName: span.name,
151
- spanType: span.type,
152
- isRootSpan: span.isRootSpan,
153
- parentSpanId: span.parentSpanId,
154
- method
155
- });
156
- }
157
- getLangfuseParent(options) {
158
- const { traceData, span, method } = options;
159
- const parentId = span.parentSpanId;
160
- if (!parentId) {
161
- return traceData.trace;
162
- }
163
- if (traceData.spans.has(parentId)) {
164
- return traceData.spans.get(parentId);
165
- }
166
- if (traceData.events.has(parentId)) {
167
- return traceData.events.get(parentId);
168
- }
169
- this.logger.warn("Langfuse exporter: No parent data found for span", {
170
- traceId: span.traceId,
171
- spanId: span.id,
172
- spanName: span.name,
173
- spanType: span.type,
174
- isRootSpan: span.isRootSpan,
175
- parentSpanId: span.parentSpanId,
176
- method
125
+ async _abortSpan(args) {
126
+ const { span, reason } = args;
127
+ span.end({
128
+ level: "ERROR",
129
+ statusMessage: reason.message
177
130
  });
178
131
  }
179
132
  buildTracePayload(span) {
@@ -185,6 +138,7 @@ var LangfuseExporter = class extends observability.BaseExporter {
185
138
  if (userId) payload.userId = userId;
186
139
  if (sessionId) payload.sessionId = sessionId;
187
140
  if (span.input) payload.input = span.input;
141
+ if (span.tags?.length) payload.tags = span.tags;
188
142
  payload.metadata = {
189
143
  spanType: span.type,
190
144
  ...span.attributes,
@@ -193,59 +147,46 @@ var LangfuseExporter = class extends observability.BaseExporter {
193
147
  return payload;
194
148
  }
195
149
  /**
196
- * Normalize usage data to handle both AI SDK v4 and v5 formats.
197
- *
198
- * AI SDK v4 uses: promptTokens, completionTokens
199
- * AI SDK v5 uses: inputTokens, outputTokens
200
- *
201
- * This function normalizes to a unified format that Langfuse can consume,
202
- * prioritizing v5 format while maintaining backward compatibility.
203
- *
204
- * @param usage - Token usage data from AI SDK (v4 or v5 format)
205
- * @returns Normalized usage object, or undefined if no usage data available
150
+ * Look up the Langfuse prompt from the closest span that has one.
151
+ * This enables prompt inheritance for MODEL_GENERATION spans when the prompt
152
+ * is set on a parent span (e.g., AGENT_RUN) rather than directly on the generation.
153
+ * This enables prompt linking when:
154
+ * - A workflow calls multiple agents, each with different prompts
155
+ * - Nested agents have different prompts
156
+ * - The prompt is set on AGENT_RUN but MODEL_GENERATION inherits it
206
157
  */
207
- normalizeUsage(usage) {
208
- if (!usage) return void 0;
209
- const normalized = {};
210
- const inputTokens = usage.inputTokens ?? usage.promptTokens;
211
- if (inputTokens !== void 0) {
212
- normalized.input = inputTokens;
213
- }
214
- const outputTokens = usage.outputTokens ?? usage.completionTokens;
215
- if (outputTokens !== void 0) {
216
- normalized.output = outputTokens;
217
- }
218
- if (usage.totalTokens !== void 0) {
219
- normalized.total = usage.totalTokens;
220
- } else if (normalized.input !== void 0 && normalized.output !== void 0) {
221
- normalized.total = normalized.input + normalized.output;
222
- }
223
- if (usage.reasoningTokens !== void 0) {
224
- normalized.reasoning = usage.reasoningTokens;
225
- }
226
- if (usage.cachedInputTokens !== void 0) {
227
- normalized.cachedInput = usage.cachedInputTokens;
228
- }
229
- if (usage.promptCacheHitTokens !== void 0) {
230
- normalized.promptCacheHit = usage.promptCacheHitTokens;
231
- }
232
- if (usage.promptCacheMissTokens !== void 0) {
233
- normalized.promptCacheMiss = usage.promptCacheMissTokens;
158
+ findLangfusePrompt(traceData, span) {
159
+ let currentSpanId = span.id;
160
+ while (currentSpanId) {
161
+ const providerMetadata = traceData.getMetadata({ spanId: currentSpanId });
162
+ if (providerMetadata?.prompt) {
163
+ this.logger.debug(`${this.name}: found prompt in provider metadata`, {
164
+ traceId: span.traceId,
165
+ spanId: span.id,
166
+ prompt: providerMetadata?.prompt
167
+ });
168
+ return providerMetadata.prompt;
169
+ }
170
+ currentSpanId = traceData.getParentId({ spanId: currentSpanId });
234
171
  }
235
- return Object.keys(normalized).length > 0 ? normalized : void 0;
172
+ return void 0;
236
173
  }
237
- buildSpanPayload(span, isCreate) {
174
+ buildSpanPayload(span, isCreate, traceData) {
238
175
  const payload = {};
239
176
  if (isCreate) {
240
177
  payload.id = span.id;
241
178
  payload.name = span.name;
242
179
  payload.startTime = span.startTime;
243
- if (span.input !== void 0) payload.input = span.input;
244
180
  }
181
+ if (span.input !== void 0) payload.input = span.input;
245
182
  if (span.output !== void 0) payload.output = span.output;
246
183
  if (span.endTime !== void 0) payload.endTime = span.endTime;
247
184
  const attributes = span.attributes ?? {};
185
+ const metadata = {
186
+ ...span.metadata
187
+ };
248
188
  const attributesToOmit = [];
189
+ const metadataToOmit = [];
249
190
  if (span.type === observability$1.SpanType.MODEL_GENERATION) {
250
191
  const modelAttr = attributes;
251
192
  if (modelAttr.model !== void 0) {
@@ -253,21 +194,32 @@ var LangfuseExporter = class extends observability.BaseExporter {
253
194
  attributesToOmit.push("model");
254
195
  }
255
196
  if (modelAttr.usage !== void 0) {
256
- const normalizedUsage = this.normalizeUsage(modelAttr.usage);
257
- if (normalizedUsage) {
258
- payload.usage = normalizedUsage;
259
- }
197
+ payload.usageDetails = formatUsageMetrics(modelAttr.usage);
260
198
  attributesToOmit.push("usage");
261
199
  }
262
200
  if (modelAttr.parameters !== void 0) {
263
201
  payload.modelParameters = modelAttr.parameters;
264
202
  attributesToOmit.push("parameters");
265
203
  }
204
+ const promptData = this.findLangfusePrompt(traceData, span);
205
+ const hasNameAndVersion = promptData?.name !== void 0 && promptData?.version !== void 0;
206
+ const hasId = promptData?.id !== void 0;
207
+ if (hasNameAndVersion || hasId) {
208
+ payload.prompt = {};
209
+ if (promptData?.name !== void 0) payload.prompt.name = promptData.name;
210
+ if (promptData?.version !== void 0) payload.prompt.version = promptData.version;
211
+ if (promptData?.id !== void 0) payload.prompt.id = promptData.id;
212
+ metadataToOmit.push("langfuse");
213
+ }
214
+ if (modelAttr.completionStartTime !== void 0) {
215
+ payload.completionStartTime = modelAttr.completionStartTime;
216
+ attributesToOmit.push("completionStartTime");
217
+ }
266
218
  }
267
219
  payload.metadata = {
268
220
  spanType: span.type,
269
221
  ...utils.omitKeys(attributes, attributesToOmit),
270
- ...span.metadata
222
+ ...utils.omitKeys(metadata, metadataToOmit)
271
223
  };
272
224
  if (span.errorInfo) {
273
225
  payload.level = "ERROR";
@@ -283,9 +235,9 @@ var LangfuseExporter = class extends observability.BaseExporter {
283
235
  scorerName,
284
236
  metadata
285
237
  }) {
286
- if (!this.client) return;
238
+ if (!this.#client) return;
287
239
  try {
288
- await this.client.score({
240
+ await this.#client.score({
289
241
  id: `${traceId}-${scorerName}`,
290
242
  traceId,
291
243
  observationId: spanId,
@@ -304,15 +256,40 @@ var LangfuseExporter = class extends observability.BaseExporter {
304
256
  });
305
257
  }
306
258
  }
307
- async shutdown() {
308
- if (this.client) {
309
- await this.client.shutdownAsync();
259
+ /**
260
+ * Force flush any buffered data to Langfuse without shutting down.
261
+ */
262
+ async _flush() {
263
+ if (this.#client) {
264
+ await this.#client.flushAsync();
265
+ }
266
+ }
267
+ async _postShutdown() {
268
+ if (this.#client) {
269
+ await this.#client.shutdownAsync();
310
270
  }
311
- this.traceMap.clear();
312
- await super.shutdown();
313
271
  }
314
272
  };
315
273
 
274
+ // src/helpers.ts
275
+ function withLangfusePrompt(prompt) {
276
+ return (opts) => ({
277
+ ...opts,
278
+ metadata: {
279
+ ...opts.metadata,
280
+ langfuse: {
281
+ ...opts.metadata?.langfuse,
282
+ prompt: {
283
+ ...prompt.name !== void 0 && { name: prompt.name },
284
+ ...prompt.version !== void 0 && { version: prompt.version },
285
+ ...prompt.id !== void 0 && { id: prompt.id }
286
+ }
287
+ }
288
+ }
289
+ });
290
+ }
291
+
316
292
  exports.LangfuseExporter = LangfuseExporter;
293
+ exports.withLangfusePrompt = withLangfusePrompt;
317
294
  //# sourceMappingURL=index.cjs.map
318
295
  //# sourceMappingURL=index.cjs.map