@mastra/langfuse 1.5.13 → 1.5.14-alpha.0
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/dist/index.cjs +7 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -198,6 +198,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
198
198
|
attributes["gen_ai.usage.reasoning.output_tokens"] = attributes["gen_ai.usage.reasoning_tokens"];
|
|
199
199
|
delete attributes["gen_ai.usage.reasoning_tokens"];
|
|
200
200
|
}
|
|
201
|
+
const nested = span.nestedUnderParent === true;
|
|
201
202
|
if (environment) attributes["langfuse.environment"] = environment;
|
|
202
203
|
if (release) attributes["langfuse.release"] = release;
|
|
203
204
|
const langfuseMetadata = attributes["mastra.metadata.langfuse"];
|
|
@@ -210,13 +211,13 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
210
211
|
if (parsed.prompt.version !== void 0) attributes["langfuse.observation.prompt.version"] = parsed.prompt.version;
|
|
211
212
|
}
|
|
212
213
|
for (const [key, value] of Object.entries(parsed)) {
|
|
213
|
-
if (key === "prompt" || value === null || value === void 0) continue;
|
|
214
|
+
if (nested || key === "prompt" || value === null || value === void 0) continue;
|
|
214
215
|
const traceKey = `langfuse.trace.metadata.${key}`;
|
|
215
216
|
if (attributes[traceKey] === void 0) attributes[traceKey] = typeof value === "string" ? value : JSON.stringify(value);
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
} catch {}
|
|
219
|
-
delete attributes["mastra.metadata.langfuse"];
|
|
220
|
+
if (!nested) delete attributes["mastra.metadata.langfuse"];
|
|
220
221
|
}
|
|
221
222
|
if (attributes["mastra.completion_start_time"]) {
|
|
222
223
|
attributes["langfuse.observation.completion_start_time"] = attributes["mastra.completion_start_time"];
|
|
@@ -224,14 +225,14 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
224
225
|
}
|
|
225
226
|
const costContext = span.type === _mastra_core_observability.SpanType.MODEL_GENERATION ? span.attributes?.costContext : void 0;
|
|
226
227
|
if (costContext?.costMetadata?.source === "provider_reported" && costContext.costUnit === "USD" && typeof costContext.estimatedCost === "number" && Number.isFinite(costContext.estimatedCost) && costContext.estimatedCost >= 0) attributes["langfuse.observation.cost_details"] = JSON.stringify({ total: costContext.estimatedCost });
|
|
227
|
-
if (attributes["mastra.metadata.userId"]) {
|
|
228
|
+
if (!nested && attributes["mastra.metadata.userId"]) {
|
|
228
229
|
attributes["user.id"] = attributes["mastra.metadata.userId"];
|
|
229
230
|
delete attributes["mastra.metadata.userId"];
|
|
230
231
|
}
|
|
231
232
|
const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];
|
|
232
233
|
const sessionId = attributes["mastra.metadata.sessionId"] ?? (typeof omCallerThreadId === "string" && omCallerThreadId ? omCallerThreadId : attributes["mastra.metadata.threadId"]);
|
|
233
234
|
delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];
|
|
234
|
-
if (sessionId) {
|
|
235
|
+
if (!nested && sessionId) {
|
|
235
236
|
attributes["session.id"] = sessionId;
|
|
236
237
|
delete attributes["mastra.metadata.sessionId"];
|
|
237
238
|
delete attributes["mastra.metadata.threadId"];
|
|
@@ -240,7 +241,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
240
241
|
attributes["langfuse.trace.tags"] = attributes["mastra.tags"];
|
|
241
242
|
delete attributes["mastra.tags"];
|
|
242
243
|
}
|
|
243
|
-
if (attributes["mastra.metadata.traceName"]) {
|
|
244
|
+
if (!nested && attributes["mastra.metadata.traceName"]) {
|
|
244
245
|
attributes["langfuse.trace.name"] = attributes["mastra.metadata.traceName"];
|
|
245
246
|
delete attributes["mastra.metadata.traceName"];
|
|
246
247
|
}
|
|
@@ -248,7 +249,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
248
249
|
attributes["langfuse.version"] = attributes["mastra.metadata.version"];
|
|
249
250
|
delete attributes["mastra.metadata.version"];
|
|
250
251
|
}
|
|
251
|
-
if (span.isRootSpan) {
|
|
252
|
+
if (span.isRootSpan && !nested) {
|
|
252
253
|
const traceInput = serializeTraceIo(span.input);
|
|
253
254
|
if (traceInput !== void 0) attributes["langfuse.trace.input"] = traceInput;
|
|
254
255
|
const traceOutput = serializeTraceIo(span.output);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["BaseExporter","#realtime","#resourceAttributes","#processor","LangfuseSpanProcessor","#client","LangfuseClient","#environment","#release","#spanConverter","SpanConverter","TracingEventType","SpanType"],"sources":["../src/tracing.ts","../src/helpers.ts"],"sourcesContent":["/**\n * Langfuse Exporter for Mastra Observability\n *\n * Sends observability data to Langfuse using the official @langfuse/otel span processor\n * and @langfuse/client for non-tracing features (scoring, prompt management, evaluations).\n *\n * @see https://langfuse.com/docs/observability/sdk/typescript/overview\n */\n\nimport { LangfuseClient } from '@langfuse/client';\nimport { LangfuseSpanProcessor } from '@langfuse/otel';\nimport type {\n TracingEvent,\n AnyExportedSpan,\n InitExporterOptions,\n ModelGenerationAttributes,\n ScoreEvent,\n} from '@mastra/core/observability';\nimport { SpanType, TracingEventType } from '@mastra/core/observability';\nimport { BaseExporter } from '@mastra/observability';\nimport type { BaseExporterConfig } from '@mastra/observability';\nimport { SpanConverter } from '@mastra/otel-exporter';\nimport type { OtelExporterConfig } from '@mastra/otel-exporter';\n\nconst LOG_PREFIX = '[LangfuseExporter]';\nconst MASTRA_METADATA_PREFIX = 'mastra.metadata.';\n/** Metadata keys mapped to dedicated Langfuse fields; never forwarded as trace metadata. */\nconst OM_CALLER_THREAD_ID = '__mastraObservationalMemoryCallerThreadId';\nconst DEDICATED_METADATA_KEYS = new Set([\n 'userId',\n 'sessionId',\n 'threadId',\n 'traceName',\n 'version',\n 'langfuse',\n OM_CALLER_THREAD_ID,\n]);\n\nexport const LANGFUSE_DEFAULT_BASE_URL = 'https://cloud.langfuse.com';\n\nexport interface LangfuseExporterConfig extends BaseExporterConfig {\n /** Langfuse public key */\n publicKey?: string;\n /** Langfuse secret key */\n secretKey?: string;\n /** Langfuse host URL (defaults to https://cloud.langfuse.com) */\n baseUrl?: string;\n /** Additional headers sent with requests to Langfuse */\n additionalHeaders?: Record<string, string>;\n /** Enable realtime mode - flushes after each event for immediate visibility */\n realtime?: boolean;\n /** Maximum number of spans per OTEL export batch */\n flushAt?: number;\n /** Maximum time in seconds before pending spans are exported */\n flushInterval?: number;\n /** Langfuse environment tag for traces */\n environment?: string;\n /** Langfuse release tag for traces */\n release?: string;\n /** Override OpenTelemetry resource attributes, including service.version. */\n resourceAttributes?: OtelExporterConfig['resourceAttributes'];\n}\n\nexport class LangfuseExporter extends BaseExporter {\n name = 'langfuse';\n #processor: LangfuseSpanProcessor | undefined;\n #client: LangfuseClient | undefined;\n #spanConverter: SpanConverter | undefined;\n #realtime: boolean;\n #environment: string | undefined;\n #release: string | undefined;\n #resourceAttributes: OtelExporterConfig['resourceAttributes'];\n\n constructor(config: LangfuseExporterConfig = {}) {\n super(config);\n\n const publicKey = config.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;\n const secretKey = config.secretKey ?? process.env.LANGFUSE_SECRET_KEY;\n const baseUrl = stripTrailingSlashes(config.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? LANGFUSE_DEFAULT_BASE_URL);\n this.#realtime = config.realtime ?? false;\n this.#resourceAttributes = config.resourceAttributes;\n\n if (!publicKey || !secretKey) {\n const publicKeySource = config.publicKey\n ? 'from config'\n : process.env.LANGFUSE_PUBLIC_KEY\n ? 'from env'\n : 'missing';\n const secretKeySource = config.secretKey\n ? 'from config'\n : process.env.LANGFUSE_SECRET_KEY\n ? 'from env'\n : 'missing';\n this.setDisabled(\n `${LOG_PREFIX} Missing required credentials (publicKey: ${publicKeySource}, secretKey: ${secretKeySource}). ` +\n `Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment variables or pass them in config.`,\n );\n return;\n }\n\n this.#processor = new LangfuseSpanProcessor({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n environment: config.environment,\n release: config.release,\n exportMode: this.#realtime ? 'immediate' : 'batched',\n flushAt: config.flushAt,\n flushInterval: config.flushInterval,\n // Export all spans — the default filter only passes spans with gen_ai.* attributes\n // or known LLM instrumentation scopes, but Mastra spans use mastra.* attributes.\n shouldExportSpan: () => true,\n });\n\n this.#client = new LangfuseClient({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n });\n\n this.#environment = config.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT;\n this.#release = config.release ?? process.env.LANGFUSE_RELEASE;\n }\n\n init(options: InitExporterOptions) {\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: options.config?.serviceName,\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n protected async _exportTracingEvent(event: TracingEvent): Promise<void> {\n if (event.type !== TracingEventType.SPAN_ENDED) return;\n if (!this.#processor) return;\n\n await this.exportSpan(event.exportedSpan);\n }\n\n private async exportSpan(span: AnyExportedSpan): Promise<void> {\n if (!this.#spanConverter) {\n // Fallback if init() was not called (e.g., standalone usage without Mastra)\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: 'mastra-service',\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n try {\n const otelSpan = await this.#spanConverter.convertSpan(span);\n\n // Map mastra.* attributes to langfuse.* namespace so that Langfuse's OTLP\n // endpoint reads them correctly. SpanConverter produces mastra.* attributes,\n // but Langfuse only reads langfuse.* attributes for prompt linking, TTFT, etc.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n mapMastraToLangfuseAttributes(otelSpan.attributes, span, this.#environment, this.#release);\n\n this.#processor!.onEnd(otelSpan);\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Failed to export span ${span.id}:`, error);\n }\n }\n\n /**\n * The LangfuseClient instance for advanced Langfuse features.\n * Use this for prompt management, evaluations, datasets, and direct API access.\n */\n get client(): LangfuseClient | undefined {\n return this.#client;\n }\n\n /**\n * Submit a score to Langfuse. Used by both the new `onScoreEvent` path and the\n * deprecated `addScoreToTrace` wrapper.\n */\n private submitScore(args: {\n id: string;\n traceId: string;\n spanId?: string;\n name: string;\n value: number;\n comment?: string;\n metadata?: Record<string, unknown>;\n }): void {\n if (!this.#client) return;\n\n const { id, traceId, spanId, name, value, comment, metadata } = args;\n try {\n this.#client.score.create({\n id,\n traceId,\n ...(spanId ? { observationId: spanId } : {}),\n name,\n value,\n ...(comment ? { comment } : {}),\n ...(metadata ? { metadata } : {}),\n ...(this.#environment ? { environment: this.#environment } : {}),\n dataType: 'NUMERIC' as const,\n });\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Error submitting score`, {\n error,\n traceId,\n spanId,\n name,\n });\n }\n }\n\n async onScoreEvent(event: ScoreEvent): Promise<void> {\n const { score } = event;\n if (!score.traceId) return;\n this.submitScore({\n id: score.scoreId,\n traceId: score.traceId,\n spanId: score.spanId,\n name: score.scorerName ?? score.scorerId,\n value: score.score,\n comment: score.reason,\n metadata: score.metadata,\n });\n }\n\n /**\n * @deprecated Use the observability score event pipeline (`mastra.observability.addScore`)\n * instead. This method is preserved for backwards compatibility and forwards to the same\n * underlying client call as `onScoreEvent`.\n */\n async addScoreToTrace({\n traceId,\n spanId,\n score,\n reason,\n scorerName,\n metadata,\n }: {\n traceId: string;\n spanId?: string;\n score: number;\n reason?: string;\n scorerName: string;\n metadata?: Record<string, any>;\n }): Promise<void> {\n this.submitScore({\n id: `${traceId}-${spanId || ''}-${scorerName}`,\n traceId,\n spanId,\n name: scorerName,\n value: score,\n comment: reason,\n metadata,\n });\n }\n\n async flush(): Promise<void> {\n await Promise.all([this.#processor?.forceFlush(), this.#client?.flush()]);\n }\n\n async shutdown(): Promise<void> {\n await Promise.all([this.#processor?.shutdown(), this.#client?.shutdown()]);\n }\n}\n\n/**\n * Maps Mastra-specific OTel attributes to the langfuse.* namespace that\n * Langfuse's OTLP endpoint reads for prompt linking, TTFT, and other features.\n *\n * SpanConverter produces attributes like mastra.metadata.*, mastra.completion_start_time, etc.\n * Langfuse's OTLP server only reads langfuse.observation.prompt.name, langfuse.observation.completion_start_time, etc.\n *\n * This function mutates the attributes object in place.\n * @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n */\n/**\n * Serialize a root span's input/output for the trace-level attributes.\n * Returns undefined (attribute omitted) for absent values and for values that\n * cannot be JSON-serialized, so a bad payload never fails the span export.\n */\nfunction serializeTraceIo(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value);\n } catch {\n return undefined;\n }\n}\n\nfunction mapMastraToLangfuseAttributes(\n attributes: Record<string, any>,\n span: AnyExportedSpan,\n environment?: string,\n release?: string,\n): void {\n if (attributes['gen_ai.usage.reasoning_tokens'] !== undefined) {\n attributes['gen_ai.usage.reasoning.output_tokens'] = attributes['gen_ai.usage.reasoning_tokens'];\n delete attributes['gen_ai.usage.reasoning_tokens'];\n }\n\n // Environment and release: set directly since onStart() is not called\n if (environment) {\n attributes['langfuse.environment'] = environment;\n }\n if (release) {\n attributes['langfuse.release'] = release;\n }\n\n // mastra.metadata.langfuse holds a user-supplied object with two kinds of keys:\n // - the reserved `prompt` key, used for prompt linking, and\n // - arbitrary custom keys the user wants as top-level, filterable trace metadata.\n // Langfuse only allows filtering/grouping traces by top-level metadata\n // (langfuse.trace.metadata.*), so forward every non-prompt key there. These\n // attributes may be set on any span in the trace and land on the trace record.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n const langfuseMetadata = attributes['mastra.metadata.langfuse'];\n if (langfuseMetadata) {\n try {\n const parsed = typeof langfuseMetadata === 'string' ? JSON.parse(langfuseMetadata) : langfuseMetadata;\n if (parsed && typeof parsed === 'object') {\n // Prompt linking: mastra.metadata.langfuse.prompt → langfuse.observation.prompt.name / version\n if (parsed.prompt) {\n if (parsed.prompt.name !== undefined) {\n attributes['langfuse.observation.prompt.name'] = parsed.prompt.name;\n }\n if (parsed.prompt.version !== undefined) {\n attributes['langfuse.observation.prompt.version'] = parsed.prompt.version;\n }\n }\n\n // Custom keys: mastra.metadata.langfuse.<key> → langfuse.trace.metadata.<key>\n // Reserved identity keys (agentId/agentName/workflowId/workflowName) are set\n // by the root-span block below, which runs after this loop and takes precedence.\n for (const [key, value] of Object.entries(parsed)) {\n if (key === 'prompt' || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${key}`;\n // Don't overwrite a trace.metadata key already mapped from another attribute.\n if (attributes[traceKey] === undefined) {\n // Langfuse maps langfuse.trace.metadata.* as string attributes, so\n // serialize non-strings with JSON. Langfuse Cloud restores numbers,\n // booleans, and objects to their original types on ingestion.\n attributes[traceKey] = typeof value === 'string' ? value : JSON.stringify(value);\n }\n }\n }\n } catch {\n // best effort — invalid JSON is silently ignored\n }\n delete attributes['mastra.metadata.langfuse'];\n }\n\n // TTFT: mastra.completion_start_time → langfuse.observation.completion_start_time\n if (attributes['mastra.completion_start_time']) {\n attributes['langfuse.observation.completion_start_time'] = attributes['mastra.completion_start_time'];\n delete attributes['mastra.completion_start_time'];\n }\n\n // Exact provider-reported cost takes precedence over Langfuse's model-price inference.\n // Estimated costs remain unexported so Langfuse can apply its own pricing model.\n const costContext =\n span.type === SpanType.MODEL_GENERATION\n ? (span.attributes as ModelGenerationAttributes | undefined)?.costContext\n : undefined;\n if (\n costContext?.costMetadata?.source === 'provider_reported' &&\n costContext.costUnit === 'USD' &&\n typeof costContext.estimatedCost === 'number' &&\n Number.isFinite(costContext.estimatedCost) &&\n costContext.estimatedCost >= 0\n ) {\n attributes['langfuse.observation.cost_details'] = JSON.stringify({\n total: costContext.estimatedCost,\n });\n }\n\n // User ID: mastra.metadata.userId → user.id\n if (attributes['mastra.metadata.userId']) {\n attributes['user.id'] = attributes['mastra.metadata.userId'];\n delete attributes['mastra.metadata.userId'];\n }\n\n // OM keeps caller identity separate from its isolated execution thread. Read\n // the raw hint: conversion serializes malformed objects/arrays into strings.\n const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];\n const sessionId =\n attributes['mastra.metadata.sessionId'] ??\n (typeof omCallerThreadId === 'string' && omCallerThreadId\n ? omCallerThreadId\n : attributes['mastra.metadata.threadId']);\n delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];\n if (sessionId) {\n attributes['session.id'] = sessionId;\n delete attributes['mastra.metadata.sessionId'];\n delete attributes['mastra.metadata.threadId'];\n }\n\n // Tags: mastra.tags → langfuse.trace.tags\n if (attributes['mastra.tags']) {\n attributes['langfuse.trace.tags'] = attributes['mastra.tags'];\n delete attributes['mastra.tags'];\n }\n\n // Trace name: mastra.metadata.traceName → langfuse.trace.name\n if (attributes['mastra.metadata.traceName']) {\n attributes['langfuse.trace.name'] = attributes['mastra.metadata.traceName'];\n delete attributes['mastra.metadata.traceName'];\n }\n\n // Version: mastra.metadata.version → langfuse.version\n if (attributes['mastra.metadata.version']) {\n attributes['langfuse.version'] = attributes['mastra.metadata.version'];\n delete attributes['mastra.metadata.version'];\n }\n\n // Root-span trace identity: scope each Langfuse trace to the entity that\n // started it (agent or workflow). This makes the Langfuse trace name match\n // the agent/workflow id and exposes the same identity as trace metadata, so\n // users can scope Langfuse evaluators per agent via trace name or metadata\n // filters. User-provided traceName (set via mastra.metadata.traceName) takes\n // precedence and is preserved.\n if (span.isRootSpan) {\n // Trace input/output: mirror the root span's input/output onto the trace.\n // Without this, Langfuse traces have empty trace-level input/output (the\n // span data only reaches the root OBSERVATION), which breaks LLM-as-a-judge\n // evaluators mapped to Trace input/output and leaves the trace view without\n // the top-level request/response. Serialization failures (circular refs,\n // bigint) skip the attribute rather than failing the span export.\n const traceInput = serializeTraceIo(span.input);\n if (traceInput !== undefined) {\n attributes['langfuse.trace.input'] = traceInput;\n }\n const traceOutput = serializeTraceIo(span.output);\n if (traceOutput !== undefined) {\n attributes['langfuse.trace.output'] = traceOutput;\n }\n if (span.type === SpanType.AGENT_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.agentId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.agentName'] = span.entityName;\n }\n } else if (span.type === SpanType.WORKFLOW_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.workflowId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.workflowName'] = span.entityName;\n }\n }\n\n // Root-span metadata: forward the remaining mastra.metadata.* keys (runId,\n // resourceId, user-supplied keys) to langfuse.trace.metadata.* so they are\n // filterable trace metadata; Langfuse nests unmapped attributes under\n // metadata.attributes. Dedicated keys, explicit metadata.langfuse.* values,\n // and the identity keys above take precedence. Root only: Langfuse applies\n // langfuse.trace.* from any span, so a child span could overwrite the trace.\n for (const [key, value] of Object.entries(attributes)) {\n if (!key.startsWith(MASTRA_METADATA_PREFIX)) {\n continue;\n }\n const metadataKey = key.slice(MASTRA_METADATA_PREFIX.length);\n if (DEDICATED_METADATA_KEYS.has(metadataKey) || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${metadataKey}`;\n if (attributes[traceKey] !== undefined) {\n continue;\n }\n const serialized = serializeTraceIo(value);\n if (serialized !== undefined) {\n attributes[traceKey] = serialized;\n }\n }\n }\n\n // Observation metadata: map semantic attributes to langfuse.observation.metadata.*\n // so they become top-level filterable keys on each observation in Langfuse.\n // @see https://langfuse.com/integrations/native/opentelemetry#how-metadata-mapping-works\n if (attributes['gen_ai.agent.id']) {\n attributes['langfuse.observation.metadata.agentId'] = attributes['gen_ai.agent.id'];\n }\n if (attributes['gen_ai.agent.name']) {\n attributes['langfuse.observation.metadata.agentName'] = attributes['gen_ai.agent.name'];\n }\n if (attributes['mastra.span.type']) {\n attributes['langfuse.observation.metadata.spanType'] = attributes['mastra.span.type'];\n }\n if (attributes['gen_ai.operation.name']) {\n attributes['langfuse.observation.metadata.operationName'] = attributes['gen_ai.operation.name'];\n }\n\n // Input/Output: mastra.*.input/output → langfuse.observation.input/output\n // For gen_ai spans, Langfuse reads gen_ai.input.messages natively.\n // For non-gen_ai spans, we map the first mastra.*.input/output we find.\n if (!attributes['gen_ai.input.messages'] && !attributes['gen_ai.tool.call.arguments']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.input')) {\n attributes['langfuse.observation.input'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n if (!attributes['gen_ai.output.messages'] && !attributes['gen_ai.tool.call.result']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.output')) {\n attributes['langfuse.observation.output'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n}\n\n/**\n * Remove trailing \"/\" characters procedurally. Avoids polynomial\n * backtracking that a greedy regex like `/\\/+$/` can exhibit when the\n * input is attacker-controlled.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* \"/\" */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n","/**\n * Langfuse Tracing Options Helpers\n *\n * These helpers integrate with the `buildTracingOptions` pattern from\n * `@mastra/observability` to add Langfuse-specific tracing features.\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * const agent = new Agent({\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\n\nimport type { TracingOptionsUpdater } from '@mastra/observability';\n\n/**\n * Langfuse prompt input - accepts either a Langfuse SDK prompt object\n * or manual fields.\n */\nexport interface LangfusePromptInput {\n /** Prompt name */\n name?: string;\n /** Prompt version */\n version?: number;\n /** @deprecated Langfuse v5 only supports linking by name + version. This field is ignored. */\n id?: string;\n}\n\n/**\n * Adds Langfuse prompt metadata to the tracing options\n * to enable Langfuse Prompt Tracing.\n *\n * The metadata is added under `metadata.langfuse.prompt` and includes:\n * - `name` - Prompt name (required for Langfuse v5)\n * - `version` - Prompt version (required for Langfuse v5)\n *\n * All fields are deeply merged with any existing metadata.\n *\n * @param prompt - Prompt fields for linking (`name` and `version` required)\n * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * // Link a generation to a Langfuse prompt by name and version\n * const tracingOptions = buildTracingOptions(\n * withLangfusePrompt({ name: 'customer-support', version: 1 }),\n * );\n *\n * // Or directly in agent config\n * const agent = new Agent({\n * name: 'support-agent',\n * instructions: 'You are a helpful assistant',\n * model: openai('gpt-4o'),\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\nexport function withLangfusePrompt(prompt: LangfusePromptInput): TracingOptionsUpdater {\n return opts => ({\n ...opts,\n metadata: {\n ...opts.metadata,\n langfuse: {\n ...(opts.metadata?.langfuse as Record<string, unknown>),\n prompt: {\n ...(prompt.name !== undefined && { name: prompt.name }),\n ...(prompt.version !== undefined && { version: prompt.version }),\n ...(prompt.id !== undefined && { id: prompt.id }),\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["BaseExporter","#realtime","#resourceAttributes","#processor","LangfuseSpanProcessor","#client","LangfuseClient","#environment","#release","#spanConverter","SpanConverter","TracingEventType","SpanType"],"sources":["../src/tracing.ts","../src/helpers.ts"],"sourcesContent":["/**\n * Langfuse Exporter for Mastra Observability\n *\n * Sends observability data to Langfuse using the official @langfuse/otel span processor\n * and @langfuse/client for non-tracing features (scoring, prompt management, evaluations).\n *\n * @see https://langfuse.com/docs/observability/sdk/typescript/overview\n */\n\nimport { LangfuseClient } from '@langfuse/client';\nimport { LangfuseSpanProcessor } from '@langfuse/otel';\nimport type {\n TracingEvent,\n AnyExportedSpan,\n InitExporterOptions,\n ModelGenerationAttributes,\n ScoreEvent,\n} from '@mastra/core/observability';\nimport { SpanType, TracingEventType } from '@mastra/core/observability';\nimport { BaseExporter } from '@mastra/observability';\nimport type { BaseExporterConfig } from '@mastra/observability';\nimport { SpanConverter } from '@mastra/otel-exporter';\nimport type { OtelExporterConfig } from '@mastra/otel-exporter';\n\nconst LOG_PREFIX = '[LangfuseExporter]';\nconst MASTRA_METADATA_PREFIX = 'mastra.metadata.';\n/** Metadata keys mapped to dedicated Langfuse fields; never forwarded as trace metadata. */\nconst OM_CALLER_THREAD_ID = '__mastraObservationalMemoryCallerThreadId';\nconst DEDICATED_METADATA_KEYS = new Set([\n 'userId',\n 'sessionId',\n 'threadId',\n 'traceName',\n 'version',\n 'langfuse',\n OM_CALLER_THREAD_ID,\n]);\n\nexport const LANGFUSE_DEFAULT_BASE_URL = 'https://cloud.langfuse.com';\n\nexport interface LangfuseExporterConfig extends BaseExporterConfig {\n /** Langfuse public key */\n publicKey?: string;\n /** Langfuse secret key */\n secretKey?: string;\n /** Langfuse host URL (defaults to https://cloud.langfuse.com) */\n baseUrl?: string;\n /** Additional headers sent with requests to Langfuse */\n additionalHeaders?: Record<string, string>;\n /** Enable realtime mode - flushes after each event for immediate visibility */\n realtime?: boolean;\n /** Maximum number of spans per OTEL export batch */\n flushAt?: number;\n /** Maximum time in seconds before pending spans are exported */\n flushInterval?: number;\n /** Langfuse environment tag for traces */\n environment?: string;\n /** Langfuse release tag for traces */\n release?: string;\n /** Override OpenTelemetry resource attributes, including service.version. */\n resourceAttributes?: OtelExporterConfig['resourceAttributes'];\n}\n\nexport class LangfuseExporter extends BaseExporter {\n name = 'langfuse';\n #processor: LangfuseSpanProcessor | undefined;\n #client: LangfuseClient | undefined;\n #spanConverter: SpanConverter | undefined;\n #realtime: boolean;\n #environment: string | undefined;\n #release: string | undefined;\n #resourceAttributes: OtelExporterConfig['resourceAttributes'];\n\n constructor(config: LangfuseExporterConfig = {}) {\n super(config);\n\n const publicKey = config.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;\n const secretKey = config.secretKey ?? process.env.LANGFUSE_SECRET_KEY;\n const baseUrl = stripTrailingSlashes(config.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? LANGFUSE_DEFAULT_BASE_URL);\n this.#realtime = config.realtime ?? false;\n this.#resourceAttributes = config.resourceAttributes;\n\n if (!publicKey || !secretKey) {\n const publicKeySource = config.publicKey\n ? 'from config'\n : process.env.LANGFUSE_PUBLIC_KEY\n ? 'from env'\n : 'missing';\n const secretKeySource = config.secretKey\n ? 'from config'\n : process.env.LANGFUSE_SECRET_KEY\n ? 'from env'\n : 'missing';\n this.setDisabled(\n `${LOG_PREFIX} Missing required credentials (publicKey: ${publicKeySource}, secretKey: ${secretKeySource}). ` +\n `Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment variables or pass them in config.`,\n );\n return;\n }\n\n this.#processor = new LangfuseSpanProcessor({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n environment: config.environment,\n release: config.release,\n exportMode: this.#realtime ? 'immediate' : 'batched',\n flushAt: config.flushAt,\n flushInterval: config.flushInterval,\n // Export all spans — the default filter only passes spans with gen_ai.* attributes\n // or known LLM instrumentation scopes, but Mastra spans use mastra.* attributes.\n shouldExportSpan: () => true,\n });\n\n this.#client = new LangfuseClient({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n });\n\n this.#environment = config.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT;\n this.#release = config.release ?? process.env.LANGFUSE_RELEASE;\n }\n\n init(options: InitExporterOptions) {\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: options.config?.serviceName,\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n protected async _exportTracingEvent(event: TracingEvent): Promise<void> {\n if (event.type !== TracingEventType.SPAN_ENDED) return;\n if (!this.#processor) return;\n\n await this.exportSpan(event.exportedSpan);\n }\n\n private async exportSpan(span: AnyExportedSpan): Promise<void> {\n if (!this.#spanConverter) {\n // Fallback if init() was not called (e.g., standalone usage without Mastra)\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: 'mastra-service',\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n try {\n const otelSpan = await this.#spanConverter.convertSpan(span);\n\n // Map mastra.* attributes to langfuse.* namespace so that Langfuse's OTLP\n // endpoint reads them correctly. SpanConverter produces mastra.* attributes,\n // but Langfuse only reads langfuse.* attributes for prompt linking, TTFT, etc.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n mapMastraToLangfuseAttributes(otelSpan.attributes, span, this.#environment, this.#release);\n\n this.#processor!.onEnd(otelSpan);\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Failed to export span ${span.id}:`, error);\n }\n }\n\n /**\n * The LangfuseClient instance for advanced Langfuse features.\n * Use this for prompt management, evaluations, datasets, and direct API access.\n */\n get client(): LangfuseClient | undefined {\n return this.#client;\n }\n\n /**\n * Submit a score to Langfuse. Used by both the new `onScoreEvent` path and the\n * deprecated `addScoreToTrace` wrapper.\n */\n private submitScore(args: {\n id: string;\n traceId: string;\n spanId?: string;\n name: string;\n value: number;\n comment?: string;\n metadata?: Record<string, unknown>;\n }): void {\n if (!this.#client) return;\n\n const { id, traceId, spanId, name, value, comment, metadata } = args;\n try {\n this.#client.score.create({\n id,\n traceId,\n ...(spanId ? { observationId: spanId } : {}),\n name,\n value,\n ...(comment ? { comment } : {}),\n ...(metadata ? { metadata } : {}),\n ...(this.#environment ? { environment: this.#environment } : {}),\n dataType: 'NUMERIC' as const,\n });\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Error submitting score`, {\n error,\n traceId,\n spanId,\n name,\n });\n }\n }\n\n async onScoreEvent(event: ScoreEvent): Promise<void> {\n const { score } = event;\n if (!score.traceId) return;\n this.submitScore({\n id: score.scoreId,\n traceId: score.traceId,\n spanId: score.spanId,\n name: score.scorerName ?? score.scorerId,\n value: score.score,\n comment: score.reason,\n metadata: score.metadata,\n });\n }\n\n /**\n * @deprecated Use the observability score event pipeline (`mastra.observability.addScore`)\n * instead. This method is preserved for backwards compatibility and forwards to the same\n * underlying client call as `onScoreEvent`.\n */\n async addScoreToTrace({\n traceId,\n spanId,\n score,\n reason,\n scorerName,\n metadata,\n }: {\n traceId: string;\n spanId?: string;\n score: number;\n reason?: string;\n scorerName: string;\n metadata?: Record<string, any>;\n }): Promise<void> {\n this.submitScore({\n id: `${traceId}-${spanId || ''}-${scorerName}`,\n traceId,\n spanId,\n name: scorerName,\n value: score,\n comment: reason,\n metadata,\n });\n }\n\n async flush(): Promise<void> {\n await Promise.all([this.#processor?.forceFlush(), this.#client?.flush()]);\n }\n\n async shutdown(): Promise<void> {\n await Promise.all([this.#processor?.shutdown(), this.#client?.shutdown()]);\n }\n}\n\n/**\n * Maps Mastra-specific OTel attributes to the langfuse.* namespace that\n * Langfuse's OTLP endpoint reads for prompt linking, TTFT, and other features.\n *\n * SpanConverter produces attributes like mastra.metadata.*, mastra.completion_start_time, etc.\n * Langfuse's OTLP server only reads langfuse.observation.prompt.name, langfuse.observation.completion_start_time, etc.\n *\n * This function mutates the attributes object in place.\n * @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n */\n/**\n * Serialize a root span's input/output for the trace-level attributes.\n * Returns undefined (attribute omitted) for absent values and for values that\n * cannot be JSON-serialized, so a bad payload never fails the span export.\n */\nfunction serializeTraceIo(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value);\n } catch {\n return undefined;\n }\n}\n\nfunction mapMastraToLangfuseAttributes(\n attributes: Record<string, any>,\n span: AnyExportedSpan,\n environment?: string,\n release?: string,\n): void {\n if (attributes['gen_ai.usage.reasoning_tokens'] !== undefined) {\n attributes['gen_ai.usage.reasoning.output_tokens'] = attributes['gen_ai.usage.reasoning_tokens'];\n delete attributes['gen_ai.usage.reasoning_tokens'];\n }\n\n // A run attached under an existing span (tracingOptions.nestUnderParent) does\n // not own the trace. Langfuse applies trace-level attributes from any span,\n // so none of its spans may write them: they would replace the outer run's.\n // The skipped values stay on the observation as mastra.metadata.*.\n const nested = span.nestedUnderParent === true;\n\n // Environment and release: set directly since onStart() is not called\n if (environment) {\n attributes['langfuse.environment'] = environment;\n }\n if (release) {\n attributes['langfuse.release'] = release;\n }\n\n // mastra.metadata.langfuse holds a user-supplied object with two kinds of keys:\n // - the reserved `prompt` key, used for prompt linking, and\n // - arbitrary custom keys the user wants as top-level, filterable trace metadata.\n // Langfuse only allows filtering/grouping traces by top-level metadata\n // (langfuse.trace.metadata.*), so forward every non-prompt key there. These\n // attributes may be set on any span in the trace and land on the trace record.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n const langfuseMetadata = attributes['mastra.metadata.langfuse'];\n if (langfuseMetadata) {\n try {\n const parsed = typeof langfuseMetadata === 'string' ? JSON.parse(langfuseMetadata) : langfuseMetadata;\n if (parsed && typeof parsed === 'object') {\n // Prompt linking: mastra.metadata.langfuse.prompt → langfuse.observation.prompt.name / version\n if (parsed.prompt) {\n if (parsed.prompt.name !== undefined) {\n attributes['langfuse.observation.prompt.name'] = parsed.prompt.name;\n }\n if (parsed.prompt.version !== undefined) {\n attributes['langfuse.observation.prompt.version'] = parsed.prompt.version;\n }\n }\n\n // Custom keys: mastra.metadata.langfuse.<key> → langfuse.trace.metadata.<key>\n // Reserved identity keys (agentId/agentName/workflowId/workflowName) are set\n // by the root-span block below, which runs after this loop and takes precedence.\n for (const [key, value] of Object.entries(parsed)) {\n if (nested || key === 'prompt' || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${key}`;\n // Don't overwrite a trace.metadata key already mapped from another attribute.\n if (attributes[traceKey] === undefined) {\n // Langfuse maps langfuse.trace.metadata.* as string attributes, so\n // serialize non-strings with JSON. Langfuse Cloud restores numbers,\n // booleans, and objects to their original types on ingestion.\n attributes[traceKey] = typeof value === 'string' ? value : JSON.stringify(value);\n }\n }\n }\n } catch {\n // best effort — invalid JSON is silently ignored\n }\n if (!nested) {\n delete attributes['mastra.metadata.langfuse'];\n }\n }\n\n // TTFT: mastra.completion_start_time → langfuse.observation.completion_start_time\n if (attributes['mastra.completion_start_time']) {\n attributes['langfuse.observation.completion_start_time'] = attributes['mastra.completion_start_time'];\n delete attributes['mastra.completion_start_time'];\n }\n\n // Exact provider-reported cost takes precedence over Langfuse's model-price inference.\n // Estimated costs remain unexported so Langfuse can apply its own pricing model.\n const costContext =\n span.type === SpanType.MODEL_GENERATION\n ? (span.attributes as ModelGenerationAttributes | undefined)?.costContext\n : undefined;\n if (\n costContext?.costMetadata?.source === 'provider_reported' &&\n costContext.costUnit === 'USD' &&\n typeof costContext.estimatedCost === 'number' &&\n Number.isFinite(costContext.estimatedCost) &&\n costContext.estimatedCost >= 0\n ) {\n attributes['langfuse.observation.cost_details'] = JSON.stringify({\n total: costContext.estimatedCost,\n });\n }\n\n // User ID: mastra.metadata.userId → user.id\n if (!nested && attributes['mastra.metadata.userId']) {\n attributes['user.id'] = attributes['mastra.metadata.userId'];\n delete attributes['mastra.metadata.userId'];\n }\n\n // OM keeps caller identity separate from its isolated execution thread. Read\n // the raw hint: conversion serializes malformed objects/arrays into strings.\n const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];\n const sessionId =\n attributes['mastra.metadata.sessionId'] ??\n (typeof omCallerThreadId === 'string' && omCallerThreadId\n ? omCallerThreadId\n : attributes['mastra.metadata.threadId']);\n delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];\n if (!nested && sessionId) {\n attributes['session.id'] = sessionId;\n delete attributes['mastra.metadata.sessionId'];\n delete attributes['mastra.metadata.threadId'];\n }\n\n // Tags: mastra.tags → langfuse.trace.tags\n if (attributes['mastra.tags']) {\n attributes['langfuse.trace.tags'] = attributes['mastra.tags'];\n delete attributes['mastra.tags'];\n }\n\n // Trace name: mastra.metadata.traceName → langfuse.trace.name\n if (!nested && attributes['mastra.metadata.traceName']) {\n attributes['langfuse.trace.name'] = attributes['mastra.metadata.traceName'];\n delete attributes['mastra.metadata.traceName'];\n }\n\n // Version: mastra.metadata.version → langfuse.version\n if (attributes['mastra.metadata.version']) {\n attributes['langfuse.version'] = attributes['mastra.metadata.version'];\n delete attributes['mastra.metadata.version'];\n }\n\n // Root-span trace identity: scope each Langfuse trace to the entity that\n // started it (agent or workflow). This makes the Langfuse trace name match\n // the agent/workflow id and exposes the same identity as trace metadata, so\n // users can scope Langfuse evaluators per agent via trace name or metadata\n // filters. User-provided traceName (set via mastra.metadata.traceName) takes\n // precedence and is preserved.\n if (span.isRootSpan && !nested) {\n // Trace input/output: mirror the root span's input/output onto the trace.\n // Without this, Langfuse traces have empty trace-level input/output (the\n // span data only reaches the root OBSERVATION), which breaks LLM-as-a-judge\n // evaluators mapped to Trace input/output and leaves the trace view without\n // the top-level request/response. Serialization failures (circular refs,\n // bigint) skip the attribute rather than failing the span export.\n const traceInput = serializeTraceIo(span.input);\n if (traceInput !== undefined) {\n attributes['langfuse.trace.input'] = traceInput;\n }\n const traceOutput = serializeTraceIo(span.output);\n if (traceOutput !== undefined) {\n attributes['langfuse.trace.output'] = traceOutput;\n }\n if (span.type === SpanType.AGENT_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.agentId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.agentName'] = span.entityName;\n }\n } else if (span.type === SpanType.WORKFLOW_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.workflowId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.workflowName'] = span.entityName;\n }\n }\n\n // Root-span metadata: forward the remaining mastra.metadata.* keys (runId,\n // resourceId, user-supplied keys) to langfuse.trace.metadata.* so they are\n // filterable trace metadata; Langfuse nests unmapped attributes under\n // metadata.attributes. Dedicated keys, explicit metadata.langfuse.* values,\n // and the identity keys above take precedence. Root only: Langfuse applies\n // langfuse.trace.* from any span, so a child span could overwrite the trace.\n for (const [key, value] of Object.entries(attributes)) {\n if (!key.startsWith(MASTRA_METADATA_PREFIX)) {\n continue;\n }\n const metadataKey = key.slice(MASTRA_METADATA_PREFIX.length);\n if (DEDICATED_METADATA_KEYS.has(metadataKey) || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${metadataKey}`;\n if (attributes[traceKey] !== undefined) {\n continue;\n }\n const serialized = serializeTraceIo(value);\n if (serialized !== undefined) {\n attributes[traceKey] = serialized;\n }\n }\n }\n\n // Observation metadata: map semantic attributes to langfuse.observation.metadata.*\n // so they become top-level filterable keys on each observation in Langfuse.\n // @see https://langfuse.com/integrations/native/opentelemetry#how-metadata-mapping-works\n if (attributes['gen_ai.agent.id']) {\n attributes['langfuse.observation.metadata.agentId'] = attributes['gen_ai.agent.id'];\n }\n if (attributes['gen_ai.agent.name']) {\n attributes['langfuse.observation.metadata.agentName'] = attributes['gen_ai.agent.name'];\n }\n if (attributes['mastra.span.type']) {\n attributes['langfuse.observation.metadata.spanType'] = attributes['mastra.span.type'];\n }\n if (attributes['gen_ai.operation.name']) {\n attributes['langfuse.observation.metadata.operationName'] = attributes['gen_ai.operation.name'];\n }\n\n // Input/Output: mastra.*.input/output → langfuse.observation.input/output\n // For gen_ai spans, Langfuse reads gen_ai.input.messages natively.\n // For non-gen_ai spans, we map the first mastra.*.input/output we find.\n if (!attributes['gen_ai.input.messages'] && !attributes['gen_ai.tool.call.arguments']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.input')) {\n attributes['langfuse.observation.input'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n if (!attributes['gen_ai.output.messages'] && !attributes['gen_ai.tool.call.result']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.output')) {\n attributes['langfuse.observation.output'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n}\n\n/**\n * Remove trailing \"/\" characters procedurally. Avoids polynomial\n * backtracking that a greedy regex like `/\\/+$/` can exhibit when the\n * input is attacker-controlled.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* \"/\" */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n","/**\n * Langfuse Tracing Options Helpers\n *\n * These helpers integrate with the `buildTracingOptions` pattern from\n * `@mastra/observability` to add Langfuse-specific tracing features.\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * const agent = new Agent({\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\n\nimport type { TracingOptionsUpdater } from '@mastra/observability';\n\n/**\n * Langfuse prompt input - accepts either a Langfuse SDK prompt object\n * or manual fields.\n */\nexport interface LangfusePromptInput {\n /** Prompt name */\n name?: string;\n /** Prompt version */\n version?: number;\n /** @deprecated Langfuse v5 only supports linking by name + version. This field is ignored. */\n id?: string;\n}\n\n/**\n * Adds Langfuse prompt metadata to the tracing options\n * to enable Langfuse Prompt Tracing.\n *\n * The metadata is added under `metadata.langfuse.prompt` and includes:\n * - `name` - Prompt name (required for Langfuse v5)\n * - `version` - Prompt version (required for Langfuse v5)\n *\n * All fields are deeply merged with any existing metadata.\n *\n * @param prompt - Prompt fields for linking (`name` and `version` required)\n * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * // Link a generation to a Langfuse prompt by name and version\n * const tracingOptions = buildTracingOptions(\n * withLangfusePrompt({ name: 'customer-support', version: 1 }),\n * );\n *\n * // Or directly in agent config\n * const agent = new Agent({\n * name: 'support-agent',\n * instructions: 'You are a helpful assistant',\n * model: openai('gpt-4o'),\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\nexport function withLangfusePrompt(prompt: LangfusePromptInput): TracingOptionsUpdater {\n return opts => ({\n ...opts,\n metadata: {\n ...opts.metadata,\n langfuse: {\n ...(opts.metadata?.langfuse as Record<string, unknown>),\n prompt: {\n ...(prompt.name !== undefined && { name: prompt.name }),\n ...(prompt.version !== undefined && { version: prompt.version }),\n ...(prompt.id !== undefined && { id: prompt.id }),\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -197,6 +197,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
197
197
|
attributes["gen_ai.usage.reasoning.output_tokens"] = attributes["gen_ai.usage.reasoning_tokens"];
|
|
198
198
|
delete attributes["gen_ai.usage.reasoning_tokens"];
|
|
199
199
|
}
|
|
200
|
+
const nested = span.nestedUnderParent === true;
|
|
200
201
|
if (environment) attributes["langfuse.environment"] = environment;
|
|
201
202
|
if (release) attributes["langfuse.release"] = release;
|
|
202
203
|
const langfuseMetadata = attributes["mastra.metadata.langfuse"];
|
|
@@ -209,13 +210,13 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
209
210
|
if (parsed.prompt.version !== void 0) attributes["langfuse.observation.prompt.version"] = parsed.prompt.version;
|
|
210
211
|
}
|
|
211
212
|
for (const [key, value] of Object.entries(parsed)) {
|
|
212
|
-
if (key === "prompt" || value === null || value === void 0) continue;
|
|
213
|
+
if (nested || key === "prompt" || value === null || value === void 0) continue;
|
|
213
214
|
const traceKey = `langfuse.trace.metadata.${key}`;
|
|
214
215
|
if (attributes[traceKey] === void 0) attributes[traceKey] = typeof value === "string" ? value : JSON.stringify(value);
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
} catch {}
|
|
218
|
-
delete attributes["mastra.metadata.langfuse"];
|
|
219
|
+
if (!nested) delete attributes["mastra.metadata.langfuse"];
|
|
219
220
|
}
|
|
220
221
|
if (attributes["mastra.completion_start_time"]) {
|
|
221
222
|
attributes["langfuse.observation.completion_start_time"] = attributes["mastra.completion_start_time"];
|
|
@@ -223,14 +224,14 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
223
224
|
}
|
|
224
225
|
const costContext = span.type === SpanType.MODEL_GENERATION ? span.attributes?.costContext : void 0;
|
|
225
226
|
if (costContext?.costMetadata?.source === "provider_reported" && costContext.costUnit === "USD" && typeof costContext.estimatedCost === "number" && Number.isFinite(costContext.estimatedCost) && costContext.estimatedCost >= 0) attributes["langfuse.observation.cost_details"] = JSON.stringify({ total: costContext.estimatedCost });
|
|
226
|
-
if (attributes["mastra.metadata.userId"]) {
|
|
227
|
+
if (!nested && attributes["mastra.metadata.userId"]) {
|
|
227
228
|
attributes["user.id"] = attributes["mastra.metadata.userId"];
|
|
228
229
|
delete attributes["mastra.metadata.userId"];
|
|
229
230
|
}
|
|
230
231
|
const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];
|
|
231
232
|
const sessionId = attributes["mastra.metadata.sessionId"] ?? (typeof omCallerThreadId === "string" && omCallerThreadId ? omCallerThreadId : attributes["mastra.metadata.threadId"]);
|
|
232
233
|
delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];
|
|
233
|
-
if (sessionId) {
|
|
234
|
+
if (!nested && sessionId) {
|
|
234
235
|
attributes["session.id"] = sessionId;
|
|
235
236
|
delete attributes["mastra.metadata.sessionId"];
|
|
236
237
|
delete attributes["mastra.metadata.threadId"];
|
|
@@ -239,7 +240,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
239
240
|
attributes["langfuse.trace.tags"] = attributes["mastra.tags"];
|
|
240
241
|
delete attributes["mastra.tags"];
|
|
241
242
|
}
|
|
242
|
-
if (attributes["mastra.metadata.traceName"]) {
|
|
243
|
+
if (!nested && attributes["mastra.metadata.traceName"]) {
|
|
243
244
|
attributes["langfuse.trace.name"] = attributes["mastra.metadata.traceName"];
|
|
244
245
|
delete attributes["mastra.metadata.traceName"];
|
|
245
246
|
}
|
|
@@ -247,7 +248,7 @@ function mapMastraToLangfuseAttributes(attributes, span, environment, release) {
|
|
|
247
248
|
attributes["langfuse.version"] = attributes["mastra.metadata.version"];
|
|
248
249
|
delete attributes["mastra.metadata.version"];
|
|
249
250
|
}
|
|
250
|
-
if (span.isRootSpan) {
|
|
251
|
+
if (span.isRootSpan && !nested) {
|
|
251
252
|
const traceInput = serializeTraceIo(span.input);
|
|
252
253
|
if (traceInput !== void 0) attributes["langfuse.trace.input"] = traceInput;
|
|
253
254
|
const traceOutput = serializeTraceIo(span.output);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#realtime","#resourceAttributes","#processor","#client","#environment","#release","#spanConverter"],"sources":["../src/tracing.ts","../src/helpers.ts"],"sourcesContent":["/**\n * Langfuse Exporter for Mastra Observability\n *\n * Sends observability data to Langfuse using the official @langfuse/otel span processor\n * and @langfuse/client for non-tracing features (scoring, prompt management, evaluations).\n *\n * @see https://langfuse.com/docs/observability/sdk/typescript/overview\n */\n\nimport { LangfuseClient } from '@langfuse/client';\nimport { LangfuseSpanProcessor } from '@langfuse/otel';\nimport type {\n TracingEvent,\n AnyExportedSpan,\n InitExporterOptions,\n ModelGenerationAttributes,\n ScoreEvent,\n} from '@mastra/core/observability';\nimport { SpanType, TracingEventType } from '@mastra/core/observability';\nimport { BaseExporter } from '@mastra/observability';\nimport type { BaseExporterConfig } from '@mastra/observability';\nimport { SpanConverter } from '@mastra/otel-exporter';\nimport type { OtelExporterConfig } from '@mastra/otel-exporter';\n\nconst LOG_PREFIX = '[LangfuseExporter]';\nconst MASTRA_METADATA_PREFIX = 'mastra.metadata.';\n/** Metadata keys mapped to dedicated Langfuse fields; never forwarded as trace metadata. */\nconst OM_CALLER_THREAD_ID = '__mastraObservationalMemoryCallerThreadId';\nconst DEDICATED_METADATA_KEYS = new Set([\n 'userId',\n 'sessionId',\n 'threadId',\n 'traceName',\n 'version',\n 'langfuse',\n OM_CALLER_THREAD_ID,\n]);\n\nexport const LANGFUSE_DEFAULT_BASE_URL = 'https://cloud.langfuse.com';\n\nexport interface LangfuseExporterConfig extends BaseExporterConfig {\n /** Langfuse public key */\n publicKey?: string;\n /** Langfuse secret key */\n secretKey?: string;\n /** Langfuse host URL (defaults to https://cloud.langfuse.com) */\n baseUrl?: string;\n /** Additional headers sent with requests to Langfuse */\n additionalHeaders?: Record<string, string>;\n /** Enable realtime mode - flushes after each event for immediate visibility */\n realtime?: boolean;\n /** Maximum number of spans per OTEL export batch */\n flushAt?: number;\n /** Maximum time in seconds before pending spans are exported */\n flushInterval?: number;\n /** Langfuse environment tag for traces */\n environment?: string;\n /** Langfuse release tag for traces */\n release?: string;\n /** Override OpenTelemetry resource attributes, including service.version. */\n resourceAttributes?: OtelExporterConfig['resourceAttributes'];\n}\n\nexport class LangfuseExporter extends BaseExporter {\n name = 'langfuse';\n #processor: LangfuseSpanProcessor | undefined;\n #client: LangfuseClient | undefined;\n #spanConverter: SpanConverter | undefined;\n #realtime: boolean;\n #environment: string | undefined;\n #release: string | undefined;\n #resourceAttributes: OtelExporterConfig['resourceAttributes'];\n\n constructor(config: LangfuseExporterConfig = {}) {\n super(config);\n\n const publicKey = config.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;\n const secretKey = config.secretKey ?? process.env.LANGFUSE_SECRET_KEY;\n const baseUrl = stripTrailingSlashes(config.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? LANGFUSE_DEFAULT_BASE_URL);\n this.#realtime = config.realtime ?? false;\n this.#resourceAttributes = config.resourceAttributes;\n\n if (!publicKey || !secretKey) {\n const publicKeySource = config.publicKey\n ? 'from config'\n : process.env.LANGFUSE_PUBLIC_KEY\n ? 'from env'\n : 'missing';\n const secretKeySource = config.secretKey\n ? 'from config'\n : process.env.LANGFUSE_SECRET_KEY\n ? 'from env'\n : 'missing';\n this.setDisabled(\n `${LOG_PREFIX} Missing required credentials (publicKey: ${publicKeySource}, secretKey: ${secretKeySource}). ` +\n `Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment variables or pass them in config.`,\n );\n return;\n }\n\n this.#processor = new LangfuseSpanProcessor({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n environment: config.environment,\n release: config.release,\n exportMode: this.#realtime ? 'immediate' : 'batched',\n flushAt: config.flushAt,\n flushInterval: config.flushInterval,\n // Export all spans — the default filter only passes spans with gen_ai.* attributes\n // or known LLM instrumentation scopes, but Mastra spans use mastra.* attributes.\n shouldExportSpan: () => true,\n });\n\n this.#client = new LangfuseClient({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n });\n\n this.#environment = config.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT;\n this.#release = config.release ?? process.env.LANGFUSE_RELEASE;\n }\n\n init(options: InitExporterOptions) {\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: options.config?.serviceName,\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n protected async _exportTracingEvent(event: TracingEvent): Promise<void> {\n if (event.type !== TracingEventType.SPAN_ENDED) return;\n if (!this.#processor) return;\n\n await this.exportSpan(event.exportedSpan);\n }\n\n private async exportSpan(span: AnyExportedSpan): Promise<void> {\n if (!this.#spanConverter) {\n // Fallback if init() was not called (e.g., standalone usage without Mastra)\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: 'mastra-service',\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n try {\n const otelSpan = await this.#spanConverter.convertSpan(span);\n\n // Map mastra.* attributes to langfuse.* namespace so that Langfuse's OTLP\n // endpoint reads them correctly. SpanConverter produces mastra.* attributes,\n // but Langfuse only reads langfuse.* attributes for prompt linking, TTFT, etc.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n mapMastraToLangfuseAttributes(otelSpan.attributes, span, this.#environment, this.#release);\n\n this.#processor!.onEnd(otelSpan);\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Failed to export span ${span.id}:`, error);\n }\n }\n\n /**\n * The LangfuseClient instance for advanced Langfuse features.\n * Use this for prompt management, evaluations, datasets, and direct API access.\n */\n get client(): LangfuseClient | undefined {\n return this.#client;\n }\n\n /**\n * Submit a score to Langfuse. Used by both the new `onScoreEvent` path and the\n * deprecated `addScoreToTrace` wrapper.\n */\n private submitScore(args: {\n id: string;\n traceId: string;\n spanId?: string;\n name: string;\n value: number;\n comment?: string;\n metadata?: Record<string, unknown>;\n }): void {\n if (!this.#client) return;\n\n const { id, traceId, spanId, name, value, comment, metadata } = args;\n try {\n this.#client.score.create({\n id,\n traceId,\n ...(spanId ? { observationId: spanId } : {}),\n name,\n value,\n ...(comment ? { comment } : {}),\n ...(metadata ? { metadata } : {}),\n ...(this.#environment ? { environment: this.#environment } : {}),\n dataType: 'NUMERIC' as const,\n });\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Error submitting score`, {\n error,\n traceId,\n spanId,\n name,\n });\n }\n }\n\n async onScoreEvent(event: ScoreEvent): Promise<void> {\n const { score } = event;\n if (!score.traceId) return;\n this.submitScore({\n id: score.scoreId,\n traceId: score.traceId,\n spanId: score.spanId,\n name: score.scorerName ?? score.scorerId,\n value: score.score,\n comment: score.reason,\n metadata: score.metadata,\n });\n }\n\n /**\n * @deprecated Use the observability score event pipeline (`mastra.observability.addScore`)\n * instead. This method is preserved for backwards compatibility and forwards to the same\n * underlying client call as `onScoreEvent`.\n */\n async addScoreToTrace({\n traceId,\n spanId,\n score,\n reason,\n scorerName,\n metadata,\n }: {\n traceId: string;\n spanId?: string;\n score: number;\n reason?: string;\n scorerName: string;\n metadata?: Record<string, any>;\n }): Promise<void> {\n this.submitScore({\n id: `${traceId}-${spanId || ''}-${scorerName}`,\n traceId,\n spanId,\n name: scorerName,\n value: score,\n comment: reason,\n metadata,\n });\n }\n\n async flush(): Promise<void> {\n await Promise.all([this.#processor?.forceFlush(), this.#client?.flush()]);\n }\n\n async shutdown(): Promise<void> {\n await Promise.all([this.#processor?.shutdown(), this.#client?.shutdown()]);\n }\n}\n\n/**\n * Maps Mastra-specific OTel attributes to the langfuse.* namespace that\n * Langfuse's OTLP endpoint reads for prompt linking, TTFT, and other features.\n *\n * SpanConverter produces attributes like mastra.metadata.*, mastra.completion_start_time, etc.\n * Langfuse's OTLP server only reads langfuse.observation.prompt.name, langfuse.observation.completion_start_time, etc.\n *\n * This function mutates the attributes object in place.\n * @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n */\n/**\n * Serialize a root span's input/output for the trace-level attributes.\n * Returns undefined (attribute omitted) for absent values and for values that\n * cannot be JSON-serialized, so a bad payload never fails the span export.\n */\nfunction serializeTraceIo(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value);\n } catch {\n return undefined;\n }\n}\n\nfunction mapMastraToLangfuseAttributes(\n attributes: Record<string, any>,\n span: AnyExportedSpan,\n environment?: string,\n release?: string,\n): void {\n if (attributes['gen_ai.usage.reasoning_tokens'] !== undefined) {\n attributes['gen_ai.usage.reasoning.output_tokens'] = attributes['gen_ai.usage.reasoning_tokens'];\n delete attributes['gen_ai.usage.reasoning_tokens'];\n }\n\n // Environment and release: set directly since onStart() is not called\n if (environment) {\n attributes['langfuse.environment'] = environment;\n }\n if (release) {\n attributes['langfuse.release'] = release;\n }\n\n // mastra.metadata.langfuse holds a user-supplied object with two kinds of keys:\n // - the reserved `prompt` key, used for prompt linking, and\n // - arbitrary custom keys the user wants as top-level, filterable trace metadata.\n // Langfuse only allows filtering/grouping traces by top-level metadata\n // (langfuse.trace.metadata.*), so forward every non-prompt key there. These\n // attributes may be set on any span in the trace and land on the trace record.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n const langfuseMetadata = attributes['mastra.metadata.langfuse'];\n if (langfuseMetadata) {\n try {\n const parsed = typeof langfuseMetadata === 'string' ? JSON.parse(langfuseMetadata) : langfuseMetadata;\n if (parsed && typeof parsed === 'object') {\n // Prompt linking: mastra.metadata.langfuse.prompt → langfuse.observation.prompt.name / version\n if (parsed.prompt) {\n if (parsed.prompt.name !== undefined) {\n attributes['langfuse.observation.prompt.name'] = parsed.prompt.name;\n }\n if (parsed.prompt.version !== undefined) {\n attributes['langfuse.observation.prompt.version'] = parsed.prompt.version;\n }\n }\n\n // Custom keys: mastra.metadata.langfuse.<key> → langfuse.trace.metadata.<key>\n // Reserved identity keys (agentId/agentName/workflowId/workflowName) are set\n // by the root-span block below, which runs after this loop and takes precedence.\n for (const [key, value] of Object.entries(parsed)) {\n if (key === 'prompt' || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${key}`;\n // Don't overwrite a trace.metadata key already mapped from another attribute.\n if (attributes[traceKey] === undefined) {\n // Langfuse maps langfuse.trace.metadata.* as string attributes, so\n // serialize non-strings with JSON. Langfuse Cloud restores numbers,\n // booleans, and objects to their original types on ingestion.\n attributes[traceKey] = typeof value === 'string' ? value : JSON.stringify(value);\n }\n }\n }\n } catch {\n // best effort — invalid JSON is silently ignored\n }\n delete attributes['mastra.metadata.langfuse'];\n }\n\n // TTFT: mastra.completion_start_time → langfuse.observation.completion_start_time\n if (attributes['mastra.completion_start_time']) {\n attributes['langfuse.observation.completion_start_time'] = attributes['mastra.completion_start_time'];\n delete attributes['mastra.completion_start_time'];\n }\n\n // Exact provider-reported cost takes precedence over Langfuse's model-price inference.\n // Estimated costs remain unexported so Langfuse can apply its own pricing model.\n const costContext =\n span.type === SpanType.MODEL_GENERATION\n ? (span.attributes as ModelGenerationAttributes | undefined)?.costContext\n : undefined;\n if (\n costContext?.costMetadata?.source === 'provider_reported' &&\n costContext.costUnit === 'USD' &&\n typeof costContext.estimatedCost === 'number' &&\n Number.isFinite(costContext.estimatedCost) &&\n costContext.estimatedCost >= 0\n ) {\n attributes['langfuse.observation.cost_details'] = JSON.stringify({\n total: costContext.estimatedCost,\n });\n }\n\n // User ID: mastra.metadata.userId → user.id\n if (attributes['mastra.metadata.userId']) {\n attributes['user.id'] = attributes['mastra.metadata.userId'];\n delete attributes['mastra.metadata.userId'];\n }\n\n // OM keeps caller identity separate from its isolated execution thread. Read\n // the raw hint: conversion serializes malformed objects/arrays into strings.\n const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];\n const sessionId =\n attributes['mastra.metadata.sessionId'] ??\n (typeof omCallerThreadId === 'string' && omCallerThreadId\n ? omCallerThreadId\n : attributes['mastra.metadata.threadId']);\n delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];\n if (sessionId) {\n attributes['session.id'] = sessionId;\n delete attributes['mastra.metadata.sessionId'];\n delete attributes['mastra.metadata.threadId'];\n }\n\n // Tags: mastra.tags → langfuse.trace.tags\n if (attributes['mastra.tags']) {\n attributes['langfuse.trace.tags'] = attributes['mastra.tags'];\n delete attributes['mastra.tags'];\n }\n\n // Trace name: mastra.metadata.traceName → langfuse.trace.name\n if (attributes['mastra.metadata.traceName']) {\n attributes['langfuse.trace.name'] = attributes['mastra.metadata.traceName'];\n delete attributes['mastra.metadata.traceName'];\n }\n\n // Version: mastra.metadata.version → langfuse.version\n if (attributes['mastra.metadata.version']) {\n attributes['langfuse.version'] = attributes['mastra.metadata.version'];\n delete attributes['mastra.metadata.version'];\n }\n\n // Root-span trace identity: scope each Langfuse trace to the entity that\n // started it (agent or workflow). This makes the Langfuse trace name match\n // the agent/workflow id and exposes the same identity as trace metadata, so\n // users can scope Langfuse evaluators per agent via trace name or metadata\n // filters. User-provided traceName (set via mastra.metadata.traceName) takes\n // precedence and is preserved.\n if (span.isRootSpan) {\n // Trace input/output: mirror the root span's input/output onto the trace.\n // Without this, Langfuse traces have empty trace-level input/output (the\n // span data only reaches the root OBSERVATION), which breaks LLM-as-a-judge\n // evaluators mapped to Trace input/output and leaves the trace view without\n // the top-level request/response. Serialization failures (circular refs,\n // bigint) skip the attribute rather than failing the span export.\n const traceInput = serializeTraceIo(span.input);\n if (traceInput !== undefined) {\n attributes['langfuse.trace.input'] = traceInput;\n }\n const traceOutput = serializeTraceIo(span.output);\n if (traceOutput !== undefined) {\n attributes['langfuse.trace.output'] = traceOutput;\n }\n if (span.type === SpanType.AGENT_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.agentId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.agentName'] = span.entityName;\n }\n } else if (span.type === SpanType.WORKFLOW_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.workflowId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.workflowName'] = span.entityName;\n }\n }\n\n // Root-span metadata: forward the remaining mastra.metadata.* keys (runId,\n // resourceId, user-supplied keys) to langfuse.trace.metadata.* so they are\n // filterable trace metadata; Langfuse nests unmapped attributes under\n // metadata.attributes. Dedicated keys, explicit metadata.langfuse.* values,\n // and the identity keys above take precedence. Root only: Langfuse applies\n // langfuse.trace.* from any span, so a child span could overwrite the trace.\n for (const [key, value] of Object.entries(attributes)) {\n if (!key.startsWith(MASTRA_METADATA_PREFIX)) {\n continue;\n }\n const metadataKey = key.slice(MASTRA_METADATA_PREFIX.length);\n if (DEDICATED_METADATA_KEYS.has(metadataKey) || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${metadataKey}`;\n if (attributes[traceKey] !== undefined) {\n continue;\n }\n const serialized = serializeTraceIo(value);\n if (serialized !== undefined) {\n attributes[traceKey] = serialized;\n }\n }\n }\n\n // Observation metadata: map semantic attributes to langfuse.observation.metadata.*\n // so they become top-level filterable keys on each observation in Langfuse.\n // @see https://langfuse.com/integrations/native/opentelemetry#how-metadata-mapping-works\n if (attributes['gen_ai.agent.id']) {\n attributes['langfuse.observation.metadata.agentId'] = attributes['gen_ai.agent.id'];\n }\n if (attributes['gen_ai.agent.name']) {\n attributes['langfuse.observation.metadata.agentName'] = attributes['gen_ai.agent.name'];\n }\n if (attributes['mastra.span.type']) {\n attributes['langfuse.observation.metadata.spanType'] = attributes['mastra.span.type'];\n }\n if (attributes['gen_ai.operation.name']) {\n attributes['langfuse.observation.metadata.operationName'] = attributes['gen_ai.operation.name'];\n }\n\n // Input/Output: mastra.*.input/output → langfuse.observation.input/output\n // For gen_ai spans, Langfuse reads gen_ai.input.messages natively.\n // For non-gen_ai spans, we map the first mastra.*.input/output we find.\n if (!attributes['gen_ai.input.messages'] && !attributes['gen_ai.tool.call.arguments']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.input')) {\n attributes['langfuse.observation.input'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n if (!attributes['gen_ai.output.messages'] && !attributes['gen_ai.tool.call.result']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.output')) {\n attributes['langfuse.observation.output'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n}\n\n/**\n * Remove trailing \"/\" characters procedurally. Avoids polynomial\n * backtracking that a greedy regex like `/\\/+$/` can exhibit when the\n * input is attacker-controlled.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* \"/\" */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n","/**\n * Langfuse Tracing Options Helpers\n *\n * These helpers integrate with the `buildTracingOptions` pattern from\n * `@mastra/observability` to add Langfuse-specific tracing features.\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * const agent = new Agent({\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\n\nimport type { TracingOptionsUpdater } from '@mastra/observability';\n\n/**\n * Langfuse prompt input - accepts either a Langfuse SDK prompt object\n * or manual fields.\n */\nexport interface LangfusePromptInput {\n /** Prompt name */\n name?: string;\n /** Prompt version */\n version?: number;\n /** @deprecated Langfuse v5 only supports linking by name + version. This field is ignored. */\n id?: string;\n}\n\n/**\n * Adds Langfuse prompt metadata to the tracing options\n * to enable Langfuse Prompt Tracing.\n *\n * The metadata is added under `metadata.langfuse.prompt` and includes:\n * - `name` - Prompt name (required for Langfuse v5)\n * - `version` - Prompt version (required for Langfuse v5)\n *\n * All fields are deeply merged with any existing metadata.\n *\n * @param prompt - Prompt fields for linking (`name` and `version` required)\n * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * // Link a generation to a Langfuse prompt by name and version\n * const tracingOptions = buildTracingOptions(\n * withLangfusePrompt({ name: 'customer-support', version: 1 }),\n * );\n *\n * // Or directly in agent config\n * const agent = new Agent({\n * name: 'support-agent',\n * instructions: 'You are a helpful assistant',\n * model: openai('gpt-4o'),\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\nexport function withLangfusePrompt(prompt: LangfusePromptInput): TracingOptionsUpdater {\n return opts => ({\n ...opts,\n metadata: {\n ...opts.metadata,\n langfuse: {\n ...(opts.metadata?.langfuse as Record<string, unknown>),\n prompt: {\n ...(prompt.name !== undefined && { name: prompt.name }),\n ...(prompt.version !== undefined && { version: prompt.version }),\n ...(prompt.id !== undefined && { id: prompt.id }),\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#realtime","#resourceAttributes","#processor","#client","#environment","#release","#spanConverter"],"sources":["../src/tracing.ts","../src/helpers.ts"],"sourcesContent":["/**\n * Langfuse Exporter for Mastra Observability\n *\n * Sends observability data to Langfuse using the official @langfuse/otel span processor\n * and @langfuse/client for non-tracing features (scoring, prompt management, evaluations).\n *\n * @see https://langfuse.com/docs/observability/sdk/typescript/overview\n */\n\nimport { LangfuseClient } from '@langfuse/client';\nimport { LangfuseSpanProcessor } from '@langfuse/otel';\nimport type {\n TracingEvent,\n AnyExportedSpan,\n InitExporterOptions,\n ModelGenerationAttributes,\n ScoreEvent,\n} from '@mastra/core/observability';\nimport { SpanType, TracingEventType } from '@mastra/core/observability';\nimport { BaseExporter } from '@mastra/observability';\nimport type { BaseExporterConfig } from '@mastra/observability';\nimport { SpanConverter } from '@mastra/otel-exporter';\nimport type { OtelExporterConfig } from '@mastra/otel-exporter';\n\nconst LOG_PREFIX = '[LangfuseExporter]';\nconst MASTRA_METADATA_PREFIX = 'mastra.metadata.';\n/** Metadata keys mapped to dedicated Langfuse fields; never forwarded as trace metadata. */\nconst OM_CALLER_THREAD_ID = '__mastraObservationalMemoryCallerThreadId';\nconst DEDICATED_METADATA_KEYS = new Set([\n 'userId',\n 'sessionId',\n 'threadId',\n 'traceName',\n 'version',\n 'langfuse',\n OM_CALLER_THREAD_ID,\n]);\n\nexport const LANGFUSE_DEFAULT_BASE_URL = 'https://cloud.langfuse.com';\n\nexport interface LangfuseExporterConfig extends BaseExporterConfig {\n /** Langfuse public key */\n publicKey?: string;\n /** Langfuse secret key */\n secretKey?: string;\n /** Langfuse host URL (defaults to https://cloud.langfuse.com) */\n baseUrl?: string;\n /** Additional headers sent with requests to Langfuse */\n additionalHeaders?: Record<string, string>;\n /** Enable realtime mode - flushes after each event for immediate visibility */\n realtime?: boolean;\n /** Maximum number of spans per OTEL export batch */\n flushAt?: number;\n /** Maximum time in seconds before pending spans are exported */\n flushInterval?: number;\n /** Langfuse environment tag for traces */\n environment?: string;\n /** Langfuse release tag for traces */\n release?: string;\n /** Override OpenTelemetry resource attributes, including service.version. */\n resourceAttributes?: OtelExporterConfig['resourceAttributes'];\n}\n\nexport class LangfuseExporter extends BaseExporter {\n name = 'langfuse';\n #processor: LangfuseSpanProcessor | undefined;\n #client: LangfuseClient | undefined;\n #spanConverter: SpanConverter | undefined;\n #realtime: boolean;\n #environment: string | undefined;\n #release: string | undefined;\n #resourceAttributes: OtelExporterConfig['resourceAttributes'];\n\n constructor(config: LangfuseExporterConfig = {}) {\n super(config);\n\n const publicKey = config.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;\n const secretKey = config.secretKey ?? process.env.LANGFUSE_SECRET_KEY;\n const baseUrl = stripTrailingSlashes(config.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? LANGFUSE_DEFAULT_BASE_URL);\n this.#realtime = config.realtime ?? false;\n this.#resourceAttributes = config.resourceAttributes;\n\n if (!publicKey || !secretKey) {\n const publicKeySource = config.publicKey\n ? 'from config'\n : process.env.LANGFUSE_PUBLIC_KEY\n ? 'from env'\n : 'missing';\n const secretKeySource = config.secretKey\n ? 'from config'\n : process.env.LANGFUSE_SECRET_KEY\n ? 'from env'\n : 'missing';\n this.setDisabled(\n `${LOG_PREFIX} Missing required credentials (publicKey: ${publicKeySource}, secretKey: ${secretKeySource}). ` +\n `Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment variables or pass them in config.`,\n );\n return;\n }\n\n this.#processor = new LangfuseSpanProcessor({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n environment: config.environment,\n release: config.release,\n exportMode: this.#realtime ? 'immediate' : 'batched',\n flushAt: config.flushAt,\n flushInterval: config.flushInterval,\n // Export all spans — the default filter only passes spans with gen_ai.* attributes\n // or known LLM instrumentation scopes, but Mastra spans use mastra.* attributes.\n shouldExportSpan: () => true,\n });\n\n this.#client = new LangfuseClient({\n publicKey,\n secretKey,\n baseUrl,\n additionalHeaders: config.additionalHeaders,\n });\n\n this.#environment = config.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT;\n this.#release = config.release ?? process.env.LANGFUSE_RELEASE;\n }\n\n init(options: InitExporterOptions) {\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: options.config?.serviceName,\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n protected async _exportTracingEvent(event: TracingEvent): Promise<void> {\n if (event.type !== TracingEventType.SPAN_ENDED) return;\n if (!this.#processor) return;\n\n await this.exportSpan(event.exportedSpan);\n }\n\n private async exportSpan(span: AnyExportedSpan): Promise<void> {\n if (!this.#spanConverter) {\n // Fallback if init() was not called (e.g., standalone usage without Mastra)\n this.#spanConverter = new SpanConverter({\n packageName: '@mastra/langfuse',\n serviceName: 'mastra-service',\n config: { resourceAttributes: this.#resourceAttributes },\n format: 'GenAI_v1_38_0',\n });\n }\n\n try {\n const otelSpan = await this.#spanConverter.convertSpan(span);\n\n // Map mastra.* attributes to langfuse.* namespace so that Langfuse's OTLP\n // endpoint reads them correctly. SpanConverter produces mastra.* attributes,\n // but Langfuse only reads langfuse.* attributes for prompt linking, TTFT, etc.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n mapMastraToLangfuseAttributes(otelSpan.attributes, span, this.#environment, this.#release);\n\n this.#processor!.onEnd(otelSpan);\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Failed to export span ${span.id}:`, error);\n }\n }\n\n /**\n * The LangfuseClient instance for advanced Langfuse features.\n * Use this for prompt management, evaluations, datasets, and direct API access.\n */\n get client(): LangfuseClient | undefined {\n return this.#client;\n }\n\n /**\n * Submit a score to Langfuse. Used by both the new `onScoreEvent` path and the\n * deprecated `addScoreToTrace` wrapper.\n */\n private submitScore(args: {\n id: string;\n traceId: string;\n spanId?: string;\n name: string;\n value: number;\n comment?: string;\n metadata?: Record<string, unknown>;\n }): void {\n if (!this.#client) return;\n\n const { id, traceId, spanId, name, value, comment, metadata } = args;\n try {\n this.#client.score.create({\n id,\n traceId,\n ...(spanId ? { observationId: spanId } : {}),\n name,\n value,\n ...(comment ? { comment } : {}),\n ...(metadata ? { metadata } : {}),\n ...(this.#environment ? { environment: this.#environment } : {}),\n dataType: 'NUMERIC' as const,\n });\n } catch (error) {\n this.logger.error(`${LOG_PREFIX} Error submitting score`, {\n error,\n traceId,\n spanId,\n name,\n });\n }\n }\n\n async onScoreEvent(event: ScoreEvent): Promise<void> {\n const { score } = event;\n if (!score.traceId) return;\n this.submitScore({\n id: score.scoreId,\n traceId: score.traceId,\n spanId: score.spanId,\n name: score.scorerName ?? score.scorerId,\n value: score.score,\n comment: score.reason,\n metadata: score.metadata,\n });\n }\n\n /**\n * @deprecated Use the observability score event pipeline (`mastra.observability.addScore`)\n * instead. This method is preserved for backwards compatibility and forwards to the same\n * underlying client call as `onScoreEvent`.\n */\n async addScoreToTrace({\n traceId,\n spanId,\n score,\n reason,\n scorerName,\n metadata,\n }: {\n traceId: string;\n spanId?: string;\n score: number;\n reason?: string;\n scorerName: string;\n metadata?: Record<string, any>;\n }): Promise<void> {\n this.submitScore({\n id: `${traceId}-${spanId || ''}-${scorerName}`,\n traceId,\n spanId,\n name: scorerName,\n value: score,\n comment: reason,\n metadata,\n });\n }\n\n async flush(): Promise<void> {\n await Promise.all([this.#processor?.forceFlush(), this.#client?.flush()]);\n }\n\n async shutdown(): Promise<void> {\n await Promise.all([this.#processor?.shutdown(), this.#client?.shutdown()]);\n }\n}\n\n/**\n * Maps Mastra-specific OTel attributes to the langfuse.* namespace that\n * Langfuse's OTLP endpoint reads for prompt linking, TTFT, and other features.\n *\n * SpanConverter produces attributes like mastra.metadata.*, mastra.completion_start_time, etc.\n * Langfuse's OTLP server only reads langfuse.observation.prompt.name, langfuse.observation.completion_start_time, etc.\n *\n * This function mutates the attributes object in place.\n * @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n */\n/**\n * Serialize a root span's input/output for the trace-level attributes.\n * Returns undefined (attribute omitted) for absent values and for values that\n * cannot be JSON-serialized, so a bad payload never fails the span export.\n */\nfunction serializeTraceIo(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value);\n } catch {\n return undefined;\n }\n}\n\nfunction mapMastraToLangfuseAttributes(\n attributes: Record<string, any>,\n span: AnyExportedSpan,\n environment?: string,\n release?: string,\n): void {\n if (attributes['gen_ai.usage.reasoning_tokens'] !== undefined) {\n attributes['gen_ai.usage.reasoning.output_tokens'] = attributes['gen_ai.usage.reasoning_tokens'];\n delete attributes['gen_ai.usage.reasoning_tokens'];\n }\n\n // A run attached under an existing span (tracingOptions.nestUnderParent) does\n // not own the trace. Langfuse applies trace-level attributes from any span,\n // so none of its spans may write them: they would replace the outer run's.\n // The skipped values stay on the observation as mastra.metadata.*.\n const nested = span.nestedUnderParent === true;\n\n // Environment and release: set directly since onStart() is not called\n if (environment) {\n attributes['langfuse.environment'] = environment;\n }\n if (release) {\n attributes['langfuse.release'] = release;\n }\n\n // mastra.metadata.langfuse holds a user-supplied object with two kinds of keys:\n // - the reserved `prompt` key, used for prompt linking, and\n // - arbitrary custom keys the user wants as top-level, filterable trace metadata.\n // Langfuse only allows filtering/grouping traces by top-level metadata\n // (langfuse.trace.metadata.*), so forward every non-prompt key there. These\n // attributes may be set on any span in the trace and land on the trace record.\n // @see https://langfuse.com/integrations/native/opentelemetry#property-mapping\n const langfuseMetadata = attributes['mastra.metadata.langfuse'];\n if (langfuseMetadata) {\n try {\n const parsed = typeof langfuseMetadata === 'string' ? JSON.parse(langfuseMetadata) : langfuseMetadata;\n if (parsed && typeof parsed === 'object') {\n // Prompt linking: mastra.metadata.langfuse.prompt → langfuse.observation.prompt.name / version\n if (parsed.prompt) {\n if (parsed.prompt.name !== undefined) {\n attributes['langfuse.observation.prompt.name'] = parsed.prompt.name;\n }\n if (parsed.prompt.version !== undefined) {\n attributes['langfuse.observation.prompt.version'] = parsed.prompt.version;\n }\n }\n\n // Custom keys: mastra.metadata.langfuse.<key> → langfuse.trace.metadata.<key>\n // Reserved identity keys (agentId/agentName/workflowId/workflowName) are set\n // by the root-span block below, which runs after this loop and takes precedence.\n for (const [key, value] of Object.entries(parsed)) {\n if (nested || key === 'prompt' || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${key}`;\n // Don't overwrite a trace.metadata key already mapped from another attribute.\n if (attributes[traceKey] === undefined) {\n // Langfuse maps langfuse.trace.metadata.* as string attributes, so\n // serialize non-strings with JSON. Langfuse Cloud restores numbers,\n // booleans, and objects to their original types on ingestion.\n attributes[traceKey] = typeof value === 'string' ? value : JSON.stringify(value);\n }\n }\n }\n } catch {\n // best effort — invalid JSON is silently ignored\n }\n if (!nested) {\n delete attributes['mastra.metadata.langfuse'];\n }\n }\n\n // TTFT: mastra.completion_start_time → langfuse.observation.completion_start_time\n if (attributes['mastra.completion_start_time']) {\n attributes['langfuse.observation.completion_start_time'] = attributes['mastra.completion_start_time'];\n delete attributes['mastra.completion_start_time'];\n }\n\n // Exact provider-reported cost takes precedence over Langfuse's model-price inference.\n // Estimated costs remain unexported so Langfuse can apply its own pricing model.\n const costContext =\n span.type === SpanType.MODEL_GENERATION\n ? (span.attributes as ModelGenerationAttributes | undefined)?.costContext\n : undefined;\n if (\n costContext?.costMetadata?.source === 'provider_reported' &&\n costContext.costUnit === 'USD' &&\n typeof costContext.estimatedCost === 'number' &&\n Number.isFinite(costContext.estimatedCost) &&\n costContext.estimatedCost >= 0\n ) {\n attributes['langfuse.observation.cost_details'] = JSON.stringify({\n total: costContext.estimatedCost,\n });\n }\n\n // User ID: mastra.metadata.userId → user.id\n if (!nested && attributes['mastra.metadata.userId']) {\n attributes['user.id'] = attributes['mastra.metadata.userId'];\n delete attributes['mastra.metadata.userId'];\n }\n\n // OM keeps caller identity separate from its isolated execution thread. Read\n // the raw hint: conversion serializes malformed objects/arrays into strings.\n const omCallerThreadId = span.metadata?.[OM_CALLER_THREAD_ID];\n const sessionId =\n attributes['mastra.metadata.sessionId'] ??\n (typeof omCallerThreadId === 'string' && omCallerThreadId\n ? omCallerThreadId\n : attributes['mastra.metadata.threadId']);\n delete attributes[`${MASTRA_METADATA_PREFIX}${OM_CALLER_THREAD_ID}`];\n if (!nested && sessionId) {\n attributes['session.id'] = sessionId;\n delete attributes['mastra.metadata.sessionId'];\n delete attributes['mastra.metadata.threadId'];\n }\n\n // Tags: mastra.tags → langfuse.trace.tags\n if (attributes['mastra.tags']) {\n attributes['langfuse.trace.tags'] = attributes['mastra.tags'];\n delete attributes['mastra.tags'];\n }\n\n // Trace name: mastra.metadata.traceName → langfuse.trace.name\n if (!nested && attributes['mastra.metadata.traceName']) {\n attributes['langfuse.trace.name'] = attributes['mastra.metadata.traceName'];\n delete attributes['mastra.metadata.traceName'];\n }\n\n // Version: mastra.metadata.version → langfuse.version\n if (attributes['mastra.metadata.version']) {\n attributes['langfuse.version'] = attributes['mastra.metadata.version'];\n delete attributes['mastra.metadata.version'];\n }\n\n // Root-span trace identity: scope each Langfuse trace to the entity that\n // started it (agent or workflow). This makes the Langfuse trace name match\n // the agent/workflow id and exposes the same identity as trace metadata, so\n // users can scope Langfuse evaluators per agent via trace name or metadata\n // filters. User-provided traceName (set via mastra.metadata.traceName) takes\n // precedence and is preserved.\n if (span.isRootSpan && !nested) {\n // Trace input/output: mirror the root span's input/output onto the trace.\n // Without this, Langfuse traces have empty trace-level input/output (the\n // span data only reaches the root OBSERVATION), which breaks LLM-as-a-judge\n // evaluators mapped to Trace input/output and leaves the trace view without\n // the top-level request/response. Serialization failures (circular refs,\n // bigint) skip the attribute rather than failing the span export.\n const traceInput = serializeTraceIo(span.input);\n if (traceInput !== undefined) {\n attributes['langfuse.trace.input'] = traceInput;\n }\n const traceOutput = serializeTraceIo(span.output);\n if (traceOutput !== undefined) {\n attributes['langfuse.trace.output'] = traceOutput;\n }\n if (span.type === SpanType.AGENT_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.agentId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.agentName'] = span.entityName;\n }\n } else if (span.type === SpanType.WORKFLOW_RUN) {\n if (!attributes['langfuse.trace.name'] && (span.entityName || span.entityId)) {\n attributes['langfuse.trace.name'] = span.entityName ?? span.entityId;\n }\n if (span.entityId) {\n attributes['langfuse.trace.metadata.workflowId'] = span.entityId;\n }\n if (span.entityName) {\n attributes['langfuse.trace.metadata.workflowName'] = span.entityName;\n }\n }\n\n // Root-span metadata: forward the remaining mastra.metadata.* keys (runId,\n // resourceId, user-supplied keys) to langfuse.trace.metadata.* so they are\n // filterable trace metadata; Langfuse nests unmapped attributes under\n // metadata.attributes. Dedicated keys, explicit metadata.langfuse.* values,\n // and the identity keys above take precedence. Root only: Langfuse applies\n // langfuse.trace.* from any span, so a child span could overwrite the trace.\n for (const [key, value] of Object.entries(attributes)) {\n if (!key.startsWith(MASTRA_METADATA_PREFIX)) {\n continue;\n }\n const metadataKey = key.slice(MASTRA_METADATA_PREFIX.length);\n if (DEDICATED_METADATA_KEYS.has(metadataKey) || value === null || value === undefined) {\n continue;\n }\n const traceKey = `langfuse.trace.metadata.${metadataKey}`;\n if (attributes[traceKey] !== undefined) {\n continue;\n }\n const serialized = serializeTraceIo(value);\n if (serialized !== undefined) {\n attributes[traceKey] = serialized;\n }\n }\n }\n\n // Observation metadata: map semantic attributes to langfuse.observation.metadata.*\n // so they become top-level filterable keys on each observation in Langfuse.\n // @see https://langfuse.com/integrations/native/opentelemetry#how-metadata-mapping-works\n if (attributes['gen_ai.agent.id']) {\n attributes['langfuse.observation.metadata.agentId'] = attributes['gen_ai.agent.id'];\n }\n if (attributes['gen_ai.agent.name']) {\n attributes['langfuse.observation.metadata.agentName'] = attributes['gen_ai.agent.name'];\n }\n if (attributes['mastra.span.type']) {\n attributes['langfuse.observation.metadata.spanType'] = attributes['mastra.span.type'];\n }\n if (attributes['gen_ai.operation.name']) {\n attributes['langfuse.observation.metadata.operationName'] = attributes['gen_ai.operation.name'];\n }\n\n // Input/Output: mastra.*.input/output → langfuse.observation.input/output\n // For gen_ai spans, Langfuse reads gen_ai.input.messages natively.\n // For non-gen_ai spans, we map the first mastra.*.input/output we find.\n if (!attributes['gen_ai.input.messages'] && !attributes['gen_ai.tool.call.arguments']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.input')) {\n attributes['langfuse.observation.input'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n if (!attributes['gen_ai.output.messages'] && !attributes['gen_ai.tool.call.result']) {\n for (const key of Object.keys(attributes)) {\n if (key.startsWith('mastra.') && key.endsWith('.output')) {\n attributes['langfuse.observation.output'] = attributes[key];\n delete attributes[key];\n break;\n }\n }\n }\n}\n\n/**\n * Remove trailing \"/\" characters procedurally. Avoids polynomial\n * backtracking that a greedy regex like `/\\/+$/` can exhibit when the\n * input is attacker-controlled.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* \"/\" */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n","/**\n * Langfuse Tracing Options Helpers\n *\n * These helpers integrate with the `buildTracingOptions` pattern from\n * `@mastra/observability` to add Langfuse-specific tracing features.\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * const agent = new Agent({\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\n\nimport type { TracingOptionsUpdater } from '@mastra/observability';\n\n/**\n * Langfuse prompt input - accepts either a Langfuse SDK prompt object\n * or manual fields.\n */\nexport interface LangfusePromptInput {\n /** Prompt name */\n name?: string;\n /** Prompt version */\n version?: number;\n /** @deprecated Langfuse v5 only supports linking by name + version. This field is ignored. */\n id?: string;\n}\n\n/**\n * Adds Langfuse prompt metadata to the tracing options\n * to enable Langfuse Prompt Tracing.\n *\n * The metadata is added under `metadata.langfuse.prompt` and includes:\n * - `name` - Prompt name (required for Langfuse v5)\n * - `version` - Prompt version (required for Langfuse v5)\n *\n * All fields are deeply merged with any existing metadata.\n *\n * @param prompt - Prompt fields for linking (`name` and `version` required)\n * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`\n *\n * @example\n * ```typescript\n * import { buildTracingOptions } from '@mastra/observability';\n * import { withLangfusePrompt } from '@mastra/langfuse';\n *\n * // Link a generation to a Langfuse prompt by name and version\n * const tracingOptions = buildTracingOptions(\n * withLangfusePrompt({ name: 'customer-support', version: 1 }),\n * );\n *\n * // Or directly in agent config\n * const agent = new Agent({\n * name: 'support-agent',\n * instructions: 'You are a helpful assistant',\n * model: openai('gpt-4o'),\n * defaultGenerateOptions: {\n * tracingOptions: buildTracingOptions(\n * withLangfusePrompt({ name: 'my-prompt', version: 1 }),\n * ),\n * },\n * });\n * ```\n */\nexport function withLangfusePrompt(prompt: LangfusePromptInput): TracingOptionsUpdater {\n return opts => ({\n ...opts,\n metadata: {\n ...opts.metadata,\n langfuse: {\n ...(opts.metadata?.langfuse as Record<string, unknown>),\n prompt: {\n ...(prompt.name !== undefined && { name: prompt.name }),\n ...(prompt.version !== undefined && { version: prompt.version }),\n ...(prompt.id !== undefined && { id: prompt.id }),\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/langfuse",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.14-alpha.0",
|
|
4
4
|
"description": "Langfuse observability provider for Mastra - uses official Langfuse v5 SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@langfuse/client": "^5.9.1",
|
|
27
27
|
"@langfuse/otel": "^5.9.1",
|
|
28
|
-
"@mastra/
|
|
29
|
-
"@mastra/
|
|
28
|
+
"@mastra/observability": "1.19.0-alpha.0",
|
|
29
|
+
"@mastra/otel-exporter": "1.4.6-alpha.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@opentelemetry/api": "^1.9.1",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"tsdown": "0.22.9",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
40
|
"vitest": "4.1.11",
|
|
41
|
-
"@internal/types-builder": "0.0.116",
|
|
42
41
|
"@internal/lint": "0.0.141",
|
|
43
|
-
"@
|
|
42
|
+
"@internal/types-builder": "0.0.116",
|
|
43
|
+
"@mastra/core": "1.76.0-alpha.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@mastra/core": ">=1.16.0-0 <2.0.0-0",
|