@mastra/posthog 1.3.16 → 1.4.0-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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Feature detection for paired @mastra/core and @mastra/observability versions.
3
+ *
4
+ * Both are resolved synchronously so span classification is stable from the
5
+ * very first span, without depending on a pending dynamic import.
6
+ *
7
+ * `@mastra/observability` is a hard dependency, but older versions do not
8
+ * export `observabilityFeatures`. The namespace import reads it as `undefined`
9
+ * in that case instead of failing to link, which is the graceful fallback
10
+ * documented in `observability/mastra/src/features.ts`.
11
+ */
12
+ /**
13
+ * Returns true when both packages report the `model-inference-span` feature,
14
+ * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives which span
15
+ * is exported as `$ai_generation` (new: MODEL_INFERENCE; legacy:
16
+ * MODEL_GENERATION).
17
+ */
18
+ export declare function isModelInferenceEnabled(): boolean;
19
+ /**
20
+ * @internal Test-only override. Allows tests to simulate a paired older or
21
+ * newer `@mastra/observability` without juggling module mocks.
22
+ */
23
+ export declare function __setObservabilityFeaturesForTest(features: ReadonlySet<string> | undefined): void;
24
+ //# sourceMappingURL=features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,CAEjG"}
package/dist/index.cjs CHANGED
@@ -1,7 +1,55 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
2
24
  let _mastra_core_observability = require("@mastra/core/observability");
3
25
  let _mastra_observability = require("@mastra/observability");
26
+ _mastra_observability = __toESM(_mastra_observability, 1);
4
27
  let posthog_node = require("posthog-node");
28
+ let _mastra_core_features = require("@mastra/core/features");
29
+ //#region src/features.ts
30
+ /**
31
+ * Feature detection for paired @mastra/core and @mastra/observability versions.
32
+ *
33
+ * Both are resolved synchronously so span classification is stable from the
34
+ * very first span, without depending on a pending dynamic import.
35
+ *
36
+ * `@mastra/observability` is a hard dependency, but older versions do not
37
+ * export `observabilityFeatures`. The namespace import reads it as `undefined`
38
+ * in that case instead of failing to link, which is the graceful fallback
39
+ * documented in `observability/mastra/src/features.ts`.
40
+ */
41
+ const FEATURE = "model-inference-span";
42
+ let observabilityFeatures = _mastra_observability.observabilityFeatures;
43
+ /**
44
+ * Returns true when both packages report the `model-inference-span` feature,
45
+ * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives which span
46
+ * is exported as `$ai_generation` (new: MODEL_INFERENCE; legacy:
47
+ * MODEL_GENERATION).
48
+ */
49
+ function isModelInferenceEnabled() {
50
+ return observabilityFeatures?.has(FEATURE) === true && _mastra_core_features.coreFeatures.has(FEATURE);
51
+ }
52
+ //#endregion
5
53
  //#region src/tracing.ts
6
54
  /**
7
55
  * Formats UsageStats to PostHog's expected property format.
@@ -25,6 +73,8 @@ function formatUsageMetrics(usage) {
25
73
  return props;
26
74
  }
27
75
  const DISTINCT_ID = "distinctId";
76
+ /** Per-trace key prefix for the provider calls and tool results recorded under one MODEL_GENERATION. */
77
+ const CONVERSATION = "conversation:";
28
78
  var PosthogExporter = class PosthogExporter extends _mastra_observability.TrackingExporter {
29
79
  name = "posthog";
30
80
  #client;
@@ -67,12 +117,12 @@ var PosthogExporter = class PosthogExporter extends _mastra_observability.Tracki
67
117
  skipCachingEventSpans = true;
68
118
  async _buildEvent(args) {
69
119
  const { span, traceData } = args;
70
- const eventName = this.mapToPostHogEvent(span.type);
120
+ const isModelCall = this.isModelCall(span);
71
121
  const distinctId = this.getDistinctId(span, traceData);
72
- const properties = this.buildEventProperties(span, 0);
122
+ const properties = this.buildEventProperties(span, 0, false, isModelCall);
73
123
  this.#client?.captureAi(this.withGroups({
74
124
  distinctId,
75
- event: eventName,
125
+ event: isModelCall ? "$ai_generation" : "$ai_span",
76
126
  properties,
77
127
  timestamp: span.endTime ? new Date(span.endTime) : /* @__PURE__ */ new Date()
78
128
  }));
@@ -98,16 +148,17 @@ var PosthogExporter = class PosthogExporter extends _mastra_observability.Tracki
98
148
  input: cachedSpan.input
99
149
  } : span;
100
150
  const eventMessage = this.buildEventMessage({
101
- span: mergedSpan,
151
+ span: this.withCallInput(mergedSpan, traceData),
102
152
  traceData
103
153
  });
104
154
  this.#client?.captureAi(this.withGroups(eventMessage));
155
+ this.recordForConversation(span, traceData);
105
156
  }
106
157
  async _abortSpan(args) {
107
158
  const { span, reason, traceData } = args;
108
159
  span.errorInfo = reason;
109
160
  const eventMessage = this.buildEventMessage({
110
- span,
161
+ span: this.withCallInput(span, traceData),
111
162
  traceData
112
163
  });
113
164
  this.#client?.captureAi(this.withGroups(eventMessage));
@@ -219,22 +270,102 @@ var PosthogExporter = class PosthogExporter extends _mastra_observability.Tracki
219
270
  }
220
271
  buildChildEventMessage(args) {
221
272
  const { span, distinctId, endTime, traceData } = args;
222
- const eventName = this.mapToPostHogEvent(span.type);
273
+ const isModelCall = this.isModelCall(span);
223
274
  const latency = (endTime - span.startTime.getTime()) / 1e3;
224
275
  const parentIsRootSpan = this.isParentRootSpan(span, traceData);
276
+ const properties = this.buildEventProperties(span, latency, parentIsRootSpan, isModelCall);
225
277
  return {
226
278
  distinctId,
227
- event: eventName,
228
- properties: this.buildEventProperties(span, latency, parentIsRootSpan),
279
+ event: isModelCall ? "$ai_generation" : "$ai_span",
280
+ properties,
229
281
  timestamp: new Date(endTime)
230
282
  };
231
283
  }
232
284
  toDate(timestamp) {
233
285
  return timestamp instanceof Date ? timestamp : new Date(timestamp);
234
286
  }
235
- mapToPostHogEvent(spanType) {
236
- if (spanType == _mastra_core_observability.SpanType.MODEL_GENERATION) return "$ai_generation";
237
- return "$ai_span";
287
+ /**
288
+ * Whether this span is exported as `$ai_generation`. PostHog expects one per
289
+ * provider call and sums tokens and cost across them, so exactly one span
290
+ * per call may carry usage. With paired packages that emit MODEL_INFERENCE,
291
+ * that is the call and MODEL_GENERATION is the loop around it, exported as
292
+ * `$ai_span`. Older pairings only emit MODEL_GENERATION, which keeps the
293
+ * `$ai_generation` role.
294
+ */
295
+ isModelCall(span) {
296
+ return span.type === (isModelInferenceEnabled() ? _mastra_core_observability.SpanType.MODEL_INFERENCE : _mastra_core_observability.SpanType.MODEL_GENERATION);
297
+ }
298
+ /**
299
+ * Give a MODEL_INFERENCE exported as `$ai_generation` the conversation that
300
+ * call received. The span has no input of its own, so it is rebuilt from the
301
+ * generation's input plus, for each earlier call in the same generation, its
302
+ * output and the results of the tools it called (as Braintrust's Thread view
303
+ * does). Earlier calls and their tools end before this call does, so they are
304
+ * already recorded.
305
+ */
306
+ withCallInput(span, traceData) {
307
+ if (span.type !== _mastra_core_observability.SpanType.MODEL_INFERENCE || !this.isModelCall(span)) return span;
308
+ const generation = this.findGeneration(span, traceData);
309
+ if (!generation?.input) return span;
310
+ const messages = this.formatMessages(generation.input, "user");
311
+ const conversation = traceData.getExtraValue(CONVERSATION + generation.id);
312
+ for (const call of conversation?.calls ?? []) {
313
+ const output = call.output;
314
+ if (!output?.text && !output?.toolCalls?.length) continue;
315
+ messages.push(...this.formatMessages(output, "assistant"));
316
+ const results = (output.toolCalls ?? []).map((toolCall) => ({
317
+ toolCall,
318
+ toolSpan: conversation.toolResults.get(toolCall.toolCallId)
319
+ })).filter((result) => !!result.toolSpan).sort((a, b) => this.toDate(a.toolSpan.startTime).getTime() - this.toDate(b.toolSpan.startTime).getTime());
320
+ for (const { toolCall, toolSpan } of results) {
321
+ const isError = !!toolSpan.errorInfo || toolSpan.type === _mastra_core_observability.SpanType.PROVIDER_TOOL_CALL && toolSpan.attributes?.success === false;
322
+ messages.push({
323
+ role: "tool",
324
+ content: [{
325
+ type: "tool-result",
326
+ toolCallId: toolCall.toolCallId,
327
+ toolName: toolCall.toolName,
328
+ output: toolSpan.errorInfo ? toolSpan.errorInfo.message : toolSpan.output,
329
+ ...isError ? { isError: true } : {}
330
+ }]
331
+ });
332
+ }
333
+ }
334
+ return {
335
+ ...span,
336
+ input: messages
337
+ };
338
+ }
339
+ /** Record ended calls and tool results so later calls in the same generation can rebuild their input. */
340
+ recordForConversation(span, traceData) {
341
+ const isCall = span.type === _mastra_core_observability.SpanType.MODEL_INFERENCE && !span.errorInfo;
342
+ const isProviderToolWithResult = span.type === _mastra_core_observability.SpanType.PROVIDER_TOOL_CALL && typeof span.attributes?.success === "boolean";
343
+ const isTool = span.type === _mastra_core_observability.SpanType.TOOL_CALL || span.type === _mastra_core_observability.SpanType.MCP_TOOL_CALL || isProviderToolWithResult;
344
+ if (!isModelInferenceEnabled() || !isCall && !isTool) return;
345
+ const generation = this.findGeneration(span, traceData);
346
+ if (!generation) return;
347
+ const key = CONVERSATION + generation.id;
348
+ let conversation = traceData.getExtraValue(key);
349
+ if (!conversation) {
350
+ conversation = {
351
+ calls: [],
352
+ toolResults: /* @__PURE__ */ new Map()
353
+ };
354
+ traceData.setExtraValue(key, conversation);
355
+ }
356
+ if (isCall) conversation.calls.push(span);
357
+ else {
358
+ const toolCallId = this.resolveToolCallId(span);
359
+ if (toolCallId) conversation.toolResults.set(toolCallId, span);
360
+ }
361
+ }
362
+ findGeneration(span, traceData) {
363
+ let ancestor = span.parentSpanId ? traceData.getSpan({ spanId: span.parentSpanId }) : void 0;
364
+ while (ancestor && ancestor.type !== _mastra_core_observability.SpanType.MODEL_GENERATION) ancestor = ancestor.parentSpanId ? traceData.getSpan({ spanId: ancestor.parentSpanId }) : void 0;
365
+ return ancestor;
366
+ }
367
+ resolveToolCallId(span) {
368
+ return span.attributes?.toolCallId ?? span.metadata?.toolCallId ?? span.input?.toolCallId;
238
369
  }
239
370
  getDistinctId(span, traceData) {
240
371
  if (span.metadata?.userId) return String(span.metadata.userId);
@@ -253,7 +384,7 @@ var PosthogExporter = class PosthogExporter extends _mastra_observability.Tracki
253
384
  if (parentCache) return parentCache.isRootSpan;
254
385
  return false;
255
386
  }
256
- buildEventProperties(span, latency, parentIsRootSpan = false) {
387
+ buildEventProperties(span, latency, parentIsRootSpan = false, isModelCall = this.isModelCall(span)) {
257
388
  const baseProperties = {
258
389
  $ai_trace_id: span.traceId,
259
390
  $ai_latency: latency,
@@ -262,7 +393,7 @@ var PosthogExporter = class PosthogExporter extends _mastra_observability.Tracki
262
393
  if (span.parentSpanId) baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;
263
394
  if (span.metadata?.sessionId) baseProperties.$ai_session_id = span.metadata.sessionId;
264
395
  if (span.isRootSpan && span.tags?.length) for (const tag of span.tags) baseProperties[tag] = true;
265
- if (span.type === _mastra_core_observability.SpanType.MODEL_GENERATION) {
396
+ if (isModelCall) {
266
397
  baseProperties.$ai_generation_id = span.id;
267
398
  return {
268
399
  ...baseProperties,
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["TrackingExporter","#client","PostHog","SpanType"],"sources":["../src/tracing.ts"],"sourcesContent":["import type {\n AnyExportedSpan,\n ExportedFeedback,\n FeedbackEvent,\n ModelGenerationAttributes,\n SpanErrorInfo,\n UsageStats,\n} from '@mastra/core/observability';\nimport { SpanType } from '@mastra/core/observability';\nimport type { TraceData, TrackingExporterConfig } from '@mastra/observability';\nimport { TrackingExporter } from '@mastra/observability';\nimport { PostHog } from 'posthog-node';\nimport type { EventMessage } from 'posthog-node';\n\n/**\n * Token usage format compatible with PostHog.\n * @see https://posthog.com/docs/llm-analytics/generations#event-properties\n */\nexport interface PostHogUsageMetrics {\n $ai_input_tokens?: number;\n $ai_output_tokens?: number;\n $ai_cache_read_input_tokens?: number;\n $ai_cache_creation_input_tokens?: number;\n $ai_cache_creation_5m_input_tokens?: number;\n $ai_cache_creation_1h_input_tokens?: number;\n}\n\n/**\n * Formats UsageStats to PostHog's expected property format.\n *\n * Pass through gross input token counts with cache fields as subsets.\n * PostHog subtracts cache tokens when computing costs for non-Anthropic\n * providers and detects Anthropic-style exclusive reporting on its own.\n *\n * @param usage - The UsageStats from span attributes\n * @returns PostHog-formatted usage properties\n */\nexport function formatUsageMetrics(usage?: UsageStats): PostHogUsageMetrics {\n if (!usage) return {};\n\n const props: PostHogUsageMetrics = {};\n\n if (usage.inputTokens !== undefined) {\n props.$ai_input_tokens = usage.inputTokens;\n }\n\n if (usage.inputDetails?.cacheRead !== undefined) {\n props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;\n }\n\n if (usage.inputDetails?.cacheWrite !== undefined) {\n props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;\n }\n if (usage.inputDetails?.cacheWrite5m !== undefined) {\n props.$ai_cache_creation_5m_input_tokens = usage.inputDetails.cacheWrite5m;\n }\n if (usage.inputDetails?.cacheWrite1h !== undefined) {\n props.$ai_cache_creation_1h_input_tokens = usage.inputDetails.cacheWrite1h;\n }\n\n if (usage.outputTokens !== undefined) {\n props.$ai_output_tokens = usage.outputTokens;\n }\n\n return props;\n}\n\ninterface PostHogMessage {\n role: 'user' | 'assistant' | 'system' | 'tool';\n content: PostHogContent[];\n}\n\ninterface PostHogContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ninterface MastraMessage {\n role: string;\n content: string | MastraContent[];\n}\n\ninterface MastraContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ntype SpanData = string | MastraMessage[] | Record<string, unknown> | unknown;\n\nconst DISTINCT_ID = 'distinctId';\n\nexport interface PosthogExporterConfig extends TrackingExporterConfig {\n /** PostHog API key. Defaults to POSTHOG_API_KEY environment variable. */\n apiKey?: string;\n /** PostHog host URL. Defaults to POSTHOG_HOST environment variable or US region. */\n host?: string;\n flushAt?: number;\n flushInterval?: number;\n serverless?: boolean;\n defaultDistinctId?: string;\n enablePrivacyMode?: boolean;\n}\n\ntype PosthogRoot = unknown;\ntype PosthogSpan = AnyExportedSpan;\n// used as a placeholder for event data since we don't need to cache\n// event data for Posthog\ntype PosthogEvent = boolean;\ntype PosthogMetadata = unknown;\ntype PosthogTraceData = TraceData<PosthogRoot, PosthogSpan, PosthogEvent, PosthogMetadata>;\n\nexport class PosthogExporter extends TrackingExporter<\n PosthogRoot,\n PosthogSpan,\n PosthogEvent,\n PosthogMetadata,\n PosthogExporterConfig\n> {\n name = 'posthog';\n #client: PostHog | undefined;\n\n private static readonly SERVERLESS_FLUSH_AT = 10;\n private static readonly SERVERLESS_FLUSH_INTERVAL = 2000;\n private static readonly DEFAULT_FLUSH_AT = 20;\n private static readonly DEFAULT_FLUSH_INTERVAL = 10000;\n\n constructor(config: PosthogExporterConfig = {}) {\n // Resolve env vars BEFORE calling super (config is readonly in base class)\n const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;\n\n super({ ...config, apiKey });\n\n if (!apiKey) {\n this.setDisabled('Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.');\n return;\n }\n\n const clientConfig = this.buildClientConfig(this.config);\n this.#client = new PostHog(apiKey, clientConfig);\n const message =\n (config.serverless ?? false) ? 'PostHog exporter initialized in serverless mode' : 'PostHog exporter initialized';\n this.logger.debug(message, config);\n }\n\n private buildClientConfig(config: PosthogExporterConfig) {\n const isServerless = config.serverless ?? false;\n const flushAt =\n config.flushAt ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_AT : PosthogExporter.DEFAULT_FLUSH_AT);\n const flushInterval =\n config.flushInterval ??\n (isServerless ? PosthogExporter.SERVERLESS_FLUSH_INTERVAL : PosthogExporter.DEFAULT_FLUSH_INTERVAL);\n\n const host = config.host || process.env.POSTHOG_HOST || 'https://us.i.posthog.com';\n\n if (!config.host && !process.env.POSTHOG_HOST) {\n this.logger.info(\n 'No PostHog host specified, using US default (https://us.i.posthog.com). ' +\n 'For EU region, set `host: \"https://eu.i.posthog.com\"` in config or POSTHOG_HOST env var. ' +\n 'For self-hosted, provide your instance URL.',\n );\n }\n\n return {\n host,\n flushAt,\n flushInterval,\n privacyMode: config.enablePrivacyMode,\n };\n }\n\n protected override skipBuildRootTask = true;\n protected override async _buildRoot(_args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogRoot | undefined> {\n throw new Error('Method not implemented.');\n }\n\n protected override skipCachingEventSpans = true;\n protected override async _buildEvent(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogEvent> {\n const { span, traceData } = args;\n\n const eventName = this.mapToPostHogEvent(span.type);\n const distinctId = this.getDistinctId(span, traceData);\n const properties = this.buildEventProperties(span, 0);\n\n // captureAi() targets PostHog's dedicated AI endpoint (8 MiB per event, oversized events\n // dropped individually). Plain capture() uses /batch/, which returns 413 for large spans.\n this.#client?.captureAi(\n this.withGroups({\n distinctId,\n event: eventName,\n properties,\n timestamp: span.endTime ? new Date(span.endTime) : new Date(),\n }),\n );\n\n return true;\n }\n\n protected override async _buildSpan(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogSpan | undefined> {\n const { span, traceData } = args;\n if (!traceData.hasExtraValue(DISTINCT_ID)) {\n const userId = span.metadata?.userId;\n if (userId) {\n traceData.setExtraValue(DISTINCT_ID, String(userId));\n }\n }\n\n return span;\n }\n\n protected override skipSpanUpdateEvents = true;\n protected override _updateSpan(_args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n protected override async _finishSpan(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n const { span, traceData } = args;\n\n // Merge input from cached span (SPAN_STARTED) if not present on end span\n // This handles the case where input is only sent at start\n const cachedSpan = traceData.getSpan({ spanId: span.id });\n const mergedSpan = !span.input && cachedSpan?.input ? { ...span, input: cachedSpan.input } : span;\n\n const eventMessage = this.buildEventMessage({ span: mergedSpan, traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n protected override async _abortSpan(args: {\n span: PosthogSpan;\n reason: SpanErrorInfo;\n traceData: PosthogTraceData;\n }): Promise<void> {\n const { span, reason, traceData } = args;\n\n // update span with the abort reason\n span.errorInfo = reason;\n\n const eventMessage = this.buildEventMessage({ span, traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n /**\n * Forward feedback recorded via `addFeedback()` to PostHog as a native\n * `$ai_feedback` event, shown as \"User feedback\" on the linked trace in\n * PostHog's AI observability UI.\n */\n async onFeedbackEvent(event: FeedbackEvent): Promise<void> {\n if (!this.#client) return;\n\n const { feedback } = event;\n if (!feedback.traceId) {\n this.logger.debug('PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id', {\n feedbackId: feedback.feedbackId,\n });\n return;\n }\n\n const properties: Record<string, any> = {\n // Custom metadata goes first so it cannot overwrite the natively mapped fields below\n ...this.extractCustomMetadata(feedback.metadata),\n $ai_trace_id: feedback.traceId,\n // PostHog's trace UI only displays feedback events that carry $ai_feedback_text\n $ai_feedback_text: feedback.comment ?? String(feedback.value),\n feedback_id: feedback.feedbackId,\n feedback_type: feedback.feedbackType,\n feedback_value: feedback.value,\n };\n\n const feedbackSource = feedback.feedbackSource ?? feedback.source;\n if (feedbackSource) properties.feedback_source = feedbackSource;\n if (feedback.spanId) properties.span_id = feedback.spanId;\n if (feedback.sourceId) properties.source_id = feedback.sourceId;\n if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;\n\n try {\n this.#client.captureAi(\n this.withGroups({\n distinctId: this.getFeedbackDistinctId(feedback),\n event: '$ai_feedback',\n properties,\n timestamp: new Date(feedback.timestamp),\n }),\n );\n } catch (err) {\n this.logger.error('PostHog exporter: failed to submit feedback', {\n error: err,\n traceId: feedback.traceId,\n feedbackId: feedback.feedbackId,\n });\n }\n }\n\n private getFeedbackDistinctId(feedback: ExportedFeedback): string {\n const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;\n if (userId) {\n return String(userId);\n }\n return this.config.defaultDistinctId ?? 'anonymous';\n }\n\n /**\n * PostHog group analytics are keyed off the top-level `groups` field on the\n * capture call. The Node SDK derives the event's `$groups` from that field and\n * overwrites any property-level `$groups`, so group metadata carried in\n * properties is dropped unless it is mirrored here.\n */\n private withGroups(message: EventMessage): EventMessage {\n const groups = message.properties?.$groups;\n if (groups && typeof groups === 'object' && !Array.isArray(groups)) {\n return { ...message, groups };\n }\n return message;\n }\n\n private buildEventMessage(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): EventMessage {\n const { span, traceData } = args;\n\n const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();\n\n const distinctId = this.getDistinctId(span, traceData);\n\n if (span.isRootSpan) {\n return this.buildRootEventMessage({ span, distinctId, endTime });\n } else {\n return this.buildChildEventMessage({ span, distinctId, endTime, traceData });\n }\n }\n\n /**\n * Capture an explicit $ai_trace event for root spans.\n * This gives us control over trace-level metadata like name and tags,\n * rather than relying on PostHog's pseudo-trace auto-creation.\n */\n private buildRootEventMessage(args: { span: AnyExportedSpan; distinctId: string; endTime: number }): EventMessage {\n const { span, distinctId, endTime } = args;\n\n // Note: We don't set $ai_latency on $ai_trace events because PostHog\n // aggregates latency from child events. Setting it here causes double-counting.\n const traceProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_span_name: span.name,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.metadata?.sessionId) {\n traceProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n if (span.input) {\n traceProperties.$ai_input_state = span.input;\n }\n\n if (span.output) {\n traceProperties.$ai_output_state = span.output;\n }\n\n if (span.errorInfo) {\n traceProperties.$ai_error = {\n message: span.errorInfo.message,\n ...(span.errorInfo.id && { id: span.errorInfo.id }),\n ...(span.errorInfo.category && { category: span.errorInfo.category }),\n };\n }\n\n // Add tags as custom properties (PostHog doesn't have native tag support on traces)\n if (span.tags?.length) {\n for (const tag of span.tags) {\n traceProperties[tag] = true;\n }\n }\n\n // Add custom metadata (excluding userId and sessionId which are handled separately)\n const { userId, sessionId, ...customMetadata } = span.metadata ?? {};\n Object.assign(traceProperties, customMetadata);\n\n return {\n distinctId,\n event: '$ai_trace',\n properties: traceProperties,\n timestamp: new Date(endTime),\n };\n }\n\n private buildChildEventMessage(args: {\n span: AnyExportedSpan;\n distinctId: string;\n endTime: number;\n traceData: PosthogTraceData;\n }): EventMessage {\n const { span, distinctId, endTime, traceData } = args;\n\n const eventName = this.mapToPostHogEvent(span.type);\n const startTime = span.startTime.getTime();\n const latency = (endTime - startTime) / 1000;\n\n // Check if parent is the root span - if so, use traceId as parent_id\n // since we don't create an $ai_span for root spans\n const parentIsRootSpan = this.isParentRootSpan(span, traceData);\n const properties = this.buildEventProperties(span, latency, parentIsRootSpan);\n\n return {\n distinctId,\n event: eventName,\n properties,\n timestamp: new Date(endTime),\n };\n }\n\n private toDate(timestamp: Date | number): Date {\n return timestamp instanceof Date ? timestamp : new Date(timestamp);\n }\n\n private mapToPostHogEvent(spanType: SpanType): string {\n if (spanType == SpanType.MODEL_GENERATION) {\n return '$ai_generation';\n }\n return '$ai_span';\n }\n\n private getDistinctId(span: AnyExportedSpan, traceData?: PosthogTraceData): string {\n if (span.metadata?.userId) {\n return String(span.metadata.userId);\n }\n\n if (traceData?.hasExtraValue(DISTINCT_ID)) {\n return String(traceData.getExtraValue(DISTINCT_ID));\n }\n\n if (this.config.defaultDistinctId) {\n return this.config.defaultDistinctId;\n }\n\n return 'anonymous';\n }\n\n /**\n * Check if the parent of this span is the root span.\n * We need this because we don't create $ai_span for root spans,\n * so children of root spans should use $ai_trace_id as their $ai_parent_id.\n */\n private isParentRootSpan(span: AnyExportedSpan, traceData: PosthogTraceData): boolean {\n if (!span.parentSpanId) {\n return false;\n }\n\n // Look up the parent span in our cache to check if it's a root span\n const parentCache = traceData.getSpan({ spanId: span.parentSpanId });\n if (parentCache) {\n return parentCache.isRootSpan;\n }\n\n // Parent not found in cache - shouldn't happen normally, but default to false\n return false;\n }\n\n private buildEventProperties(\n span: AnyExportedSpan,\n latency: number,\n parentIsRootSpan: boolean = false,\n ): Record<string, any> {\n const baseProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_latency: latency,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.parentSpanId) {\n // If parent is the root span, use trace_id as parent_id since we don't\n // create an $ai_span for root spans (only $ai_trace)\n baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;\n }\n\n if (span.metadata?.sessionId) {\n baseProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n // Include tags for root spans (tags are only set on root spans by design)\n // PostHog doesn't allow setting tags directly, so we iterate through each tag\n // and set it as a property with value true\n if (span.isRootSpan && span.tags?.length) {\n for (const tag of span.tags) {\n baseProperties[tag] = true;\n }\n }\n\n if (span.type === SpanType.MODEL_GENERATION) {\n baseProperties.$ai_generation_id = span.id;\n return { ...baseProperties, ...this.buildGenerationProperties(span) };\n } else {\n baseProperties.$ai_span_id = span.id;\n baseProperties.$ai_span_name = span.name;\n return { ...baseProperties, ...this.buildSpanProperties(span) };\n }\n }\n\n private extractErrorProperties(errorInfo?: SpanErrorInfo): Record<string, any> {\n if (!errorInfo) {\n return {};\n }\n\n const props: Record<string, string> = {\n error_message: errorInfo.message,\n };\n\n if (errorInfo.id) {\n props.error_id = errorInfo.id;\n }\n\n if (errorInfo.category) {\n props.error_category = errorInfo.category;\n }\n\n return props;\n }\n\n private extractCustomMetadata(metadata?: Record<string, unknown>): Record<string, any> {\n const { userId, sessionId, ...customMetadata } = metadata ?? {};\n return customMetadata;\n }\n\n private buildGenerationProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n const attrs = (span.attributes ?? {}) as ModelGenerationAttributes;\n\n props.$ai_model = attrs.model || 'unknown-model';\n props.$ai_provider = attrs.provider || 'unknown-provider';\n\n if (span.input) props.$ai_input = this.formatMessages(span.input, 'user');\n if (span.output) props.$ai_output_choices = this.formatMessages(span.output, 'assistant');\n\n // Extract usage properties using the shared utility\n Object.assign(props, formatUsageMetrics(attrs.usage));\n\n if (attrs.parameters) {\n if (attrs.parameters.temperature !== undefined) props.$ai_temperature = attrs.parameters.temperature;\n if (attrs.parameters.maxOutputTokens !== undefined) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;\n }\n if (attrs.streaming !== undefined) props.$ai_stream = attrs.streaming;\n if (attrs.tools?.length) {\n // OpenAI-style shape — the format PostHog's own AI SDKs send and its\n // trace view renders. Provider-defined tools pass through as-is.\n props.$ai_tools = attrs.tools.map(tool =>\n tool.type === 'function'\n ? {\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n }\n : tool,\n );\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private buildSpanProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n\n if (span.input) props.$ai_input_state = span.input;\n if (span.output) props.$ai_output_state = span.output;\n\n if (span.type === SpanType.MODEL_CHUNK) {\n const attrs = span.attributes as any;\n if (attrs?.chunkType) props.chunk_type = attrs.chunkType;\n if (attrs?.sequenceNumber !== undefined) props.chunk_sequence_number = attrs.sequenceNumber;\n }\n\n if (span.attributes) {\n Object.assign(props, span.attributes);\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private formatMessages(data: SpanData, defaultRole: 'user' | 'assistant' = 'user'): PostHogMessage[] {\n // Unwrap {messages: [...]} wrapper produced by generation span inputs\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'messages' in data) {\n const wrapped = (data as Record<string, unknown>).messages;\n if (this.isMessageArray(wrapped)) {\n return wrapped.map(msg => this.normalizeMessage(msg));\n }\n }\n\n if (this.isMessageArray(data)) {\n return data.map(msg => this.normalizeMessage(msg));\n }\n\n if (typeof data === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text: data }] }];\n }\n\n if (this.isSpanOutputWithToolCalls(data)) {\n const content: PostHogContent[] = [];\n if (data.text) {\n content.push({ type: 'text', text: data.text });\n }\n for (const tc of data.toolCalls) {\n content.push({\n type: 'tool-call',\n id: tc.toolCallId,\n function: { name: tc.toolName, arguments: tc.args },\n });\n }\n return [{ role: 'assistant', content }];\n }\n\n // Extract text from output objects (e.g. generation outputs with text but no tool calls)\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'text' in data) {\n const text = (data as Record<string, unknown>).text;\n if (typeof text === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text }] }];\n }\n }\n\n return [{ role: defaultRole, content: [{ type: 'text', text: this.safeStringify(data) }] }];\n }\n\n private isSpanOutputWithToolCalls(\n data: unknown,\n ): data is { text?: string; toolCalls: Array<{ toolCallId: string; toolName: string; args: unknown }> } {\n if (typeof data !== 'object' || data === null || !('toolCalls' in data)) return false;\n const { toolCalls } = data as Record<string, unknown>;\n return Array.isArray(toolCalls) && toolCalls.length > 0;\n }\n\n private isMessageArray(data: unknown): data is MastraMessage[] {\n if (!Array.isArray(data)) {\n return false;\n }\n\n return data.every(item => typeof item === 'object' && item !== null && 'role' in item && 'content' in item);\n }\n\n private normalizeMessage(msg: MastraMessage): PostHogMessage {\n if (typeof msg.content === 'string') {\n return {\n role: msg.role as PostHogMessage['role'],\n content: [{ type: 'text', text: msg.content }],\n };\n }\n\n return {\n role: msg.role as PostHogMessage['role'],\n content: msg.content as PostHogContent[],\n };\n }\n\n private safeStringify(data: unknown): string {\n try {\n return JSON.stringify(data);\n } catch {\n if (typeof data === 'object' && data !== null) {\n return `[Non-serializable ${data.constructor?.name || 'Object'}]`;\n }\n return String(data);\n }\n }\n\n /**\n * Force flush any buffered data to PostHog without shutting down.\n */\n protected override async _flush(): Promise<void> {\n if (this.#client) {\n await this.#client.flush();\n }\n }\n\n override async _postShutdown(): Promise<void> {\n if (this.#client) {\n await this.#client.shutdown();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["observability","coreFeatures","TrackingExporter","#client","PostHog","SpanType"],"sources":["../src/features.ts","../src/tracing.ts"],"sourcesContent":["/**\n * Feature detection for paired @mastra/core and @mastra/observability versions.\n *\n * Both are resolved synchronously so span classification is stable from the\n * very first span, without depending on a pending dynamic import.\n *\n * `@mastra/observability` is a hard dependency, but older versions do not\n * export `observabilityFeatures`. The namespace import reads it as `undefined`\n * in that case instead of failing to link, which is the graceful fallback\n * documented in `observability/mastra/src/features.ts`.\n */\n\nimport { coreFeatures } from '@mastra/core/features';\nimport * as observability from '@mastra/observability';\n\nconst FEATURE = 'model-inference-span';\n\nlet observabilityFeatures: ReadonlySet<string> | undefined = (\n observability as { observabilityFeatures?: ReadonlySet<string> }\n).observabilityFeatures;\n\n/**\n * Returns true when both packages report the `model-inference-span` feature,\n * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives which span\n * is exported as `$ai_generation` (new: MODEL_INFERENCE; legacy:\n * MODEL_GENERATION).\n */\nexport function isModelInferenceEnabled(): boolean {\n return observabilityFeatures?.has(FEATURE) === true && coreFeatures.has(FEATURE);\n}\n\n/**\n * @internal Test-only override. Allows tests to simulate a paired older or\n * newer `@mastra/observability` without juggling module mocks.\n */\nexport function __setObservabilityFeaturesForTest(features: ReadonlySet<string> | undefined): void {\n observabilityFeatures = features;\n}\n","import type {\n AnyExportedSpan,\n ExportedFeedback,\n FeedbackEvent,\n ModelGenerationAttributes,\n SpanErrorInfo,\n UsageStats,\n} from '@mastra/core/observability';\nimport { SpanType } from '@mastra/core/observability';\nimport type { TraceData, TrackingExporterConfig } from '@mastra/observability';\nimport { TrackingExporter } from '@mastra/observability';\nimport { PostHog } from 'posthog-node';\nimport type { EventMessage } from 'posthog-node';\nimport { isModelInferenceEnabled } from './features';\n\n/**\n * Token usage format compatible with PostHog.\n * @see https://posthog.com/docs/llm-analytics/generations#event-properties\n */\nexport interface PostHogUsageMetrics {\n $ai_input_tokens?: number;\n $ai_output_tokens?: number;\n $ai_cache_read_input_tokens?: number;\n $ai_cache_creation_input_tokens?: number;\n $ai_cache_creation_5m_input_tokens?: number;\n $ai_cache_creation_1h_input_tokens?: number;\n}\n\n/**\n * Formats UsageStats to PostHog's expected property format.\n *\n * Pass through gross input token counts with cache fields as subsets.\n * PostHog subtracts cache tokens when computing costs for non-Anthropic\n * providers and detects Anthropic-style exclusive reporting on its own.\n *\n * @param usage - The UsageStats from span attributes\n * @returns PostHog-formatted usage properties\n */\nexport function formatUsageMetrics(usage?: UsageStats): PostHogUsageMetrics {\n if (!usage) return {};\n\n const props: PostHogUsageMetrics = {};\n\n if (usage.inputTokens !== undefined) {\n props.$ai_input_tokens = usage.inputTokens;\n }\n\n if (usage.inputDetails?.cacheRead !== undefined) {\n props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;\n }\n\n if (usage.inputDetails?.cacheWrite !== undefined) {\n props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;\n }\n if (usage.inputDetails?.cacheWrite5m !== undefined) {\n props.$ai_cache_creation_5m_input_tokens = usage.inputDetails.cacheWrite5m;\n }\n if (usage.inputDetails?.cacheWrite1h !== undefined) {\n props.$ai_cache_creation_1h_input_tokens = usage.inputDetails.cacheWrite1h;\n }\n\n if (usage.outputTokens !== undefined) {\n props.$ai_output_tokens = usage.outputTokens;\n }\n\n return props;\n}\n\ninterface PostHogMessage {\n role: 'user' | 'assistant' | 'system' | 'tool';\n content: PostHogContent[];\n}\n\ninterface PostHogContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ninterface MastraMessage {\n role: string;\n content: string | MastraContent[];\n}\n\ninterface MastraContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ntype SpanData = string | MastraMessage[] | Record<string, unknown> | unknown;\n\nconst DISTINCT_ID = 'distinctId';\n/** Per-trace key prefix for the provider calls and tool results recorded under one MODEL_GENERATION. */\nconst CONVERSATION = 'conversation:';\n\n/** Ended spans under one MODEL_GENERATION, used to rebuild each provider call's `$ai_input`. */\ninterface Conversation {\n /** Ended MODEL_INFERENCE spans, in the order they ended. */\n calls: AnyExportedSpan[];\n /** Ended TOOL_CALL / MCP_TOOL_CALL spans and PROVIDER_TOOL_CALL spans with a result, keyed by toolCallId. */\n toolResults: Map<string, AnyExportedSpan>;\n}\n\nexport interface PosthogExporterConfig extends TrackingExporterConfig {\n /** PostHog API key. Defaults to POSTHOG_API_KEY environment variable. */\n apiKey?: string;\n /** PostHog host URL. Defaults to POSTHOG_HOST environment variable or US region. */\n host?: string;\n flushAt?: number;\n flushInterval?: number;\n serverless?: boolean;\n defaultDistinctId?: string;\n enablePrivacyMode?: boolean;\n}\n\ntype PosthogRoot = unknown;\ntype PosthogSpan = AnyExportedSpan;\n// used as a placeholder for event data since we don't need to cache\n// event data for Posthog\ntype PosthogEvent = boolean;\ntype PosthogMetadata = unknown;\ntype PosthogTraceData = TraceData<PosthogRoot, PosthogSpan, PosthogEvent, PosthogMetadata>;\n\nexport class PosthogExporter extends TrackingExporter<\n PosthogRoot,\n PosthogSpan,\n PosthogEvent,\n PosthogMetadata,\n PosthogExporterConfig\n> {\n name = 'posthog';\n #client: PostHog | undefined;\n\n private static readonly SERVERLESS_FLUSH_AT = 10;\n private static readonly SERVERLESS_FLUSH_INTERVAL = 2000;\n private static readonly DEFAULT_FLUSH_AT = 20;\n private static readonly DEFAULT_FLUSH_INTERVAL = 10000;\n\n constructor(config: PosthogExporterConfig = {}) {\n // Resolve env vars BEFORE calling super (config is readonly in base class)\n const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;\n\n super({ ...config, apiKey });\n\n if (!apiKey) {\n this.setDisabled('Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.');\n return;\n }\n\n const clientConfig = this.buildClientConfig(this.config);\n this.#client = new PostHog(apiKey, clientConfig);\n const message =\n (config.serverless ?? false) ? 'PostHog exporter initialized in serverless mode' : 'PostHog exporter initialized';\n this.logger.debug(message, config);\n }\n\n private buildClientConfig(config: PosthogExporterConfig) {\n const isServerless = config.serverless ?? false;\n const flushAt =\n config.flushAt ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_AT : PosthogExporter.DEFAULT_FLUSH_AT);\n const flushInterval =\n config.flushInterval ??\n (isServerless ? PosthogExporter.SERVERLESS_FLUSH_INTERVAL : PosthogExporter.DEFAULT_FLUSH_INTERVAL);\n\n const host = config.host || process.env.POSTHOG_HOST || 'https://us.i.posthog.com';\n\n if (!config.host && !process.env.POSTHOG_HOST) {\n this.logger.info(\n 'No PostHog host specified, using US default (https://us.i.posthog.com). ' +\n 'For EU region, set `host: \"https://eu.i.posthog.com\"` in config or POSTHOG_HOST env var. ' +\n 'For self-hosted, provide your instance URL.',\n );\n }\n\n return {\n host,\n flushAt,\n flushInterval,\n privacyMode: config.enablePrivacyMode,\n };\n }\n\n protected override skipBuildRootTask = true;\n protected override async _buildRoot(_args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogRoot | undefined> {\n throw new Error('Method not implemented.');\n }\n\n protected override skipCachingEventSpans = true;\n protected override async _buildEvent(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogEvent> {\n const { span, traceData } = args;\n\n const isModelCall = this.isModelCall(span);\n const distinctId = this.getDistinctId(span, traceData);\n const properties = this.buildEventProperties(span, 0, false, isModelCall);\n\n // captureAi() targets PostHog's dedicated AI endpoint (8 MiB per event, oversized events\n // dropped individually). Plain capture() uses /batch/, which returns 413 for large spans.\n this.#client?.captureAi(\n this.withGroups({\n distinctId,\n event: isModelCall ? '$ai_generation' : '$ai_span',\n properties,\n timestamp: span.endTime ? new Date(span.endTime) : new Date(),\n }),\n );\n\n return true;\n }\n\n protected override async _buildSpan(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogSpan | undefined> {\n const { span, traceData } = args;\n if (!traceData.hasExtraValue(DISTINCT_ID)) {\n const userId = span.metadata?.userId;\n if (userId) {\n traceData.setExtraValue(DISTINCT_ID, String(userId));\n }\n }\n\n return span;\n }\n\n protected override skipSpanUpdateEvents = true;\n protected override _updateSpan(_args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n protected override async _finishSpan(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n const { span, traceData } = args;\n\n // Merge input from cached span (SPAN_STARTED) if not present on end span\n // This handles the case where input is only sent at start\n const cachedSpan = traceData.getSpan({ spanId: span.id });\n const mergedSpan = !span.input && cachedSpan?.input ? { ...span, input: cachedSpan.input } : span;\n\n const eventMessage = this.buildEventMessage({ span: this.withCallInput(mergedSpan, traceData), traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n\n this.recordForConversation(span, traceData);\n }\n\n protected override async _abortSpan(args: {\n span: PosthogSpan;\n reason: SpanErrorInfo;\n traceData: PosthogTraceData;\n }): Promise<void> {\n const { span, reason, traceData } = args;\n\n // update span with the abort reason\n span.errorInfo = reason;\n\n const eventMessage = this.buildEventMessage({ span: this.withCallInput(span, traceData), traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n /**\n * Forward feedback recorded via `addFeedback()` to PostHog as a native\n * `$ai_feedback` event, shown as \"User feedback\" on the linked trace in\n * PostHog's AI observability UI.\n */\n async onFeedbackEvent(event: FeedbackEvent): Promise<void> {\n if (!this.#client) return;\n\n const { feedback } = event;\n if (!feedback.traceId) {\n this.logger.debug('PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id', {\n feedbackId: feedback.feedbackId,\n });\n return;\n }\n\n const properties: Record<string, any> = {\n // Custom metadata goes first so it cannot overwrite the natively mapped fields below\n ...this.extractCustomMetadata(feedback.metadata),\n $ai_trace_id: feedback.traceId,\n // PostHog's trace UI only displays feedback events that carry $ai_feedback_text\n $ai_feedback_text: feedback.comment ?? String(feedback.value),\n feedback_id: feedback.feedbackId,\n feedback_type: feedback.feedbackType,\n feedback_value: feedback.value,\n };\n\n const feedbackSource = feedback.feedbackSource ?? feedback.source;\n if (feedbackSource) properties.feedback_source = feedbackSource;\n if (feedback.spanId) properties.span_id = feedback.spanId;\n if (feedback.sourceId) properties.source_id = feedback.sourceId;\n if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;\n\n try {\n this.#client.captureAi(\n this.withGroups({\n distinctId: this.getFeedbackDistinctId(feedback),\n event: '$ai_feedback',\n properties,\n timestamp: new Date(feedback.timestamp),\n }),\n );\n } catch (err) {\n this.logger.error('PostHog exporter: failed to submit feedback', {\n error: err,\n traceId: feedback.traceId,\n feedbackId: feedback.feedbackId,\n });\n }\n }\n\n private getFeedbackDistinctId(feedback: ExportedFeedback): string {\n const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;\n if (userId) {\n return String(userId);\n }\n return this.config.defaultDistinctId ?? 'anonymous';\n }\n\n /**\n * PostHog group analytics are keyed off the top-level `groups` field on the\n * capture call. The Node SDK derives the event's `$groups` from that field and\n * overwrites any property-level `$groups`, so group metadata carried in\n * properties is dropped unless it is mirrored here.\n */\n private withGroups(message: EventMessage): EventMessage {\n const groups = message.properties?.$groups;\n if (groups && typeof groups === 'object' && !Array.isArray(groups)) {\n return { ...message, groups };\n }\n return message;\n }\n\n private buildEventMessage(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): EventMessage {\n const { span, traceData } = args;\n\n const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();\n\n const distinctId = this.getDistinctId(span, traceData);\n\n if (span.isRootSpan) {\n return this.buildRootEventMessage({ span, distinctId, endTime });\n } else {\n return this.buildChildEventMessage({ span, distinctId, endTime, traceData });\n }\n }\n\n /**\n * Capture an explicit $ai_trace event for root spans.\n * This gives us control over trace-level metadata like name and tags,\n * rather than relying on PostHog's pseudo-trace auto-creation.\n */\n private buildRootEventMessage(args: { span: AnyExportedSpan; distinctId: string; endTime: number }): EventMessage {\n const { span, distinctId, endTime } = args;\n\n // Note: We don't set $ai_latency on $ai_trace events because PostHog\n // aggregates latency from child events. Setting it here causes double-counting.\n const traceProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_span_name: span.name,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.metadata?.sessionId) {\n traceProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n if (span.input) {\n traceProperties.$ai_input_state = span.input;\n }\n\n if (span.output) {\n traceProperties.$ai_output_state = span.output;\n }\n\n if (span.errorInfo) {\n traceProperties.$ai_error = {\n message: span.errorInfo.message,\n ...(span.errorInfo.id && { id: span.errorInfo.id }),\n ...(span.errorInfo.category && { category: span.errorInfo.category }),\n };\n }\n\n // Add tags as custom properties (PostHog doesn't have native tag support on traces)\n if (span.tags?.length) {\n for (const tag of span.tags) {\n traceProperties[tag] = true;\n }\n }\n\n // Add custom metadata (excluding userId and sessionId which are handled separately)\n const { userId, sessionId, ...customMetadata } = span.metadata ?? {};\n Object.assign(traceProperties, customMetadata);\n\n return {\n distinctId,\n event: '$ai_trace',\n properties: traceProperties,\n timestamp: new Date(endTime),\n };\n }\n\n private buildChildEventMessage(args: {\n span: AnyExportedSpan;\n distinctId: string;\n endTime: number;\n traceData: PosthogTraceData;\n }): EventMessage {\n const { span, distinctId, endTime, traceData } = args;\n\n const isModelCall = this.isModelCall(span);\n const startTime = span.startTime.getTime();\n const latency = (endTime - startTime) / 1000;\n\n // Check if parent is the root span - if so, use traceId as parent_id\n // since we don't create an $ai_span for root spans\n const parentIsRootSpan = this.isParentRootSpan(span, traceData);\n const properties = this.buildEventProperties(span, latency, parentIsRootSpan, isModelCall);\n\n return {\n distinctId,\n event: isModelCall ? '$ai_generation' : '$ai_span',\n properties,\n timestamp: new Date(endTime),\n };\n }\n\n private toDate(timestamp: Date | number): Date {\n return timestamp instanceof Date ? timestamp : new Date(timestamp);\n }\n\n /**\n * Whether this span is exported as `$ai_generation`. PostHog expects one per\n * provider call and sums tokens and cost across them, so exactly one span\n * per call may carry usage. With paired packages that emit MODEL_INFERENCE,\n * that is the call and MODEL_GENERATION is the loop around it, exported as\n * `$ai_span`. Older pairings only emit MODEL_GENERATION, which keeps the\n * `$ai_generation` role.\n */\n private isModelCall(span: AnyExportedSpan): boolean {\n return span.type === (isModelInferenceEnabled() ? SpanType.MODEL_INFERENCE : SpanType.MODEL_GENERATION);\n }\n\n /**\n * Give a MODEL_INFERENCE exported as `$ai_generation` the conversation that\n * call received. The span has no input of its own, so it is rebuilt from the\n * generation's input plus, for each earlier call in the same generation, its\n * output and the results of the tools it called (as Braintrust's Thread view\n * does). Earlier calls and their tools end before this call does, so they are\n * already recorded.\n */\n private withCallInput(span: AnyExportedSpan, traceData: PosthogTraceData): AnyExportedSpan {\n if (span.type !== SpanType.MODEL_INFERENCE || !this.isModelCall(span)) return span;\n\n const generation = this.findGeneration(span, traceData);\n if (!generation?.input) return span;\n\n const messages = this.formatMessages(generation.input, 'user');\n const conversation = traceData.getExtraValue(CONVERSATION + generation.id) as Conversation | undefined;\n for (const call of conversation?.calls ?? []) {\n const output = call.output as\n | { text?: string; toolCalls?: Array<{ toolCallId: string; toolName: string }> }\n | undefined;\n if (!output?.text && !output?.toolCalls?.length) continue;\n messages.push(...this.formatMessages(output, 'assistant'));\n\n // Parallel tool calls are ordered by when each tool started.\n const results = (output.toolCalls ?? [])\n .map(toolCall => ({ toolCall, toolSpan: conversation!.toolResults.get(toolCall.toolCallId) }))\n .filter(\n (result): result is { toolCall: typeof result.toolCall; toolSpan: AnyExportedSpan } => !!result.toolSpan,\n )\n .sort((a, b) => this.toDate(a.toolSpan.startTime).getTime() - this.toDate(b.toolSpan.startTime).getTime());\n for (const { toolCall, toolSpan } of results) {\n // Provider-executed tools report failure through `success`, not `errorInfo`.\n const isError =\n !!toolSpan.errorInfo ||\n (toolSpan.type === SpanType.PROVIDER_TOOL_CALL &&\n (toolSpan.attributes as { success?: boolean } | undefined)?.success === false);\n // Same `tool-result` part shape PostHog's own AI SDK integrations send.\n messages.push({\n role: 'tool',\n content: [\n {\n type: 'tool-result',\n toolCallId: toolCall.toolCallId,\n toolName: toolCall.toolName,\n output: toolSpan.errorInfo ? toolSpan.errorInfo.message : toolSpan.output,\n ...(isError ? { isError: true } : {}),\n },\n ],\n });\n }\n }\n\n return { ...span, input: messages };\n }\n\n /** Record ended calls and tool results so later calls in the same generation can rebuild their input. */\n private recordForConversation(span: AnyExportedSpan, traceData: PosthogTraceData): void {\n const isCall = span.type === SpanType.MODEL_INFERENCE && !span.errorInfo;\n // A PROVIDER_TOOL_CALL span ended without a result (e.g. the run stopped first)\n // has no `success`, and recording it would add an empty result.\n const isProviderToolWithResult =\n span.type === SpanType.PROVIDER_TOOL_CALL &&\n typeof (span.attributes as { success?: unknown } | undefined)?.success === 'boolean';\n const isTool = span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || isProviderToolWithResult;\n if (!isModelInferenceEnabled() || (!isCall && !isTool)) return;\n\n const generation = this.findGeneration(span, traceData);\n if (!generation) return;\n\n const key = CONVERSATION + generation.id;\n let conversation = traceData.getExtraValue(key) as Conversation | undefined;\n if (!conversation) {\n conversation = { calls: [], toolResults: new Map() };\n traceData.setExtraValue(key, conversation);\n }\n if (isCall) {\n conversation.calls.push(span);\n } else {\n const toolCallId = this.resolveToolCallId(span);\n if (toolCallId) conversation.toolResults.set(toolCallId, span);\n }\n }\n\n private findGeneration(span: AnyExportedSpan, traceData: PosthogTraceData): AnyExportedSpan | undefined {\n let ancestor = span.parentSpanId ? traceData.getSpan({ spanId: span.parentSpanId }) : undefined;\n while (ancestor && ancestor.type !== SpanType.MODEL_GENERATION) {\n ancestor = ancestor.parentSpanId ? traceData.getSpan({ spanId: ancestor.parentSpanId }) : undefined;\n }\n return ancestor;\n }\n\n private resolveToolCallId(span: AnyExportedSpan): string | undefined {\n const attrs = span.attributes as { toolCallId?: string } | undefined;\n return (\n attrs?.toolCallId ??\n (span.metadata?.toolCallId as string | undefined) ??\n (span.input as { toolCallId?: string } | undefined)?.toolCallId\n );\n }\n\n private getDistinctId(span: AnyExportedSpan, traceData?: PosthogTraceData): string {\n if (span.metadata?.userId) {\n return String(span.metadata.userId);\n }\n\n if (traceData?.hasExtraValue(DISTINCT_ID)) {\n return String(traceData.getExtraValue(DISTINCT_ID));\n }\n\n if (this.config.defaultDistinctId) {\n return this.config.defaultDistinctId;\n }\n\n return 'anonymous';\n }\n\n /**\n * Check if the parent of this span is the root span.\n * We need this because we don't create $ai_span for root spans,\n * so children of root spans should use $ai_trace_id as their $ai_parent_id.\n */\n private isParentRootSpan(span: AnyExportedSpan, traceData: PosthogTraceData): boolean {\n if (!span.parentSpanId) {\n return false;\n }\n\n // Look up the parent span in our cache to check if it's a root span\n const parentCache = traceData.getSpan({ spanId: span.parentSpanId });\n if (parentCache) {\n return parentCache.isRootSpan;\n }\n\n // Parent not found in cache - shouldn't happen normally, but default to false\n return false;\n }\n\n private buildEventProperties(\n span: AnyExportedSpan,\n latency: number,\n parentIsRootSpan: boolean = false,\n isModelCall: boolean = this.isModelCall(span),\n ): Record<string, any> {\n const baseProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_latency: latency,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.parentSpanId) {\n // If parent is the root span, use trace_id as parent_id since we don't\n // create an $ai_span for root spans (only $ai_trace)\n baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;\n }\n\n if (span.metadata?.sessionId) {\n baseProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n // Include tags for root spans (tags are only set on root spans by design)\n // PostHog doesn't allow setting tags directly, so we iterate through each tag\n // and set it as a property with value true\n if (span.isRootSpan && span.tags?.length) {\n for (const tag of span.tags) {\n baseProperties[tag] = true;\n }\n }\n\n if (isModelCall) {\n baseProperties.$ai_generation_id = span.id;\n return { ...baseProperties, ...this.buildGenerationProperties(span) };\n } else {\n baseProperties.$ai_span_id = span.id;\n baseProperties.$ai_span_name = span.name;\n return { ...baseProperties, ...this.buildSpanProperties(span) };\n }\n }\n\n private extractErrorProperties(errorInfo?: SpanErrorInfo): Record<string, any> {\n if (!errorInfo) {\n return {};\n }\n\n const props: Record<string, string> = {\n error_message: errorInfo.message,\n };\n\n if (errorInfo.id) {\n props.error_id = errorInfo.id;\n }\n\n if (errorInfo.category) {\n props.error_category = errorInfo.category;\n }\n\n return props;\n }\n\n private extractCustomMetadata(metadata?: Record<string, unknown>): Record<string, any> {\n const { userId, sessionId, ...customMetadata } = metadata ?? {};\n return customMetadata;\n }\n\n private buildGenerationProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n const attrs = (span.attributes ?? {}) as ModelGenerationAttributes;\n\n props.$ai_model = attrs.model || 'unknown-model';\n props.$ai_provider = attrs.provider || 'unknown-provider';\n\n if (span.input) props.$ai_input = this.formatMessages(span.input, 'user');\n if (span.output) props.$ai_output_choices = this.formatMessages(span.output, 'assistant');\n\n // Extract usage properties using the shared utility\n Object.assign(props, formatUsageMetrics(attrs.usage));\n\n if (attrs.parameters) {\n if (attrs.parameters.temperature !== undefined) props.$ai_temperature = attrs.parameters.temperature;\n if (attrs.parameters.maxOutputTokens !== undefined) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;\n }\n if (attrs.streaming !== undefined) props.$ai_stream = attrs.streaming;\n if (attrs.tools?.length) {\n // OpenAI-style shape — the format PostHog's own AI SDKs send and its\n // trace view renders. Provider-defined tools pass through as-is.\n props.$ai_tools = attrs.tools.map(tool =>\n tool.type === 'function'\n ? {\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n }\n : tool,\n );\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private buildSpanProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n\n if (span.input) props.$ai_input_state = span.input;\n if (span.output) props.$ai_output_state = span.output;\n\n if (span.type === SpanType.MODEL_CHUNK) {\n const attrs = span.attributes as any;\n if (attrs?.chunkType) props.chunk_type = attrs.chunkType;\n if (attrs?.sequenceNumber !== undefined) props.chunk_sequence_number = attrs.sequenceNumber;\n }\n\n if (span.attributes) {\n Object.assign(props, span.attributes);\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private formatMessages(data: SpanData, defaultRole: 'user' | 'assistant' = 'user'): PostHogMessage[] {\n // Unwrap {messages: [...]} wrapper produced by generation span inputs\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'messages' in data) {\n const wrapped = (data as Record<string, unknown>).messages;\n if (this.isMessageArray(wrapped)) {\n return wrapped.map(msg => this.normalizeMessage(msg));\n }\n }\n\n if (this.isMessageArray(data)) {\n return data.map(msg => this.normalizeMessage(msg));\n }\n\n if (typeof data === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text: data }] }];\n }\n\n if (this.isSpanOutputWithToolCalls(data)) {\n const content: PostHogContent[] = [];\n if (data.text) {\n content.push({ type: 'text', text: data.text });\n }\n for (const tc of data.toolCalls) {\n content.push({\n type: 'tool-call',\n id: tc.toolCallId,\n function: { name: tc.toolName, arguments: tc.args },\n });\n }\n return [{ role: 'assistant', content }];\n }\n\n // Extract text from output objects (e.g. generation outputs with text but no tool calls)\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'text' in data) {\n const text = (data as Record<string, unknown>).text;\n if (typeof text === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text }] }];\n }\n }\n\n return [{ role: defaultRole, content: [{ type: 'text', text: this.safeStringify(data) }] }];\n }\n\n private isSpanOutputWithToolCalls(\n data: unknown,\n ): data is { text?: string; toolCalls: Array<{ toolCallId: string; toolName: string; args: unknown }> } {\n if (typeof data !== 'object' || data === null || !('toolCalls' in data)) return false;\n const { toolCalls } = data as Record<string, unknown>;\n return Array.isArray(toolCalls) && toolCalls.length > 0;\n }\n\n private isMessageArray(data: unknown): data is MastraMessage[] {\n if (!Array.isArray(data)) {\n return false;\n }\n\n return data.every(item => typeof item === 'object' && item !== null && 'role' in item && 'content' in item);\n }\n\n private normalizeMessage(msg: MastraMessage): PostHogMessage {\n if (typeof msg.content === 'string') {\n return {\n role: msg.role as PostHogMessage['role'],\n content: [{ type: 'text', text: msg.content }],\n };\n }\n\n return {\n role: msg.role as PostHogMessage['role'],\n content: msg.content as PostHogContent[],\n };\n }\n\n private safeStringify(data: unknown): string {\n try {\n return JSON.stringify(data);\n } catch {\n if (typeof data === 'object' && data !== null) {\n return `[Non-serializable ${data.constructor?.name || 'Object'}]`;\n }\n return String(data);\n }\n }\n\n /**\n * Force flush any buffered data to PostHog without shutting down.\n */\n protected override async _flush(): Promise<void> {\n if (this.#client) {\n await this.#client.flush();\n }\n }\n\n override async _postShutdown(): Promise<void> {\n if (this.#client) {\n await this.#client.shutdown();\n }\n }\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,32 @@
1
1
  import { SpanType } from "@mastra/core/observability";
2
+ import * as observability from "@mastra/observability";
2
3
  import { TrackingExporter } from "@mastra/observability";
3
4
  import { PostHog } from "posthog-node";
5
+ import { coreFeatures } from "@mastra/core/features";
6
+ //#region src/features.ts
7
+ /**
8
+ * Feature detection for paired @mastra/core and @mastra/observability versions.
9
+ *
10
+ * Both are resolved synchronously so span classification is stable from the
11
+ * very first span, without depending on a pending dynamic import.
12
+ *
13
+ * `@mastra/observability` is a hard dependency, but older versions do not
14
+ * export `observabilityFeatures`. The namespace import reads it as `undefined`
15
+ * in that case instead of failing to link, which is the graceful fallback
16
+ * documented in `observability/mastra/src/features.ts`.
17
+ */
18
+ const FEATURE = "model-inference-span";
19
+ let observabilityFeatures = observability.observabilityFeatures;
20
+ /**
21
+ * Returns true when both packages report the `model-inference-span` feature,
22
+ * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives which span
23
+ * is exported as `$ai_generation` (new: MODEL_INFERENCE; legacy:
24
+ * MODEL_GENERATION).
25
+ */
26
+ function isModelInferenceEnabled() {
27
+ return observabilityFeatures?.has(FEATURE) === true && coreFeatures.has(FEATURE);
28
+ }
29
+ //#endregion
4
30
  //#region src/tracing.ts
5
31
  /**
6
32
  * Formats UsageStats to PostHog's expected property format.
@@ -24,6 +50,8 @@ function formatUsageMetrics(usage) {
24
50
  return props;
25
51
  }
26
52
  const DISTINCT_ID = "distinctId";
53
+ /** Per-trace key prefix for the provider calls and tool results recorded under one MODEL_GENERATION. */
54
+ const CONVERSATION = "conversation:";
27
55
  var PosthogExporter = class PosthogExporter extends TrackingExporter {
28
56
  name = "posthog";
29
57
  #client;
@@ -66,12 +94,12 @@ var PosthogExporter = class PosthogExporter extends TrackingExporter {
66
94
  skipCachingEventSpans = true;
67
95
  async _buildEvent(args) {
68
96
  const { span, traceData } = args;
69
- const eventName = this.mapToPostHogEvent(span.type);
97
+ const isModelCall = this.isModelCall(span);
70
98
  const distinctId = this.getDistinctId(span, traceData);
71
- const properties = this.buildEventProperties(span, 0);
99
+ const properties = this.buildEventProperties(span, 0, false, isModelCall);
72
100
  this.#client?.captureAi(this.withGroups({
73
101
  distinctId,
74
- event: eventName,
102
+ event: isModelCall ? "$ai_generation" : "$ai_span",
75
103
  properties,
76
104
  timestamp: span.endTime ? new Date(span.endTime) : /* @__PURE__ */ new Date()
77
105
  }));
@@ -97,16 +125,17 @@ var PosthogExporter = class PosthogExporter extends TrackingExporter {
97
125
  input: cachedSpan.input
98
126
  } : span;
99
127
  const eventMessage = this.buildEventMessage({
100
- span: mergedSpan,
128
+ span: this.withCallInput(mergedSpan, traceData),
101
129
  traceData
102
130
  });
103
131
  this.#client?.captureAi(this.withGroups(eventMessage));
132
+ this.recordForConversation(span, traceData);
104
133
  }
105
134
  async _abortSpan(args) {
106
135
  const { span, reason, traceData } = args;
107
136
  span.errorInfo = reason;
108
137
  const eventMessage = this.buildEventMessage({
109
- span,
138
+ span: this.withCallInput(span, traceData),
110
139
  traceData
111
140
  });
112
141
  this.#client?.captureAi(this.withGroups(eventMessage));
@@ -218,22 +247,102 @@ var PosthogExporter = class PosthogExporter extends TrackingExporter {
218
247
  }
219
248
  buildChildEventMessage(args) {
220
249
  const { span, distinctId, endTime, traceData } = args;
221
- const eventName = this.mapToPostHogEvent(span.type);
250
+ const isModelCall = this.isModelCall(span);
222
251
  const latency = (endTime - span.startTime.getTime()) / 1e3;
223
252
  const parentIsRootSpan = this.isParentRootSpan(span, traceData);
253
+ const properties = this.buildEventProperties(span, latency, parentIsRootSpan, isModelCall);
224
254
  return {
225
255
  distinctId,
226
- event: eventName,
227
- properties: this.buildEventProperties(span, latency, parentIsRootSpan),
256
+ event: isModelCall ? "$ai_generation" : "$ai_span",
257
+ properties,
228
258
  timestamp: new Date(endTime)
229
259
  };
230
260
  }
231
261
  toDate(timestamp) {
232
262
  return timestamp instanceof Date ? timestamp : new Date(timestamp);
233
263
  }
234
- mapToPostHogEvent(spanType) {
235
- if (spanType == SpanType.MODEL_GENERATION) return "$ai_generation";
236
- return "$ai_span";
264
+ /**
265
+ * Whether this span is exported as `$ai_generation`. PostHog expects one per
266
+ * provider call and sums tokens and cost across them, so exactly one span
267
+ * per call may carry usage. With paired packages that emit MODEL_INFERENCE,
268
+ * that is the call and MODEL_GENERATION is the loop around it, exported as
269
+ * `$ai_span`. Older pairings only emit MODEL_GENERATION, which keeps the
270
+ * `$ai_generation` role.
271
+ */
272
+ isModelCall(span) {
273
+ return span.type === (isModelInferenceEnabled() ? SpanType.MODEL_INFERENCE : SpanType.MODEL_GENERATION);
274
+ }
275
+ /**
276
+ * Give a MODEL_INFERENCE exported as `$ai_generation` the conversation that
277
+ * call received. The span has no input of its own, so it is rebuilt from the
278
+ * generation's input plus, for each earlier call in the same generation, its
279
+ * output and the results of the tools it called (as Braintrust's Thread view
280
+ * does). Earlier calls and their tools end before this call does, so they are
281
+ * already recorded.
282
+ */
283
+ withCallInput(span, traceData) {
284
+ if (span.type !== SpanType.MODEL_INFERENCE || !this.isModelCall(span)) return span;
285
+ const generation = this.findGeneration(span, traceData);
286
+ if (!generation?.input) return span;
287
+ const messages = this.formatMessages(generation.input, "user");
288
+ const conversation = traceData.getExtraValue(CONVERSATION + generation.id);
289
+ for (const call of conversation?.calls ?? []) {
290
+ const output = call.output;
291
+ if (!output?.text && !output?.toolCalls?.length) continue;
292
+ messages.push(...this.formatMessages(output, "assistant"));
293
+ const results = (output.toolCalls ?? []).map((toolCall) => ({
294
+ toolCall,
295
+ toolSpan: conversation.toolResults.get(toolCall.toolCallId)
296
+ })).filter((result) => !!result.toolSpan).sort((a, b) => this.toDate(a.toolSpan.startTime).getTime() - this.toDate(b.toolSpan.startTime).getTime());
297
+ for (const { toolCall, toolSpan } of results) {
298
+ const isError = !!toolSpan.errorInfo || toolSpan.type === SpanType.PROVIDER_TOOL_CALL && toolSpan.attributes?.success === false;
299
+ messages.push({
300
+ role: "tool",
301
+ content: [{
302
+ type: "tool-result",
303
+ toolCallId: toolCall.toolCallId,
304
+ toolName: toolCall.toolName,
305
+ output: toolSpan.errorInfo ? toolSpan.errorInfo.message : toolSpan.output,
306
+ ...isError ? { isError: true } : {}
307
+ }]
308
+ });
309
+ }
310
+ }
311
+ return {
312
+ ...span,
313
+ input: messages
314
+ };
315
+ }
316
+ /** Record ended calls and tool results so later calls in the same generation can rebuild their input. */
317
+ recordForConversation(span, traceData) {
318
+ const isCall = span.type === SpanType.MODEL_INFERENCE && !span.errorInfo;
319
+ const isProviderToolWithResult = span.type === SpanType.PROVIDER_TOOL_CALL && typeof span.attributes?.success === "boolean";
320
+ const isTool = span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || isProviderToolWithResult;
321
+ if (!isModelInferenceEnabled() || !isCall && !isTool) return;
322
+ const generation = this.findGeneration(span, traceData);
323
+ if (!generation) return;
324
+ const key = CONVERSATION + generation.id;
325
+ let conversation = traceData.getExtraValue(key);
326
+ if (!conversation) {
327
+ conversation = {
328
+ calls: [],
329
+ toolResults: /* @__PURE__ */ new Map()
330
+ };
331
+ traceData.setExtraValue(key, conversation);
332
+ }
333
+ if (isCall) conversation.calls.push(span);
334
+ else {
335
+ const toolCallId = this.resolveToolCallId(span);
336
+ if (toolCallId) conversation.toolResults.set(toolCallId, span);
337
+ }
338
+ }
339
+ findGeneration(span, traceData) {
340
+ let ancestor = span.parentSpanId ? traceData.getSpan({ spanId: span.parentSpanId }) : void 0;
341
+ while (ancestor && ancestor.type !== SpanType.MODEL_GENERATION) ancestor = ancestor.parentSpanId ? traceData.getSpan({ spanId: ancestor.parentSpanId }) : void 0;
342
+ return ancestor;
343
+ }
344
+ resolveToolCallId(span) {
345
+ return span.attributes?.toolCallId ?? span.metadata?.toolCallId ?? span.input?.toolCallId;
237
346
  }
238
347
  getDistinctId(span, traceData) {
239
348
  if (span.metadata?.userId) return String(span.metadata.userId);
@@ -252,7 +361,7 @@ var PosthogExporter = class PosthogExporter extends TrackingExporter {
252
361
  if (parentCache) return parentCache.isRootSpan;
253
362
  return false;
254
363
  }
255
- buildEventProperties(span, latency, parentIsRootSpan = false) {
364
+ buildEventProperties(span, latency, parentIsRootSpan = false, isModelCall = this.isModelCall(span)) {
256
365
  const baseProperties = {
257
366
  $ai_trace_id: span.traceId,
258
367
  $ai_latency: latency,
@@ -261,7 +370,7 @@ var PosthogExporter = class PosthogExporter extends TrackingExporter {
261
370
  if (span.parentSpanId) baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;
262
371
  if (span.metadata?.sessionId) baseProperties.$ai_session_id = span.metadata.sessionId;
263
372
  if (span.isRootSpan && span.tags?.length) for (const tag of span.tags) baseProperties[tag] = true;
264
- if (span.type === SpanType.MODEL_GENERATION) {
373
+ if (isModelCall) {
265
374
  baseProperties.$ai_generation_id = span.id;
266
375
  return {
267
376
  ...baseProperties,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#client"],"sources":["../src/tracing.ts"],"sourcesContent":["import type {\n AnyExportedSpan,\n ExportedFeedback,\n FeedbackEvent,\n ModelGenerationAttributes,\n SpanErrorInfo,\n UsageStats,\n} from '@mastra/core/observability';\nimport { SpanType } from '@mastra/core/observability';\nimport type { TraceData, TrackingExporterConfig } from '@mastra/observability';\nimport { TrackingExporter } from '@mastra/observability';\nimport { PostHog } from 'posthog-node';\nimport type { EventMessage } from 'posthog-node';\n\n/**\n * Token usage format compatible with PostHog.\n * @see https://posthog.com/docs/llm-analytics/generations#event-properties\n */\nexport interface PostHogUsageMetrics {\n $ai_input_tokens?: number;\n $ai_output_tokens?: number;\n $ai_cache_read_input_tokens?: number;\n $ai_cache_creation_input_tokens?: number;\n $ai_cache_creation_5m_input_tokens?: number;\n $ai_cache_creation_1h_input_tokens?: number;\n}\n\n/**\n * Formats UsageStats to PostHog's expected property format.\n *\n * Pass through gross input token counts with cache fields as subsets.\n * PostHog subtracts cache tokens when computing costs for non-Anthropic\n * providers and detects Anthropic-style exclusive reporting on its own.\n *\n * @param usage - The UsageStats from span attributes\n * @returns PostHog-formatted usage properties\n */\nexport function formatUsageMetrics(usage?: UsageStats): PostHogUsageMetrics {\n if (!usage) return {};\n\n const props: PostHogUsageMetrics = {};\n\n if (usage.inputTokens !== undefined) {\n props.$ai_input_tokens = usage.inputTokens;\n }\n\n if (usage.inputDetails?.cacheRead !== undefined) {\n props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;\n }\n\n if (usage.inputDetails?.cacheWrite !== undefined) {\n props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;\n }\n if (usage.inputDetails?.cacheWrite5m !== undefined) {\n props.$ai_cache_creation_5m_input_tokens = usage.inputDetails.cacheWrite5m;\n }\n if (usage.inputDetails?.cacheWrite1h !== undefined) {\n props.$ai_cache_creation_1h_input_tokens = usage.inputDetails.cacheWrite1h;\n }\n\n if (usage.outputTokens !== undefined) {\n props.$ai_output_tokens = usage.outputTokens;\n }\n\n return props;\n}\n\ninterface PostHogMessage {\n role: 'user' | 'assistant' | 'system' | 'tool';\n content: PostHogContent[];\n}\n\ninterface PostHogContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ninterface MastraMessage {\n role: string;\n content: string | MastraContent[];\n}\n\ninterface MastraContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ntype SpanData = string | MastraMessage[] | Record<string, unknown> | unknown;\n\nconst DISTINCT_ID = 'distinctId';\n\nexport interface PosthogExporterConfig extends TrackingExporterConfig {\n /** PostHog API key. Defaults to POSTHOG_API_KEY environment variable. */\n apiKey?: string;\n /** PostHog host URL. Defaults to POSTHOG_HOST environment variable or US region. */\n host?: string;\n flushAt?: number;\n flushInterval?: number;\n serverless?: boolean;\n defaultDistinctId?: string;\n enablePrivacyMode?: boolean;\n}\n\ntype PosthogRoot = unknown;\ntype PosthogSpan = AnyExportedSpan;\n// used as a placeholder for event data since we don't need to cache\n// event data for Posthog\ntype PosthogEvent = boolean;\ntype PosthogMetadata = unknown;\ntype PosthogTraceData = TraceData<PosthogRoot, PosthogSpan, PosthogEvent, PosthogMetadata>;\n\nexport class PosthogExporter extends TrackingExporter<\n PosthogRoot,\n PosthogSpan,\n PosthogEvent,\n PosthogMetadata,\n PosthogExporterConfig\n> {\n name = 'posthog';\n #client: PostHog | undefined;\n\n private static readonly SERVERLESS_FLUSH_AT = 10;\n private static readonly SERVERLESS_FLUSH_INTERVAL = 2000;\n private static readonly DEFAULT_FLUSH_AT = 20;\n private static readonly DEFAULT_FLUSH_INTERVAL = 10000;\n\n constructor(config: PosthogExporterConfig = {}) {\n // Resolve env vars BEFORE calling super (config is readonly in base class)\n const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;\n\n super({ ...config, apiKey });\n\n if (!apiKey) {\n this.setDisabled('Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.');\n return;\n }\n\n const clientConfig = this.buildClientConfig(this.config);\n this.#client = new PostHog(apiKey, clientConfig);\n const message =\n (config.serverless ?? false) ? 'PostHog exporter initialized in serverless mode' : 'PostHog exporter initialized';\n this.logger.debug(message, config);\n }\n\n private buildClientConfig(config: PosthogExporterConfig) {\n const isServerless = config.serverless ?? false;\n const flushAt =\n config.flushAt ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_AT : PosthogExporter.DEFAULT_FLUSH_AT);\n const flushInterval =\n config.flushInterval ??\n (isServerless ? PosthogExporter.SERVERLESS_FLUSH_INTERVAL : PosthogExporter.DEFAULT_FLUSH_INTERVAL);\n\n const host = config.host || process.env.POSTHOG_HOST || 'https://us.i.posthog.com';\n\n if (!config.host && !process.env.POSTHOG_HOST) {\n this.logger.info(\n 'No PostHog host specified, using US default (https://us.i.posthog.com). ' +\n 'For EU region, set `host: \"https://eu.i.posthog.com\"` in config or POSTHOG_HOST env var. ' +\n 'For self-hosted, provide your instance URL.',\n );\n }\n\n return {\n host,\n flushAt,\n flushInterval,\n privacyMode: config.enablePrivacyMode,\n };\n }\n\n protected override skipBuildRootTask = true;\n protected override async _buildRoot(_args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogRoot | undefined> {\n throw new Error('Method not implemented.');\n }\n\n protected override skipCachingEventSpans = true;\n protected override async _buildEvent(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogEvent> {\n const { span, traceData } = args;\n\n const eventName = this.mapToPostHogEvent(span.type);\n const distinctId = this.getDistinctId(span, traceData);\n const properties = this.buildEventProperties(span, 0);\n\n // captureAi() targets PostHog's dedicated AI endpoint (8 MiB per event, oversized events\n // dropped individually). Plain capture() uses /batch/, which returns 413 for large spans.\n this.#client?.captureAi(\n this.withGroups({\n distinctId,\n event: eventName,\n properties,\n timestamp: span.endTime ? new Date(span.endTime) : new Date(),\n }),\n );\n\n return true;\n }\n\n protected override async _buildSpan(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogSpan | undefined> {\n const { span, traceData } = args;\n if (!traceData.hasExtraValue(DISTINCT_ID)) {\n const userId = span.metadata?.userId;\n if (userId) {\n traceData.setExtraValue(DISTINCT_ID, String(userId));\n }\n }\n\n return span;\n }\n\n protected override skipSpanUpdateEvents = true;\n protected override _updateSpan(_args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n protected override async _finishSpan(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n const { span, traceData } = args;\n\n // Merge input from cached span (SPAN_STARTED) if not present on end span\n // This handles the case where input is only sent at start\n const cachedSpan = traceData.getSpan({ spanId: span.id });\n const mergedSpan = !span.input && cachedSpan?.input ? { ...span, input: cachedSpan.input } : span;\n\n const eventMessage = this.buildEventMessage({ span: mergedSpan, traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n protected override async _abortSpan(args: {\n span: PosthogSpan;\n reason: SpanErrorInfo;\n traceData: PosthogTraceData;\n }): Promise<void> {\n const { span, reason, traceData } = args;\n\n // update span with the abort reason\n span.errorInfo = reason;\n\n const eventMessage = this.buildEventMessage({ span, traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n /**\n * Forward feedback recorded via `addFeedback()` to PostHog as a native\n * `$ai_feedback` event, shown as \"User feedback\" on the linked trace in\n * PostHog's AI observability UI.\n */\n async onFeedbackEvent(event: FeedbackEvent): Promise<void> {\n if (!this.#client) return;\n\n const { feedback } = event;\n if (!feedback.traceId) {\n this.logger.debug('PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id', {\n feedbackId: feedback.feedbackId,\n });\n return;\n }\n\n const properties: Record<string, any> = {\n // Custom metadata goes first so it cannot overwrite the natively mapped fields below\n ...this.extractCustomMetadata(feedback.metadata),\n $ai_trace_id: feedback.traceId,\n // PostHog's trace UI only displays feedback events that carry $ai_feedback_text\n $ai_feedback_text: feedback.comment ?? String(feedback.value),\n feedback_id: feedback.feedbackId,\n feedback_type: feedback.feedbackType,\n feedback_value: feedback.value,\n };\n\n const feedbackSource = feedback.feedbackSource ?? feedback.source;\n if (feedbackSource) properties.feedback_source = feedbackSource;\n if (feedback.spanId) properties.span_id = feedback.spanId;\n if (feedback.sourceId) properties.source_id = feedback.sourceId;\n if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;\n\n try {\n this.#client.captureAi(\n this.withGroups({\n distinctId: this.getFeedbackDistinctId(feedback),\n event: '$ai_feedback',\n properties,\n timestamp: new Date(feedback.timestamp),\n }),\n );\n } catch (err) {\n this.logger.error('PostHog exporter: failed to submit feedback', {\n error: err,\n traceId: feedback.traceId,\n feedbackId: feedback.feedbackId,\n });\n }\n }\n\n private getFeedbackDistinctId(feedback: ExportedFeedback): string {\n const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;\n if (userId) {\n return String(userId);\n }\n return this.config.defaultDistinctId ?? 'anonymous';\n }\n\n /**\n * PostHog group analytics are keyed off the top-level `groups` field on the\n * capture call. The Node SDK derives the event's `$groups` from that field and\n * overwrites any property-level `$groups`, so group metadata carried in\n * properties is dropped unless it is mirrored here.\n */\n private withGroups(message: EventMessage): EventMessage {\n const groups = message.properties?.$groups;\n if (groups && typeof groups === 'object' && !Array.isArray(groups)) {\n return { ...message, groups };\n }\n return message;\n }\n\n private buildEventMessage(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): EventMessage {\n const { span, traceData } = args;\n\n const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();\n\n const distinctId = this.getDistinctId(span, traceData);\n\n if (span.isRootSpan) {\n return this.buildRootEventMessage({ span, distinctId, endTime });\n } else {\n return this.buildChildEventMessage({ span, distinctId, endTime, traceData });\n }\n }\n\n /**\n * Capture an explicit $ai_trace event for root spans.\n * This gives us control over trace-level metadata like name and tags,\n * rather than relying on PostHog's pseudo-trace auto-creation.\n */\n private buildRootEventMessage(args: { span: AnyExportedSpan; distinctId: string; endTime: number }): EventMessage {\n const { span, distinctId, endTime } = args;\n\n // Note: We don't set $ai_latency on $ai_trace events because PostHog\n // aggregates latency from child events. Setting it here causes double-counting.\n const traceProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_span_name: span.name,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.metadata?.sessionId) {\n traceProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n if (span.input) {\n traceProperties.$ai_input_state = span.input;\n }\n\n if (span.output) {\n traceProperties.$ai_output_state = span.output;\n }\n\n if (span.errorInfo) {\n traceProperties.$ai_error = {\n message: span.errorInfo.message,\n ...(span.errorInfo.id && { id: span.errorInfo.id }),\n ...(span.errorInfo.category && { category: span.errorInfo.category }),\n };\n }\n\n // Add tags as custom properties (PostHog doesn't have native tag support on traces)\n if (span.tags?.length) {\n for (const tag of span.tags) {\n traceProperties[tag] = true;\n }\n }\n\n // Add custom metadata (excluding userId and sessionId which are handled separately)\n const { userId, sessionId, ...customMetadata } = span.metadata ?? {};\n Object.assign(traceProperties, customMetadata);\n\n return {\n distinctId,\n event: '$ai_trace',\n properties: traceProperties,\n timestamp: new Date(endTime),\n };\n }\n\n private buildChildEventMessage(args: {\n span: AnyExportedSpan;\n distinctId: string;\n endTime: number;\n traceData: PosthogTraceData;\n }): EventMessage {\n const { span, distinctId, endTime, traceData } = args;\n\n const eventName = this.mapToPostHogEvent(span.type);\n const startTime = span.startTime.getTime();\n const latency = (endTime - startTime) / 1000;\n\n // Check if parent is the root span - if so, use traceId as parent_id\n // since we don't create an $ai_span for root spans\n const parentIsRootSpan = this.isParentRootSpan(span, traceData);\n const properties = this.buildEventProperties(span, latency, parentIsRootSpan);\n\n return {\n distinctId,\n event: eventName,\n properties,\n timestamp: new Date(endTime),\n };\n }\n\n private toDate(timestamp: Date | number): Date {\n return timestamp instanceof Date ? timestamp : new Date(timestamp);\n }\n\n private mapToPostHogEvent(spanType: SpanType): string {\n if (spanType == SpanType.MODEL_GENERATION) {\n return '$ai_generation';\n }\n return '$ai_span';\n }\n\n private getDistinctId(span: AnyExportedSpan, traceData?: PosthogTraceData): string {\n if (span.metadata?.userId) {\n return String(span.metadata.userId);\n }\n\n if (traceData?.hasExtraValue(DISTINCT_ID)) {\n return String(traceData.getExtraValue(DISTINCT_ID));\n }\n\n if (this.config.defaultDistinctId) {\n return this.config.defaultDistinctId;\n }\n\n return 'anonymous';\n }\n\n /**\n * Check if the parent of this span is the root span.\n * We need this because we don't create $ai_span for root spans,\n * so children of root spans should use $ai_trace_id as their $ai_parent_id.\n */\n private isParentRootSpan(span: AnyExportedSpan, traceData: PosthogTraceData): boolean {\n if (!span.parentSpanId) {\n return false;\n }\n\n // Look up the parent span in our cache to check if it's a root span\n const parentCache = traceData.getSpan({ spanId: span.parentSpanId });\n if (parentCache) {\n return parentCache.isRootSpan;\n }\n\n // Parent not found in cache - shouldn't happen normally, but default to false\n return false;\n }\n\n private buildEventProperties(\n span: AnyExportedSpan,\n latency: number,\n parentIsRootSpan: boolean = false,\n ): Record<string, any> {\n const baseProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_latency: latency,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.parentSpanId) {\n // If parent is the root span, use trace_id as parent_id since we don't\n // create an $ai_span for root spans (only $ai_trace)\n baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;\n }\n\n if (span.metadata?.sessionId) {\n baseProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n // Include tags for root spans (tags are only set on root spans by design)\n // PostHog doesn't allow setting tags directly, so we iterate through each tag\n // and set it as a property with value true\n if (span.isRootSpan && span.tags?.length) {\n for (const tag of span.tags) {\n baseProperties[tag] = true;\n }\n }\n\n if (span.type === SpanType.MODEL_GENERATION) {\n baseProperties.$ai_generation_id = span.id;\n return { ...baseProperties, ...this.buildGenerationProperties(span) };\n } else {\n baseProperties.$ai_span_id = span.id;\n baseProperties.$ai_span_name = span.name;\n return { ...baseProperties, ...this.buildSpanProperties(span) };\n }\n }\n\n private extractErrorProperties(errorInfo?: SpanErrorInfo): Record<string, any> {\n if (!errorInfo) {\n return {};\n }\n\n const props: Record<string, string> = {\n error_message: errorInfo.message,\n };\n\n if (errorInfo.id) {\n props.error_id = errorInfo.id;\n }\n\n if (errorInfo.category) {\n props.error_category = errorInfo.category;\n }\n\n return props;\n }\n\n private extractCustomMetadata(metadata?: Record<string, unknown>): Record<string, any> {\n const { userId, sessionId, ...customMetadata } = metadata ?? {};\n return customMetadata;\n }\n\n private buildGenerationProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n const attrs = (span.attributes ?? {}) as ModelGenerationAttributes;\n\n props.$ai_model = attrs.model || 'unknown-model';\n props.$ai_provider = attrs.provider || 'unknown-provider';\n\n if (span.input) props.$ai_input = this.formatMessages(span.input, 'user');\n if (span.output) props.$ai_output_choices = this.formatMessages(span.output, 'assistant');\n\n // Extract usage properties using the shared utility\n Object.assign(props, formatUsageMetrics(attrs.usage));\n\n if (attrs.parameters) {\n if (attrs.parameters.temperature !== undefined) props.$ai_temperature = attrs.parameters.temperature;\n if (attrs.parameters.maxOutputTokens !== undefined) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;\n }\n if (attrs.streaming !== undefined) props.$ai_stream = attrs.streaming;\n if (attrs.tools?.length) {\n // OpenAI-style shape — the format PostHog's own AI SDKs send and its\n // trace view renders. Provider-defined tools pass through as-is.\n props.$ai_tools = attrs.tools.map(tool =>\n tool.type === 'function'\n ? {\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n }\n : tool,\n );\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private buildSpanProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n\n if (span.input) props.$ai_input_state = span.input;\n if (span.output) props.$ai_output_state = span.output;\n\n if (span.type === SpanType.MODEL_CHUNK) {\n const attrs = span.attributes as any;\n if (attrs?.chunkType) props.chunk_type = attrs.chunkType;\n if (attrs?.sequenceNumber !== undefined) props.chunk_sequence_number = attrs.sequenceNumber;\n }\n\n if (span.attributes) {\n Object.assign(props, span.attributes);\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private formatMessages(data: SpanData, defaultRole: 'user' | 'assistant' = 'user'): PostHogMessage[] {\n // Unwrap {messages: [...]} wrapper produced by generation span inputs\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'messages' in data) {\n const wrapped = (data as Record<string, unknown>).messages;\n if (this.isMessageArray(wrapped)) {\n return wrapped.map(msg => this.normalizeMessage(msg));\n }\n }\n\n if (this.isMessageArray(data)) {\n return data.map(msg => this.normalizeMessage(msg));\n }\n\n if (typeof data === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text: data }] }];\n }\n\n if (this.isSpanOutputWithToolCalls(data)) {\n const content: PostHogContent[] = [];\n if (data.text) {\n content.push({ type: 'text', text: data.text });\n }\n for (const tc of data.toolCalls) {\n content.push({\n type: 'tool-call',\n id: tc.toolCallId,\n function: { name: tc.toolName, arguments: tc.args },\n });\n }\n return [{ role: 'assistant', content }];\n }\n\n // Extract text from output objects (e.g. generation outputs with text but no tool calls)\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'text' in data) {\n const text = (data as Record<string, unknown>).text;\n if (typeof text === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text }] }];\n }\n }\n\n return [{ role: defaultRole, content: [{ type: 'text', text: this.safeStringify(data) }] }];\n }\n\n private isSpanOutputWithToolCalls(\n data: unknown,\n ): data is { text?: string; toolCalls: Array<{ toolCallId: string; toolName: string; args: unknown }> } {\n if (typeof data !== 'object' || data === null || !('toolCalls' in data)) return false;\n const { toolCalls } = data as Record<string, unknown>;\n return Array.isArray(toolCalls) && toolCalls.length > 0;\n }\n\n private isMessageArray(data: unknown): data is MastraMessage[] {\n if (!Array.isArray(data)) {\n return false;\n }\n\n return data.every(item => typeof item === 'object' && item !== null && 'role' in item && 'content' in item);\n }\n\n private normalizeMessage(msg: MastraMessage): PostHogMessage {\n if (typeof msg.content === 'string') {\n return {\n role: msg.role as PostHogMessage['role'],\n content: [{ type: 'text', text: msg.content }],\n };\n }\n\n return {\n role: msg.role as PostHogMessage['role'],\n content: msg.content as PostHogContent[],\n };\n }\n\n private safeStringify(data: unknown): string {\n try {\n return JSON.stringify(data);\n } catch {\n if (typeof data === 'object' && data !== null) {\n return `[Non-serializable ${data.constructor?.name || 'Object'}]`;\n }\n return String(data);\n }\n }\n\n /**\n * Force flush any buffered data to PostHog without shutting down.\n */\n protected override async _flush(): Promise<void> {\n if (this.#client) {\n await this.#client.flush();\n }\n }\n\n override async _postShutdown(): Promise<void> {\n if (this.#client) {\n await this.#client.shutdown();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#client"],"sources":["../src/features.ts","../src/tracing.ts"],"sourcesContent":["/**\n * Feature detection for paired @mastra/core and @mastra/observability versions.\n *\n * Both are resolved synchronously so span classification is stable from the\n * very first span, without depending on a pending dynamic import.\n *\n * `@mastra/observability` is a hard dependency, but older versions do not\n * export `observabilityFeatures`. The namespace import reads it as `undefined`\n * in that case instead of failing to link, which is the graceful fallback\n * documented in `observability/mastra/src/features.ts`.\n */\n\nimport { coreFeatures } from '@mastra/core/features';\nimport * as observability from '@mastra/observability';\n\nconst FEATURE = 'model-inference-span';\n\nlet observabilityFeatures: ReadonlySet<string> | undefined = (\n observability as { observabilityFeatures?: ReadonlySet<string> }\n).observabilityFeatures;\n\n/**\n * Returns true when both packages report the `model-inference-span` feature,\n * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives which span\n * is exported as `$ai_generation` (new: MODEL_INFERENCE; legacy:\n * MODEL_GENERATION).\n */\nexport function isModelInferenceEnabled(): boolean {\n return observabilityFeatures?.has(FEATURE) === true && coreFeatures.has(FEATURE);\n}\n\n/**\n * @internal Test-only override. Allows tests to simulate a paired older or\n * newer `@mastra/observability` without juggling module mocks.\n */\nexport function __setObservabilityFeaturesForTest(features: ReadonlySet<string> | undefined): void {\n observabilityFeatures = features;\n}\n","import type {\n AnyExportedSpan,\n ExportedFeedback,\n FeedbackEvent,\n ModelGenerationAttributes,\n SpanErrorInfo,\n UsageStats,\n} from '@mastra/core/observability';\nimport { SpanType } from '@mastra/core/observability';\nimport type { TraceData, TrackingExporterConfig } from '@mastra/observability';\nimport { TrackingExporter } from '@mastra/observability';\nimport { PostHog } from 'posthog-node';\nimport type { EventMessage } from 'posthog-node';\nimport { isModelInferenceEnabled } from './features';\n\n/**\n * Token usage format compatible with PostHog.\n * @see https://posthog.com/docs/llm-analytics/generations#event-properties\n */\nexport interface PostHogUsageMetrics {\n $ai_input_tokens?: number;\n $ai_output_tokens?: number;\n $ai_cache_read_input_tokens?: number;\n $ai_cache_creation_input_tokens?: number;\n $ai_cache_creation_5m_input_tokens?: number;\n $ai_cache_creation_1h_input_tokens?: number;\n}\n\n/**\n * Formats UsageStats to PostHog's expected property format.\n *\n * Pass through gross input token counts with cache fields as subsets.\n * PostHog subtracts cache tokens when computing costs for non-Anthropic\n * providers and detects Anthropic-style exclusive reporting on its own.\n *\n * @param usage - The UsageStats from span attributes\n * @returns PostHog-formatted usage properties\n */\nexport function formatUsageMetrics(usage?: UsageStats): PostHogUsageMetrics {\n if (!usage) return {};\n\n const props: PostHogUsageMetrics = {};\n\n if (usage.inputTokens !== undefined) {\n props.$ai_input_tokens = usage.inputTokens;\n }\n\n if (usage.inputDetails?.cacheRead !== undefined) {\n props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;\n }\n\n if (usage.inputDetails?.cacheWrite !== undefined) {\n props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;\n }\n if (usage.inputDetails?.cacheWrite5m !== undefined) {\n props.$ai_cache_creation_5m_input_tokens = usage.inputDetails.cacheWrite5m;\n }\n if (usage.inputDetails?.cacheWrite1h !== undefined) {\n props.$ai_cache_creation_1h_input_tokens = usage.inputDetails.cacheWrite1h;\n }\n\n if (usage.outputTokens !== undefined) {\n props.$ai_output_tokens = usage.outputTokens;\n }\n\n return props;\n}\n\ninterface PostHogMessage {\n role: 'user' | 'assistant' | 'system' | 'tool';\n content: PostHogContent[];\n}\n\ninterface PostHogContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ninterface MastraMessage {\n role: string;\n content: string | MastraContent[];\n}\n\ninterface MastraContent {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\ntype SpanData = string | MastraMessage[] | Record<string, unknown> | unknown;\n\nconst DISTINCT_ID = 'distinctId';\n/** Per-trace key prefix for the provider calls and tool results recorded under one MODEL_GENERATION. */\nconst CONVERSATION = 'conversation:';\n\n/** Ended spans under one MODEL_GENERATION, used to rebuild each provider call's `$ai_input`. */\ninterface Conversation {\n /** Ended MODEL_INFERENCE spans, in the order they ended. */\n calls: AnyExportedSpan[];\n /** Ended TOOL_CALL / MCP_TOOL_CALL spans and PROVIDER_TOOL_CALL spans with a result, keyed by toolCallId. */\n toolResults: Map<string, AnyExportedSpan>;\n}\n\nexport interface PosthogExporterConfig extends TrackingExporterConfig {\n /** PostHog API key. Defaults to POSTHOG_API_KEY environment variable. */\n apiKey?: string;\n /** PostHog host URL. Defaults to POSTHOG_HOST environment variable or US region. */\n host?: string;\n flushAt?: number;\n flushInterval?: number;\n serverless?: boolean;\n defaultDistinctId?: string;\n enablePrivacyMode?: boolean;\n}\n\ntype PosthogRoot = unknown;\ntype PosthogSpan = AnyExportedSpan;\n// used as a placeholder for event data since we don't need to cache\n// event data for Posthog\ntype PosthogEvent = boolean;\ntype PosthogMetadata = unknown;\ntype PosthogTraceData = TraceData<PosthogRoot, PosthogSpan, PosthogEvent, PosthogMetadata>;\n\nexport class PosthogExporter extends TrackingExporter<\n PosthogRoot,\n PosthogSpan,\n PosthogEvent,\n PosthogMetadata,\n PosthogExporterConfig\n> {\n name = 'posthog';\n #client: PostHog | undefined;\n\n private static readonly SERVERLESS_FLUSH_AT = 10;\n private static readonly SERVERLESS_FLUSH_INTERVAL = 2000;\n private static readonly DEFAULT_FLUSH_AT = 20;\n private static readonly DEFAULT_FLUSH_INTERVAL = 10000;\n\n constructor(config: PosthogExporterConfig = {}) {\n // Resolve env vars BEFORE calling super (config is readonly in base class)\n const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;\n\n super({ ...config, apiKey });\n\n if (!apiKey) {\n this.setDisabled('Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.');\n return;\n }\n\n const clientConfig = this.buildClientConfig(this.config);\n this.#client = new PostHog(apiKey, clientConfig);\n const message =\n (config.serverless ?? false) ? 'PostHog exporter initialized in serverless mode' : 'PostHog exporter initialized';\n this.logger.debug(message, config);\n }\n\n private buildClientConfig(config: PosthogExporterConfig) {\n const isServerless = config.serverless ?? false;\n const flushAt =\n config.flushAt ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_AT : PosthogExporter.DEFAULT_FLUSH_AT);\n const flushInterval =\n config.flushInterval ??\n (isServerless ? PosthogExporter.SERVERLESS_FLUSH_INTERVAL : PosthogExporter.DEFAULT_FLUSH_INTERVAL);\n\n const host = config.host || process.env.POSTHOG_HOST || 'https://us.i.posthog.com';\n\n if (!config.host && !process.env.POSTHOG_HOST) {\n this.logger.info(\n 'No PostHog host specified, using US default (https://us.i.posthog.com). ' +\n 'For EU region, set `host: \"https://eu.i.posthog.com\"` in config or POSTHOG_HOST env var. ' +\n 'For self-hosted, provide your instance URL.',\n );\n }\n\n return {\n host,\n flushAt,\n flushInterval,\n privacyMode: config.enablePrivacyMode,\n };\n }\n\n protected override skipBuildRootTask = true;\n protected override async _buildRoot(_args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogRoot | undefined> {\n throw new Error('Method not implemented.');\n }\n\n protected override skipCachingEventSpans = true;\n protected override async _buildEvent(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogEvent> {\n const { span, traceData } = args;\n\n const isModelCall = this.isModelCall(span);\n const distinctId = this.getDistinctId(span, traceData);\n const properties = this.buildEventProperties(span, 0, false, isModelCall);\n\n // captureAi() targets PostHog's dedicated AI endpoint (8 MiB per event, oversized events\n // dropped individually). Plain capture() uses /batch/, which returns 413 for large spans.\n this.#client?.captureAi(\n this.withGroups({\n distinctId,\n event: isModelCall ? '$ai_generation' : '$ai_span',\n properties,\n timestamp: span.endTime ? new Date(span.endTime) : new Date(),\n }),\n );\n\n return true;\n }\n\n protected override async _buildSpan(args: {\n span: AnyExportedSpan;\n traceData: PosthogTraceData;\n }): Promise<PosthogSpan | undefined> {\n const { span, traceData } = args;\n if (!traceData.hasExtraValue(DISTINCT_ID)) {\n const userId = span.metadata?.userId;\n if (userId) {\n traceData.setExtraValue(DISTINCT_ID, String(userId));\n }\n }\n\n return span;\n }\n\n protected override skipSpanUpdateEvents = true;\n protected override _updateSpan(_args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n protected override async _finishSpan(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): Promise<void> {\n const { span, traceData } = args;\n\n // Merge input from cached span (SPAN_STARTED) if not present on end span\n // This handles the case where input is only sent at start\n const cachedSpan = traceData.getSpan({ spanId: span.id });\n const mergedSpan = !span.input && cachedSpan?.input ? { ...span, input: cachedSpan.input } : span;\n\n const eventMessage = this.buildEventMessage({ span: this.withCallInput(mergedSpan, traceData), traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n\n this.recordForConversation(span, traceData);\n }\n\n protected override async _abortSpan(args: {\n span: PosthogSpan;\n reason: SpanErrorInfo;\n traceData: PosthogTraceData;\n }): Promise<void> {\n const { span, reason, traceData } = args;\n\n // update span with the abort reason\n span.errorInfo = reason;\n\n const eventMessage = this.buildEventMessage({ span: this.withCallInput(span, traceData), traceData });\n this.#client?.captureAi(this.withGroups(eventMessage));\n }\n\n /**\n * Forward feedback recorded via `addFeedback()` to PostHog as a native\n * `$ai_feedback` event, shown as \"User feedback\" on the linked trace in\n * PostHog's AI observability UI.\n */\n async onFeedbackEvent(event: FeedbackEvent): Promise<void> {\n if (!this.#client) return;\n\n const { feedback } = event;\n if (!feedback.traceId) {\n this.logger.debug('PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id', {\n feedbackId: feedback.feedbackId,\n });\n return;\n }\n\n const properties: Record<string, any> = {\n // Custom metadata goes first so it cannot overwrite the natively mapped fields below\n ...this.extractCustomMetadata(feedback.metadata),\n $ai_trace_id: feedback.traceId,\n // PostHog's trace UI only displays feedback events that carry $ai_feedback_text\n $ai_feedback_text: feedback.comment ?? String(feedback.value),\n feedback_id: feedback.feedbackId,\n feedback_type: feedback.feedbackType,\n feedback_value: feedback.value,\n };\n\n const feedbackSource = feedback.feedbackSource ?? feedback.source;\n if (feedbackSource) properties.feedback_source = feedbackSource;\n if (feedback.spanId) properties.span_id = feedback.spanId;\n if (feedback.sourceId) properties.source_id = feedback.sourceId;\n if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;\n\n try {\n this.#client.captureAi(\n this.withGroups({\n distinctId: this.getFeedbackDistinctId(feedback),\n event: '$ai_feedback',\n properties,\n timestamp: new Date(feedback.timestamp),\n }),\n );\n } catch (err) {\n this.logger.error('PostHog exporter: failed to submit feedback', {\n error: err,\n traceId: feedback.traceId,\n feedbackId: feedback.feedbackId,\n });\n }\n }\n\n private getFeedbackDistinctId(feedback: ExportedFeedback): string {\n const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;\n if (userId) {\n return String(userId);\n }\n return this.config.defaultDistinctId ?? 'anonymous';\n }\n\n /**\n * PostHog group analytics are keyed off the top-level `groups` field on the\n * capture call. The Node SDK derives the event's `$groups` from that field and\n * overwrites any property-level `$groups`, so group metadata carried in\n * properties is dropped unless it is mirrored here.\n */\n private withGroups(message: EventMessage): EventMessage {\n const groups = message.properties?.$groups;\n if (groups && typeof groups === 'object' && !Array.isArray(groups)) {\n return { ...message, groups };\n }\n return message;\n }\n\n private buildEventMessage(args: { span: AnyExportedSpan; traceData: PosthogTraceData }): EventMessage {\n const { span, traceData } = args;\n\n const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();\n\n const distinctId = this.getDistinctId(span, traceData);\n\n if (span.isRootSpan) {\n return this.buildRootEventMessage({ span, distinctId, endTime });\n } else {\n return this.buildChildEventMessage({ span, distinctId, endTime, traceData });\n }\n }\n\n /**\n * Capture an explicit $ai_trace event for root spans.\n * This gives us control over trace-level metadata like name and tags,\n * rather than relying on PostHog's pseudo-trace auto-creation.\n */\n private buildRootEventMessage(args: { span: AnyExportedSpan; distinctId: string; endTime: number }): EventMessage {\n const { span, distinctId, endTime } = args;\n\n // Note: We don't set $ai_latency on $ai_trace events because PostHog\n // aggregates latency from child events. Setting it here causes double-counting.\n const traceProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_span_name: span.name,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.metadata?.sessionId) {\n traceProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n if (span.input) {\n traceProperties.$ai_input_state = span.input;\n }\n\n if (span.output) {\n traceProperties.$ai_output_state = span.output;\n }\n\n if (span.errorInfo) {\n traceProperties.$ai_error = {\n message: span.errorInfo.message,\n ...(span.errorInfo.id && { id: span.errorInfo.id }),\n ...(span.errorInfo.category && { category: span.errorInfo.category }),\n };\n }\n\n // Add tags as custom properties (PostHog doesn't have native tag support on traces)\n if (span.tags?.length) {\n for (const tag of span.tags) {\n traceProperties[tag] = true;\n }\n }\n\n // Add custom metadata (excluding userId and sessionId which are handled separately)\n const { userId, sessionId, ...customMetadata } = span.metadata ?? {};\n Object.assign(traceProperties, customMetadata);\n\n return {\n distinctId,\n event: '$ai_trace',\n properties: traceProperties,\n timestamp: new Date(endTime),\n };\n }\n\n private buildChildEventMessage(args: {\n span: AnyExportedSpan;\n distinctId: string;\n endTime: number;\n traceData: PosthogTraceData;\n }): EventMessage {\n const { span, distinctId, endTime, traceData } = args;\n\n const isModelCall = this.isModelCall(span);\n const startTime = span.startTime.getTime();\n const latency = (endTime - startTime) / 1000;\n\n // Check if parent is the root span - if so, use traceId as parent_id\n // since we don't create an $ai_span for root spans\n const parentIsRootSpan = this.isParentRootSpan(span, traceData);\n const properties = this.buildEventProperties(span, latency, parentIsRootSpan, isModelCall);\n\n return {\n distinctId,\n event: isModelCall ? '$ai_generation' : '$ai_span',\n properties,\n timestamp: new Date(endTime),\n };\n }\n\n private toDate(timestamp: Date | number): Date {\n return timestamp instanceof Date ? timestamp : new Date(timestamp);\n }\n\n /**\n * Whether this span is exported as `$ai_generation`. PostHog expects one per\n * provider call and sums tokens and cost across them, so exactly one span\n * per call may carry usage. With paired packages that emit MODEL_INFERENCE,\n * that is the call and MODEL_GENERATION is the loop around it, exported as\n * `$ai_span`. Older pairings only emit MODEL_GENERATION, which keeps the\n * `$ai_generation` role.\n */\n private isModelCall(span: AnyExportedSpan): boolean {\n return span.type === (isModelInferenceEnabled() ? SpanType.MODEL_INFERENCE : SpanType.MODEL_GENERATION);\n }\n\n /**\n * Give a MODEL_INFERENCE exported as `$ai_generation` the conversation that\n * call received. The span has no input of its own, so it is rebuilt from the\n * generation's input plus, for each earlier call in the same generation, its\n * output and the results of the tools it called (as Braintrust's Thread view\n * does). Earlier calls and their tools end before this call does, so they are\n * already recorded.\n */\n private withCallInput(span: AnyExportedSpan, traceData: PosthogTraceData): AnyExportedSpan {\n if (span.type !== SpanType.MODEL_INFERENCE || !this.isModelCall(span)) return span;\n\n const generation = this.findGeneration(span, traceData);\n if (!generation?.input) return span;\n\n const messages = this.formatMessages(generation.input, 'user');\n const conversation = traceData.getExtraValue(CONVERSATION + generation.id) as Conversation | undefined;\n for (const call of conversation?.calls ?? []) {\n const output = call.output as\n | { text?: string; toolCalls?: Array<{ toolCallId: string; toolName: string }> }\n | undefined;\n if (!output?.text && !output?.toolCalls?.length) continue;\n messages.push(...this.formatMessages(output, 'assistant'));\n\n // Parallel tool calls are ordered by when each tool started.\n const results = (output.toolCalls ?? [])\n .map(toolCall => ({ toolCall, toolSpan: conversation!.toolResults.get(toolCall.toolCallId) }))\n .filter(\n (result): result is { toolCall: typeof result.toolCall; toolSpan: AnyExportedSpan } => !!result.toolSpan,\n )\n .sort((a, b) => this.toDate(a.toolSpan.startTime).getTime() - this.toDate(b.toolSpan.startTime).getTime());\n for (const { toolCall, toolSpan } of results) {\n // Provider-executed tools report failure through `success`, not `errorInfo`.\n const isError =\n !!toolSpan.errorInfo ||\n (toolSpan.type === SpanType.PROVIDER_TOOL_CALL &&\n (toolSpan.attributes as { success?: boolean } | undefined)?.success === false);\n // Same `tool-result` part shape PostHog's own AI SDK integrations send.\n messages.push({\n role: 'tool',\n content: [\n {\n type: 'tool-result',\n toolCallId: toolCall.toolCallId,\n toolName: toolCall.toolName,\n output: toolSpan.errorInfo ? toolSpan.errorInfo.message : toolSpan.output,\n ...(isError ? { isError: true } : {}),\n },\n ],\n });\n }\n }\n\n return { ...span, input: messages };\n }\n\n /** Record ended calls and tool results so later calls in the same generation can rebuild their input. */\n private recordForConversation(span: AnyExportedSpan, traceData: PosthogTraceData): void {\n const isCall = span.type === SpanType.MODEL_INFERENCE && !span.errorInfo;\n // A PROVIDER_TOOL_CALL span ended without a result (e.g. the run stopped first)\n // has no `success`, and recording it would add an empty result.\n const isProviderToolWithResult =\n span.type === SpanType.PROVIDER_TOOL_CALL &&\n typeof (span.attributes as { success?: unknown } | undefined)?.success === 'boolean';\n const isTool = span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || isProviderToolWithResult;\n if (!isModelInferenceEnabled() || (!isCall && !isTool)) return;\n\n const generation = this.findGeneration(span, traceData);\n if (!generation) return;\n\n const key = CONVERSATION + generation.id;\n let conversation = traceData.getExtraValue(key) as Conversation | undefined;\n if (!conversation) {\n conversation = { calls: [], toolResults: new Map() };\n traceData.setExtraValue(key, conversation);\n }\n if (isCall) {\n conversation.calls.push(span);\n } else {\n const toolCallId = this.resolveToolCallId(span);\n if (toolCallId) conversation.toolResults.set(toolCallId, span);\n }\n }\n\n private findGeneration(span: AnyExportedSpan, traceData: PosthogTraceData): AnyExportedSpan | undefined {\n let ancestor = span.parentSpanId ? traceData.getSpan({ spanId: span.parentSpanId }) : undefined;\n while (ancestor && ancestor.type !== SpanType.MODEL_GENERATION) {\n ancestor = ancestor.parentSpanId ? traceData.getSpan({ spanId: ancestor.parentSpanId }) : undefined;\n }\n return ancestor;\n }\n\n private resolveToolCallId(span: AnyExportedSpan): string | undefined {\n const attrs = span.attributes as { toolCallId?: string } | undefined;\n return (\n attrs?.toolCallId ??\n (span.metadata?.toolCallId as string | undefined) ??\n (span.input as { toolCallId?: string } | undefined)?.toolCallId\n );\n }\n\n private getDistinctId(span: AnyExportedSpan, traceData?: PosthogTraceData): string {\n if (span.metadata?.userId) {\n return String(span.metadata.userId);\n }\n\n if (traceData?.hasExtraValue(DISTINCT_ID)) {\n return String(traceData.getExtraValue(DISTINCT_ID));\n }\n\n if (this.config.defaultDistinctId) {\n return this.config.defaultDistinctId;\n }\n\n return 'anonymous';\n }\n\n /**\n * Check if the parent of this span is the root span.\n * We need this because we don't create $ai_span for root spans,\n * so children of root spans should use $ai_trace_id as their $ai_parent_id.\n */\n private isParentRootSpan(span: AnyExportedSpan, traceData: PosthogTraceData): boolean {\n if (!span.parentSpanId) {\n return false;\n }\n\n // Look up the parent span in our cache to check if it's a root span\n const parentCache = traceData.getSpan({ spanId: span.parentSpanId });\n if (parentCache) {\n return parentCache.isRootSpan;\n }\n\n // Parent not found in cache - shouldn't happen normally, but default to false\n return false;\n }\n\n private buildEventProperties(\n span: AnyExportedSpan,\n latency: number,\n parentIsRootSpan: boolean = false,\n isModelCall: boolean = this.isModelCall(span),\n ): Record<string, any> {\n const baseProperties: Record<string, any> = {\n $ai_trace_id: span.traceId,\n $ai_latency: latency,\n $ai_is_error: !!span.errorInfo,\n };\n\n if (span.parentSpanId) {\n // If parent is the root span, use trace_id as parent_id since we don't\n // create an $ai_span for root spans (only $ai_trace)\n baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;\n }\n\n if (span.metadata?.sessionId) {\n baseProperties.$ai_session_id = span.metadata.sessionId;\n }\n\n // Include tags for root spans (tags are only set on root spans by design)\n // PostHog doesn't allow setting tags directly, so we iterate through each tag\n // and set it as a property with value true\n if (span.isRootSpan && span.tags?.length) {\n for (const tag of span.tags) {\n baseProperties[tag] = true;\n }\n }\n\n if (isModelCall) {\n baseProperties.$ai_generation_id = span.id;\n return { ...baseProperties, ...this.buildGenerationProperties(span) };\n } else {\n baseProperties.$ai_span_id = span.id;\n baseProperties.$ai_span_name = span.name;\n return { ...baseProperties, ...this.buildSpanProperties(span) };\n }\n }\n\n private extractErrorProperties(errorInfo?: SpanErrorInfo): Record<string, any> {\n if (!errorInfo) {\n return {};\n }\n\n const props: Record<string, string> = {\n error_message: errorInfo.message,\n };\n\n if (errorInfo.id) {\n props.error_id = errorInfo.id;\n }\n\n if (errorInfo.category) {\n props.error_category = errorInfo.category;\n }\n\n return props;\n }\n\n private extractCustomMetadata(metadata?: Record<string, unknown>): Record<string, any> {\n const { userId, sessionId, ...customMetadata } = metadata ?? {};\n return customMetadata;\n }\n\n private buildGenerationProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n const attrs = (span.attributes ?? {}) as ModelGenerationAttributes;\n\n props.$ai_model = attrs.model || 'unknown-model';\n props.$ai_provider = attrs.provider || 'unknown-provider';\n\n if (span.input) props.$ai_input = this.formatMessages(span.input, 'user');\n if (span.output) props.$ai_output_choices = this.formatMessages(span.output, 'assistant');\n\n // Extract usage properties using the shared utility\n Object.assign(props, formatUsageMetrics(attrs.usage));\n\n if (attrs.parameters) {\n if (attrs.parameters.temperature !== undefined) props.$ai_temperature = attrs.parameters.temperature;\n if (attrs.parameters.maxOutputTokens !== undefined) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;\n }\n if (attrs.streaming !== undefined) props.$ai_stream = attrs.streaming;\n if (attrs.tools?.length) {\n // OpenAI-style shape — the format PostHog's own AI SDKs send and its\n // trace view renders. Provider-defined tools pass through as-is.\n props.$ai_tools = attrs.tools.map(tool =>\n tool.type === 'function'\n ? {\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n }\n : tool,\n );\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private buildSpanProperties(span: AnyExportedSpan): Record<string, any> {\n const props: Record<string, any> = {};\n\n if (span.input) props.$ai_input_state = span.input;\n if (span.output) props.$ai_output_state = span.output;\n\n if (span.type === SpanType.MODEL_CHUNK) {\n const attrs = span.attributes as any;\n if (attrs?.chunkType) props.chunk_type = attrs.chunkType;\n if (attrs?.sequenceNumber !== undefined) props.chunk_sequence_number = attrs.sequenceNumber;\n }\n\n if (span.attributes) {\n Object.assign(props, span.attributes);\n }\n\n return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };\n }\n\n private formatMessages(data: SpanData, defaultRole: 'user' | 'assistant' = 'user'): PostHogMessage[] {\n // Unwrap {messages: [...]} wrapper produced by generation span inputs\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'messages' in data) {\n const wrapped = (data as Record<string, unknown>).messages;\n if (this.isMessageArray(wrapped)) {\n return wrapped.map(msg => this.normalizeMessage(msg));\n }\n }\n\n if (this.isMessageArray(data)) {\n return data.map(msg => this.normalizeMessage(msg));\n }\n\n if (typeof data === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text: data }] }];\n }\n\n if (this.isSpanOutputWithToolCalls(data)) {\n const content: PostHogContent[] = [];\n if (data.text) {\n content.push({ type: 'text', text: data.text });\n }\n for (const tc of data.toolCalls) {\n content.push({\n type: 'tool-call',\n id: tc.toolCallId,\n function: { name: tc.toolName, arguments: tc.args },\n });\n }\n return [{ role: 'assistant', content }];\n }\n\n // Extract text from output objects (e.g. generation outputs with text but no tool calls)\n if (typeof data === 'object' && data !== null && !Array.isArray(data) && 'text' in data) {\n const text = (data as Record<string, unknown>).text;\n if (typeof text === 'string') {\n return [{ role: defaultRole, content: [{ type: 'text', text }] }];\n }\n }\n\n return [{ role: defaultRole, content: [{ type: 'text', text: this.safeStringify(data) }] }];\n }\n\n private isSpanOutputWithToolCalls(\n data: unknown,\n ): data is { text?: string; toolCalls: Array<{ toolCallId: string; toolName: string; args: unknown }> } {\n if (typeof data !== 'object' || data === null || !('toolCalls' in data)) return false;\n const { toolCalls } = data as Record<string, unknown>;\n return Array.isArray(toolCalls) && toolCalls.length > 0;\n }\n\n private isMessageArray(data: unknown): data is MastraMessage[] {\n if (!Array.isArray(data)) {\n return false;\n }\n\n return data.every(item => typeof item === 'object' && item !== null && 'role' in item && 'content' in item);\n }\n\n private normalizeMessage(msg: MastraMessage): PostHogMessage {\n if (typeof msg.content === 'string') {\n return {\n role: msg.role as PostHogMessage['role'],\n content: [{ type: 'text', text: msg.content }],\n };\n }\n\n return {\n role: msg.role as PostHogMessage['role'],\n content: msg.content as PostHogContent[],\n };\n }\n\n private safeStringify(data: unknown): string {\n try {\n return JSON.stringify(data);\n } catch {\n if (typeof data === 'object' && data !== null) {\n return `[Non-serializable ${data.constructor?.name || 'Object'}]`;\n }\n return String(data);\n }\n }\n\n /**\n * Force flush any buffered data to PostHog without shutting down.\n */\n protected override async _flush(): Promise<void> {\n if (this.#client) {\n await this.#client.flush();\n }\n }\n\n override async _postShutdown(): Promise<void> {\n if (this.#client) {\n await this.#client.shutdown();\n }\n }\n}\n"],"mappings":"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"}
package/dist/tracing.d.ts CHANGED
@@ -100,7 +100,28 @@ export declare class PosthogExporter extends TrackingExporter<PosthogRoot, Posth
100
100
  private buildRootEventMessage;
101
101
  private buildChildEventMessage;
102
102
  private toDate;
103
- private mapToPostHogEvent;
103
+ /**
104
+ * Whether this span is exported as `$ai_generation`. PostHog expects one per
105
+ * provider call and sums tokens and cost across them, so exactly one span
106
+ * per call may carry usage. With paired packages that emit MODEL_INFERENCE,
107
+ * that is the call and MODEL_GENERATION is the loop around it, exported as
108
+ * `$ai_span`. Older pairings only emit MODEL_GENERATION, which keeps the
109
+ * `$ai_generation` role.
110
+ */
111
+ private isModelCall;
112
+ /**
113
+ * Give a MODEL_INFERENCE exported as `$ai_generation` the conversation that
114
+ * call received. The span has no input of its own, so it is rebuilt from the
115
+ * generation's input plus, for each earlier call in the same generation, its
116
+ * output and the results of the tools it called (as Braintrust's Thread view
117
+ * does). Earlier calls and their tools end before this call does, so they are
118
+ * already recorded.
119
+ */
120
+ private withCallInput;
121
+ /** Record ended calls and tool results so later calls in the same generation can rebuild their input. */
122
+ private recordForConversation;
123
+ private findGeneration;
124
+ private resolveToolCallId;
104
125
  private getDistinctId;
105
126
  /**
106
127
  * Check if the parent of this span is the root span.
@@ -1 +1 @@
1
- {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/posthog",
3
- "version": "1.3.16",
3
+ "version": "1.4.0-alpha.0",
4
4
  "description": "PostHog observability provider for Mastra",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "posthog-node": "^5.49.0",
27
- "@mastra/observability": "1.18.4"
27
+ "@mastra/observability": "1.19.0-alpha.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "22.20.1",
@@ -33,9 +33,9 @@
33
33
  "typescript": "^7.0.2",
34
34
  "vitest": "4.1.11",
35
35
  "@internal/lint": "0.0.141",
36
+ "@mastra/core": "1.76.0-alpha.0",
36
37
  "@internal/types-builder": "0.0.116",
37
- "@observability/test-utils": "0.1.0",
38
- "@mastra/core": "1.75.0"
38
+ "@observability/test-utils": "0.1.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@mastra/core": ">=1.16.0-0 <2.0.0-0"