@mastra/posthog 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,437 +1,418 @@
1
- 'use strict';
2
-
3
- var observability$1 = require('@mastra/core/observability');
4
- var observability = require('@mastra/observability');
5
- var posthogNode = require('posthog-node');
6
-
7
- // src/tracing.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_observability = require("@mastra/core/observability");
3
+ let _mastra_observability = require("@mastra/observability");
4
+ let posthog_node = require("posthog-node");
5
+ //#region src/tracing.ts
6
+ /**
7
+ * Formats UsageStats to PostHog's expected property format.
8
+ *
9
+ * Pass through gross input token counts with cache fields as subsets.
10
+ * PostHog subtracts cache tokens when computing costs for non-Anthropic
11
+ * providers and detects Anthropic-style exclusive reporting on its own.
12
+ *
13
+ * @param usage - The UsageStats from span attributes
14
+ * @returns PostHog-formatted usage properties
15
+ */
8
16
  function formatUsageMetrics(usage) {
9
- if (!usage) return {};
10
- const props = {};
11
- if (usage.inputTokens !== void 0) {
12
- props.$ai_input_tokens = usage.inputTokens;
13
- }
14
- if (usage.inputDetails?.cacheRead !== void 0) {
15
- props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;
16
- }
17
- if (usage.inputDetails?.cacheWrite !== void 0) {
18
- props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;
19
- }
20
- if (usage.outputTokens !== void 0) {
21
- props.$ai_output_tokens = usage.outputTokens;
22
- }
23
- return props;
17
+ if (!usage) return {};
18
+ const props = {};
19
+ if (usage.inputTokens !== void 0) props.$ai_input_tokens = usage.inputTokens;
20
+ if (usage.inputDetails?.cacheRead !== void 0) props.$ai_cache_read_input_tokens = usage.inputDetails.cacheRead;
21
+ if (usage.inputDetails?.cacheWrite !== void 0) props.$ai_cache_creation_input_tokens = usage.inputDetails.cacheWrite;
22
+ if (usage.outputTokens !== void 0) props.$ai_output_tokens = usage.outputTokens;
23
+ return props;
24
24
  }
25
- var DISTINCT_ID = "distinctId";
26
- var PosthogExporter = class _PosthogExporter extends observability.TrackingExporter {
27
- name = "posthog";
28
- #client;
29
- static SERVERLESS_FLUSH_AT = 10;
30
- static SERVERLESS_FLUSH_INTERVAL = 2e3;
31
- static DEFAULT_FLUSH_AT = 20;
32
- static DEFAULT_FLUSH_INTERVAL = 1e4;
33
- constructor(config = {}) {
34
- const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;
35
- super({ ...config, apiKey });
36
- if (!apiKey) {
37
- this.setDisabled("Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.");
38
- return;
39
- }
40
- const clientConfig = this.buildClientConfig(this.config);
41
- this.#client = new posthogNode.PostHog(apiKey, clientConfig);
42
- const message = config.serverless ?? false ? "PostHog exporter initialized in serverless mode" : "PostHog exporter initialized";
43
- this.logger.debug(message, config);
44
- }
45
- buildClientConfig(config) {
46
- const isServerless = config.serverless ?? false;
47
- const flushAt = config.flushAt ?? (isServerless ? _PosthogExporter.SERVERLESS_FLUSH_AT : _PosthogExporter.DEFAULT_FLUSH_AT);
48
- const flushInterval = config.flushInterval ?? (isServerless ? _PosthogExporter.SERVERLESS_FLUSH_INTERVAL : _PosthogExporter.DEFAULT_FLUSH_INTERVAL);
49
- const host = config.host || process.env.POSTHOG_HOST || "https://us.i.posthog.com";
50
- if (!config.host && !process.env.POSTHOG_HOST) {
51
- this.logger.info(
52
- 'No PostHog host specified, using US default (https://us.i.posthog.com). For EU region, set `host: "https://eu.i.posthog.com"` in config or POSTHOG_HOST env var. For self-hosted, provide your instance URL.'
53
- );
54
- }
55
- return {
56
- host,
57
- flushAt,
58
- flushInterval,
59
- privacyMode: config.enablePrivacyMode
60
- };
61
- }
62
- skipBuildRootTask = true;
63
- async _buildRoot(_args) {
64
- throw new Error("Method not implemented.");
65
- }
66
- skipCachingEventSpans = true;
67
- async _buildEvent(args) {
68
- const { span, traceData } = args;
69
- const eventName = this.mapToPostHogEvent(span.type);
70
- const distinctId = this.getDistinctId(span, traceData);
71
- const properties = this.buildEventProperties(span, 0);
72
- this.#client?.capture(
73
- this.withGroups({
74
- distinctId,
75
- event: eventName,
76
- properties,
77
- timestamp: span.endTime ? new Date(span.endTime) : /* @__PURE__ */ new Date()
78
- })
79
- );
80
- return true;
81
- }
82
- async _buildSpan(args) {
83
- const { span, traceData } = args;
84
- if (!traceData.hasExtraValue(DISTINCT_ID)) {
85
- const userId = span.metadata?.userId;
86
- if (userId) {
87
- traceData.setExtraValue(DISTINCT_ID, String(userId));
88
- }
89
- }
90
- return span;
91
- }
92
- skipSpanUpdateEvents = true;
93
- _updateSpan(_args) {
94
- throw new Error("Method not implemented.");
95
- }
96
- async _finishSpan(args) {
97
- const { span, traceData } = args;
98
- const cachedSpan = traceData.getSpan({ spanId: span.id });
99
- const mergedSpan = !span.input && cachedSpan?.input ? { ...span, input: cachedSpan.input } : span;
100
- const eventMessage = this.buildEventMessage({ span: mergedSpan, traceData });
101
- this.#client?.capture(this.withGroups(eventMessage));
102
- }
103
- async _abortSpan(args) {
104
- const { span, reason, traceData } = args;
105
- span.errorInfo = reason;
106
- const eventMessage = this.buildEventMessage({ span, traceData });
107
- this.#client?.capture(this.withGroups(eventMessage));
108
- }
109
- /**
110
- * Forward feedback recorded via `addFeedback()` to PostHog as a native
111
- * `$ai_feedback` event, shown as "User feedback" on the linked trace in
112
- * PostHog's AI observability UI.
113
- */
114
- async onFeedbackEvent(event) {
115
- if (!this.#client) return;
116
- const { feedback } = event;
117
- if (!feedback.traceId) {
118
- this.logger.debug("PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id", {
119
- feedbackId: feedback.feedbackId
120
- });
121
- return;
122
- }
123
- const properties = {
124
- // Custom metadata goes first so it cannot overwrite the natively mapped fields below
125
- ...this.extractCustomMetadata(feedback.metadata),
126
- $ai_trace_id: feedback.traceId,
127
- // PostHog's trace UI only displays feedback events that carry $ai_feedback_text
128
- $ai_feedback_text: feedback.comment ?? String(feedback.value),
129
- feedback_id: feedback.feedbackId,
130
- feedback_type: feedback.feedbackType,
131
- feedback_value: feedback.value
132
- };
133
- const feedbackSource = feedback.feedbackSource ?? feedback.source;
134
- if (feedbackSource) properties.feedback_source = feedbackSource;
135
- if (feedback.spanId) properties.span_id = feedback.spanId;
136
- if (feedback.sourceId) properties.source_id = feedback.sourceId;
137
- if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;
138
- try {
139
- this.#client.capture(
140
- this.withGroups({
141
- distinctId: this.getFeedbackDistinctId(feedback),
142
- event: "$ai_feedback",
143
- properties,
144
- timestamp: new Date(feedback.timestamp)
145
- })
146
- );
147
- } catch (err) {
148
- this.logger.error("PostHog exporter: failed to submit feedback", {
149
- error: err,
150
- traceId: feedback.traceId,
151
- feedbackId: feedback.feedbackId
152
- });
153
- }
154
- }
155
- getFeedbackDistinctId(feedback) {
156
- const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;
157
- if (userId) {
158
- return String(userId);
159
- }
160
- return this.config.defaultDistinctId ?? "anonymous";
161
- }
162
- /**
163
- * PostHog group analytics are keyed off the top-level `groups` field on the
164
- * capture call. The Node SDK derives the event's `$groups` from that field and
165
- * overwrites any property-level `$groups`, so group metadata carried in
166
- * properties is dropped unless it is mirrored here.
167
- */
168
- withGroups(message) {
169
- const groups = message.properties?.$groups;
170
- if (groups && typeof groups === "object" && !Array.isArray(groups)) {
171
- return { ...message, groups };
172
- }
173
- return message;
174
- }
175
- buildEventMessage(args) {
176
- const { span, traceData } = args;
177
- const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();
178
- const distinctId = this.getDistinctId(span, traceData);
179
- if (span.isRootSpan) {
180
- return this.buildRootEventMessage({ span, distinctId, endTime });
181
- } else {
182
- return this.buildChildEventMessage({ span, distinctId, endTime, traceData });
183
- }
184
- }
185
- /**
186
- * Capture an explicit $ai_trace event for root spans.
187
- * This gives us control over trace-level metadata like name and tags,
188
- * rather than relying on PostHog's pseudo-trace auto-creation.
189
- */
190
- buildRootEventMessage(args) {
191
- const { span, distinctId, endTime } = args;
192
- const traceProperties = {
193
- $ai_trace_id: span.traceId,
194
- $ai_span_name: span.name,
195
- $ai_is_error: !!span.errorInfo
196
- };
197
- if (span.metadata?.sessionId) {
198
- traceProperties.$ai_session_id = span.metadata.sessionId;
199
- }
200
- if (span.input) {
201
- traceProperties.$ai_input_state = span.input;
202
- }
203
- if (span.output) {
204
- traceProperties.$ai_output_state = span.output;
205
- }
206
- if (span.errorInfo) {
207
- traceProperties.$ai_error = {
208
- message: span.errorInfo.message,
209
- ...span.errorInfo.id && { id: span.errorInfo.id },
210
- ...span.errorInfo.category && { category: span.errorInfo.category }
211
- };
212
- }
213
- if (span.tags?.length) {
214
- for (const tag of span.tags) {
215
- traceProperties[tag] = true;
216
- }
217
- }
218
- const { userId, sessionId, ...customMetadata } = span.metadata ?? {};
219
- Object.assign(traceProperties, customMetadata);
220
- return {
221
- distinctId,
222
- event: "$ai_trace",
223
- properties: traceProperties,
224
- timestamp: new Date(endTime)
225
- };
226
- }
227
- buildChildEventMessage(args) {
228
- const { span, distinctId, endTime, traceData } = args;
229
- const eventName = this.mapToPostHogEvent(span.type);
230
- const startTime = span.startTime.getTime();
231
- const latency = (endTime - startTime) / 1e3;
232
- const parentIsRootSpan = this.isParentRootSpan(span, traceData);
233
- const properties = this.buildEventProperties(span, latency, parentIsRootSpan);
234
- return {
235
- distinctId,
236
- event: eventName,
237
- properties,
238
- timestamp: new Date(endTime)
239
- };
240
- }
241
- toDate(timestamp) {
242
- return timestamp instanceof Date ? timestamp : new Date(timestamp);
243
- }
244
- mapToPostHogEvent(spanType) {
245
- if (spanType == observability$1.SpanType.MODEL_GENERATION) {
246
- return "$ai_generation";
247
- }
248
- return "$ai_span";
249
- }
250
- getDistinctId(span, traceData) {
251
- if (span.metadata?.userId) {
252
- return String(span.metadata.userId);
253
- }
254
- if (traceData?.hasExtraValue(DISTINCT_ID)) {
255
- return String(traceData.getExtraValue(DISTINCT_ID));
256
- }
257
- if (this.config.defaultDistinctId) {
258
- return this.config.defaultDistinctId;
259
- }
260
- return "anonymous";
261
- }
262
- /**
263
- * Check if the parent of this span is the root span.
264
- * We need this because we don't create $ai_span for root spans,
265
- * so children of root spans should use $ai_trace_id as their $ai_parent_id.
266
- */
267
- isParentRootSpan(span, traceData) {
268
- if (!span.parentSpanId) {
269
- return false;
270
- }
271
- const parentCache = traceData.getSpan({ spanId: span.parentSpanId });
272
- if (parentCache) {
273
- return parentCache.isRootSpan;
274
- }
275
- return false;
276
- }
277
- buildEventProperties(span, latency, parentIsRootSpan = false) {
278
- const baseProperties = {
279
- $ai_trace_id: span.traceId,
280
- $ai_latency: latency,
281
- $ai_is_error: !!span.errorInfo
282
- };
283
- if (span.parentSpanId) {
284
- baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;
285
- }
286
- if (span.metadata?.sessionId) {
287
- baseProperties.$ai_session_id = span.metadata.sessionId;
288
- }
289
- if (span.isRootSpan && span.tags?.length) {
290
- for (const tag of span.tags) {
291
- baseProperties[tag] = true;
292
- }
293
- }
294
- if (span.type === observability$1.SpanType.MODEL_GENERATION) {
295
- baseProperties.$ai_generation_id = span.id;
296
- return { ...baseProperties, ...this.buildGenerationProperties(span) };
297
- } else {
298
- baseProperties.$ai_span_id = span.id;
299
- baseProperties.$ai_span_name = span.name;
300
- return { ...baseProperties, ...this.buildSpanProperties(span) };
301
- }
302
- }
303
- extractErrorProperties(errorInfo) {
304
- if (!errorInfo) {
305
- return {};
306
- }
307
- const props = {
308
- error_message: errorInfo.message
309
- };
310
- if (errorInfo.id) {
311
- props.error_id = errorInfo.id;
312
- }
313
- if (errorInfo.category) {
314
- props.error_category = errorInfo.category;
315
- }
316
- return props;
317
- }
318
- extractCustomMetadata(metadata) {
319
- const { userId, sessionId, ...customMetadata } = metadata ?? {};
320
- return customMetadata;
321
- }
322
- buildGenerationProperties(span) {
323
- const props = {};
324
- const attrs = span.attributes ?? {};
325
- props.$ai_model = attrs.model || "unknown-model";
326
- props.$ai_provider = attrs.provider || "unknown-provider";
327
- if (span.input) props.$ai_input = this.formatMessages(span.input, "user");
328
- if (span.output) props.$ai_output_choices = this.formatMessages(span.output, "assistant");
329
- Object.assign(props, formatUsageMetrics(attrs.usage));
330
- if (attrs.parameters) {
331
- if (attrs.parameters.temperature !== void 0) props.$ai_temperature = attrs.parameters.temperature;
332
- if (attrs.parameters.maxOutputTokens !== void 0) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;
333
- }
334
- if (attrs.streaming !== void 0) props.$ai_stream = attrs.streaming;
335
- return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };
336
- }
337
- buildSpanProperties(span) {
338
- const props = {};
339
- if (span.input) props.$ai_input_state = span.input;
340
- if (span.output) props.$ai_output_state = span.output;
341
- if (span.type === observability$1.SpanType.MODEL_CHUNK) {
342
- const attrs = span.attributes;
343
- if (attrs?.chunkType) props.chunk_type = attrs.chunkType;
344
- if (attrs?.sequenceNumber !== void 0) props.chunk_sequence_number = attrs.sequenceNumber;
345
- }
346
- if (span.attributes) {
347
- Object.assign(props, span.attributes);
348
- }
349
- return { ...props, ...this.extractErrorProperties(span.errorInfo), ...this.extractCustomMetadata(span.metadata) };
350
- }
351
- formatMessages(data, defaultRole = "user") {
352
- if (typeof data === "object" && data !== null && !Array.isArray(data) && "messages" in data) {
353
- const wrapped = data.messages;
354
- if (this.isMessageArray(wrapped)) {
355
- return wrapped.map((msg) => this.normalizeMessage(msg));
356
- }
357
- }
358
- if (this.isMessageArray(data)) {
359
- return data.map((msg) => this.normalizeMessage(msg));
360
- }
361
- if (typeof data === "string") {
362
- return [{ role: defaultRole, content: [{ type: "text", text: data }] }];
363
- }
364
- if (this.isSpanOutputWithToolCalls(data)) {
365
- const content = [];
366
- if (data.text) {
367
- content.push({ type: "text", text: data.text });
368
- }
369
- for (const tc of data.toolCalls) {
370
- content.push({
371
- type: "tool-call",
372
- id: tc.toolCallId,
373
- function: { name: tc.toolName, arguments: tc.args }
374
- });
375
- }
376
- return [{ role: "assistant", content }];
377
- }
378
- if (typeof data === "object" && data !== null && !Array.isArray(data) && "text" in data) {
379
- const text = data.text;
380
- if (typeof text === "string") {
381
- return [{ role: defaultRole, content: [{ type: "text", text }] }];
382
- }
383
- }
384
- return [{ role: defaultRole, content: [{ type: "text", text: this.safeStringify(data) }] }];
385
- }
386
- isSpanOutputWithToolCalls(data) {
387
- if (typeof data !== "object" || data === null || !("toolCalls" in data)) return false;
388
- const { toolCalls } = data;
389
- return Array.isArray(toolCalls) && toolCalls.length > 0;
390
- }
391
- isMessageArray(data) {
392
- if (!Array.isArray(data)) {
393
- return false;
394
- }
395
- return data.every((item) => typeof item === "object" && item !== null && "role" in item && "content" in item);
396
- }
397
- normalizeMessage(msg) {
398
- if (typeof msg.content === "string") {
399
- return {
400
- role: msg.role,
401
- content: [{ type: "text", text: msg.content }]
402
- };
403
- }
404
- return {
405
- role: msg.role,
406
- content: msg.content
407
- };
408
- }
409
- safeStringify(data) {
410
- try {
411
- return JSON.stringify(data);
412
- } catch {
413
- if (typeof data === "object" && data !== null) {
414
- return `[Non-serializable ${data.constructor?.name || "Object"}]`;
415
- }
416
- return String(data);
417
- }
418
- }
419
- /**
420
- * Force flush any buffered data to PostHog without shutting down.
421
- */
422
- async _flush() {
423
- if (this.#client) {
424
- await this.#client.flush();
425
- }
426
- }
427
- async _postShutdown() {
428
- if (this.#client) {
429
- await this.#client.shutdown();
430
- }
431
- }
25
+ const DISTINCT_ID = "distinctId";
26
+ var PosthogExporter = class PosthogExporter extends _mastra_observability.TrackingExporter {
27
+ name = "posthog";
28
+ #client;
29
+ static SERVERLESS_FLUSH_AT = 10;
30
+ static SERVERLESS_FLUSH_INTERVAL = 2e3;
31
+ static DEFAULT_FLUSH_AT = 20;
32
+ static DEFAULT_FLUSH_INTERVAL = 1e4;
33
+ constructor(config = {}) {
34
+ const apiKey = config.apiKey ?? process.env.POSTHOG_API_KEY;
35
+ super({
36
+ ...config,
37
+ apiKey
38
+ });
39
+ if (!apiKey) {
40
+ this.setDisabled("Missing required API key. Set POSTHOG_API_KEY environment variable or pass apiKey in config.");
41
+ return;
42
+ }
43
+ const clientConfig = this.buildClientConfig(this.config);
44
+ this.#client = new posthog_node.PostHog(apiKey, clientConfig);
45
+ const message = config.serverless ?? false ? "PostHog exporter initialized in serverless mode" : "PostHog exporter initialized";
46
+ this.logger.debug(message, config);
47
+ }
48
+ buildClientConfig(config) {
49
+ const isServerless = config.serverless ?? false;
50
+ const flushAt = config.flushAt ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_AT : PosthogExporter.DEFAULT_FLUSH_AT);
51
+ const flushInterval = config.flushInterval ?? (isServerless ? PosthogExporter.SERVERLESS_FLUSH_INTERVAL : PosthogExporter.DEFAULT_FLUSH_INTERVAL);
52
+ const host = config.host || process.env.POSTHOG_HOST || "https://us.i.posthog.com";
53
+ if (!config.host && !process.env.POSTHOG_HOST) this.logger.info("No PostHog host specified, using US default (https://us.i.posthog.com). For EU region, set `host: \"https://eu.i.posthog.com\"` in config or POSTHOG_HOST env var. For self-hosted, provide your instance URL.");
54
+ return {
55
+ host,
56
+ flushAt,
57
+ flushInterval,
58
+ privacyMode: config.enablePrivacyMode
59
+ };
60
+ }
61
+ skipBuildRootTask = true;
62
+ async _buildRoot(_args) {
63
+ throw new Error("Method not implemented.");
64
+ }
65
+ skipCachingEventSpans = true;
66
+ async _buildEvent(args) {
67
+ const { span, traceData } = args;
68
+ const eventName = this.mapToPostHogEvent(span.type);
69
+ const distinctId = this.getDistinctId(span, traceData);
70
+ const properties = this.buildEventProperties(span, 0);
71
+ this.#client?.capture(this.withGroups({
72
+ distinctId,
73
+ event: eventName,
74
+ properties,
75
+ timestamp: span.endTime ? new Date(span.endTime) : /* @__PURE__ */ new Date()
76
+ }));
77
+ return true;
78
+ }
79
+ async _buildSpan(args) {
80
+ const { span, traceData } = args;
81
+ if (!traceData.hasExtraValue(DISTINCT_ID)) {
82
+ const userId = span.metadata?.userId;
83
+ if (userId) traceData.setExtraValue(DISTINCT_ID, String(userId));
84
+ }
85
+ return span;
86
+ }
87
+ skipSpanUpdateEvents = true;
88
+ _updateSpan(_args) {
89
+ throw new Error("Method not implemented.");
90
+ }
91
+ async _finishSpan(args) {
92
+ const { span, traceData } = args;
93
+ const cachedSpan = traceData.getSpan({ spanId: span.id });
94
+ const mergedSpan = !span.input && cachedSpan?.input ? {
95
+ ...span,
96
+ input: cachedSpan.input
97
+ } : span;
98
+ const eventMessage = this.buildEventMessage({
99
+ span: mergedSpan,
100
+ traceData
101
+ });
102
+ this.#client?.capture(this.withGroups(eventMessage));
103
+ }
104
+ async _abortSpan(args) {
105
+ const { span, reason, traceData } = args;
106
+ span.errorInfo = reason;
107
+ const eventMessage = this.buildEventMessage({
108
+ span,
109
+ traceData
110
+ });
111
+ this.#client?.capture(this.withGroups(eventMessage));
112
+ }
113
+ /**
114
+ * Forward feedback recorded via `addFeedback()` to PostHog as a native
115
+ * `$ai_feedback` event, shown as "User feedback" on the linked trace in
116
+ * PostHog's AI observability UI.
117
+ */
118
+ async onFeedbackEvent(event) {
119
+ if (!this.#client) return;
120
+ const { feedback } = event;
121
+ if (!feedback.traceId) {
122
+ this.logger.debug("PostHog exporter: dropping feedback with no traceId; PostHog requires $ai_trace_id", { feedbackId: feedback.feedbackId });
123
+ return;
124
+ }
125
+ const properties = {
126
+ ...this.extractCustomMetadata(feedback.metadata),
127
+ $ai_trace_id: feedback.traceId,
128
+ $ai_feedback_text: feedback.comment ?? String(feedback.value),
129
+ feedback_id: feedback.feedbackId,
130
+ feedback_type: feedback.feedbackType,
131
+ feedback_value: feedback.value
132
+ };
133
+ const feedbackSource = feedback.feedbackSource ?? feedback.source;
134
+ if (feedbackSource) properties.feedback_source = feedbackSource;
135
+ if (feedback.spanId) properties.span_id = feedback.spanId;
136
+ if (feedback.sourceId) properties.source_id = feedback.sourceId;
137
+ if (feedback.metadata?.sessionId) properties.$ai_session_id = feedback.metadata.sessionId;
138
+ try {
139
+ this.#client.capture(this.withGroups({
140
+ distinctId: this.getFeedbackDistinctId(feedback),
141
+ event: "$ai_feedback",
142
+ properties,
143
+ timestamp: new Date(feedback.timestamp)
144
+ }));
145
+ } catch (err) {
146
+ this.logger.error("PostHog exporter: failed to submit feedback", {
147
+ error: err,
148
+ traceId: feedback.traceId,
149
+ feedbackId: feedback.feedbackId
150
+ });
151
+ }
152
+ }
153
+ getFeedbackDistinctId(feedback) {
154
+ const userId = feedback.feedbackUserId ?? feedback.userId ?? feedback.metadata?.userId;
155
+ if (userId) return String(userId);
156
+ return this.config.defaultDistinctId ?? "anonymous";
157
+ }
158
+ /**
159
+ * PostHog group analytics are keyed off the top-level `groups` field on the
160
+ * capture call. The Node SDK derives the event's `$groups` from that field and
161
+ * overwrites any property-level `$groups`, so group metadata carried in
162
+ * properties is dropped unless it is mirrored here.
163
+ */
164
+ withGroups(message) {
165
+ const groups = message.properties?.$groups;
166
+ if (groups && typeof groups === "object" && !Array.isArray(groups)) return {
167
+ ...message,
168
+ groups
169
+ };
170
+ return message;
171
+ }
172
+ buildEventMessage(args) {
173
+ const { span, traceData } = args;
174
+ const endTime = span.endTime ? this.toDate(span.endTime).getTime() : Date.now();
175
+ const distinctId = this.getDistinctId(span, traceData);
176
+ if (span.isRootSpan) return this.buildRootEventMessage({
177
+ span,
178
+ distinctId,
179
+ endTime
180
+ });
181
+ else return this.buildChildEventMessage({
182
+ span,
183
+ distinctId,
184
+ endTime,
185
+ traceData
186
+ });
187
+ }
188
+ /**
189
+ * Capture an explicit $ai_trace event for root spans.
190
+ * This gives us control over trace-level metadata like name and tags,
191
+ * rather than relying on PostHog's pseudo-trace auto-creation.
192
+ */
193
+ buildRootEventMessage(args) {
194
+ const { span, distinctId, endTime } = args;
195
+ const traceProperties = {
196
+ $ai_trace_id: span.traceId,
197
+ $ai_span_name: span.name,
198
+ $ai_is_error: !!span.errorInfo
199
+ };
200
+ if (span.metadata?.sessionId) traceProperties.$ai_session_id = span.metadata.sessionId;
201
+ if (span.input) traceProperties.$ai_input_state = span.input;
202
+ if (span.output) traceProperties.$ai_output_state = span.output;
203
+ if (span.errorInfo) traceProperties.$ai_error = {
204
+ message: span.errorInfo.message,
205
+ ...span.errorInfo.id && { id: span.errorInfo.id },
206
+ ...span.errorInfo.category && { category: span.errorInfo.category }
207
+ };
208
+ if (span.tags?.length) for (const tag of span.tags) traceProperties[tag] = true;
209
+ const { userId, sessionId, ...customMetadata } = span.metadata ?? {};
210
+ Object.assign(traceProperties, customMetadata);
211
+ return {
212
+ distinctId,
213
+ event: "$ai_trace",
214
+ properties: traceProperties,
215
+ timestamp: new Date(endTime)
216
+ };
217
+ }
218
+ buildChildEventMessage(args) {
219
+ const { span, distinctId, endTime, traceData } = args;
220
+ const eventName = this.mapToPostHogEvent(span.type);
221
+ const latency = (endTime - span.startTime.getTime()) / 1e3;
222
+ const parentIsRootSpan = this.isParentRootSpan(span, traceData);
223
+ return {
224
+ distinctId,
225
+ event: eventName,
226
+ properties: this.buildEventProperties(span, latency, parentIsRootSpan),
227
+ timestamp: new Date(endTime)
228
+ };
229
+ }
230
+ toDate(timestamp) {
231
+ return timestamp instanceof Date ? timestamp : new Date(timestamp);
232
+ }
233
+ mapToPostHogEvent(spanType) {
234
+ if (spanType == _mastra_core_observability.SpanType.MODEL_GENERATION) return "$ai_generation";
235
+ return "$ai_span";
236
+ }
237
+ getDistinctId(span, traceData) {
238
+ if (span.metadata?.userId) return String(span.metadata.userId);
239
+ if (traceData?.hasExtraValue(DISTINCT_ID)) return String(traceData.getExtraValue(DISTINCT_ID));
240
+ if (this.config.defaultDistinctId) return this.config.defaultDistinctId;
241
+ return "anonymous";
242
+ }
243
+ /**
244
+ * Check if the parent of this span is the root span.
245
+ * We need this because we don't create $ai_span for root spans,
246
+ * so children of root spans should use $ai_trace_id as their $ai_parent_id.
247
+ */
248
+ isParentRootSpan(span, traceData) {
249
+ if (!span.parentSpanId) return false;
250
+ const parentCache = traceData.getSpan({ spanId: span.parentSpanId });
251
+ if (parentCache) return parentCache.isRootSpan;
252
+ return false;
253
+ }
254
+ buildEventProperties(span, latency, parentIsRootSpan = false) {
255
+ const baseProperties = {
256
+ $ai_trace_id: span.traceId,
257
+ $ai_latency: latency,
258
+ $ai_is_error: !!span.errorInfo
259
+ };
260
+ if (span.parentSpanId) baseProperties.$ai_parent_id = parentIsRootSpan ? span.traceId : span.parentSpanId;
261
+ if (span.metadata?.sessionId) baseProperties.$ai_session_id = span.metadata.sessionId;
262
+ if (span.isRootSpan && span.tags?.length) for (const tag of span.tags) baseProperties[tag] = true;
263
+ if (span.type === _mastra_core_observability.SpanType.MODEL_GENERATION) {
264
+ baseProperties.$ai_generation_id = span.id;
265
+ return {
266
+ ...baseProperties,
267
+ ...this.buildGenerationProperties(span)
268
+ };
269
+ } else {
270
+ baseProperties.$ai_span_id = span.id;
271
+ baseProperties.$ai_span_name = span.name;
272
+ return {
273
+ ...baseProperties,
274
+ ...this.buildSpanProperties(span)
275
+ };
276
+ }
277
+ }
278
+ extractErrorProperties(errorInfo) {
279
+ if (!errorInfo) return {};
280
+ const props = { error_message: errorInfo.message };
281
+ if (errorInfo.id) props.error_id = errorInfo.id;
282
+ if (errorInfo.category) props.error_category = errorInfo.category;
283
+ return props;
284
+ }
285
+ extractCustomMetadata(metadata) {
286
+ const { userId, sessionId, ...customMetadata } = metadata ?? {};
287
+ return customMetadata;
288
+ }
289
+ buildGenerationProperties(span) {
290
+ const props = {};
291
+ const attrs = span.attributes ?? {};
292
+ props.$ai_model = attrs.model || "unknown-model";
293
+ props.$ai_provider = attrs.provider || "unknown-provider";
294
+ if (span.input) props.$ai_input = this.formatMessages(span.input, "user");
295
+ if (span.output) props.$ai_output_choices = this.formatMessages(span.output, "assistant");
296
+ Object.assign(props, formatUsageMetrics(attrs.usage));
297
+ if (attrs.parameters) {
298
+ if (attrs.parameters.temperature !== void 0) props.$ai_temperature = attrs.parameters.temperature;
299
+ if (attrs.parameters.maxOutputTokens !== void 0) props.$ai_max_tokens = attrs.parameters.maxOutputTokens;
300
+ }
301
+ if (attrs.streaming !== void 0) props.$ai_stream = attrs.streaming;
302
+ return {
303
+ ...props,
304
+ ...this.extractErrorProperties(span.errorInfo),
305
+ ...this.extractCustomMetadata(span.metadata)
306
+ };
307
+ }
308
+ buildSpanProperties(span) {
309
+ const props = {};
310
+ if (span.input) props.$ai_input_state = span.input;
311
+ if (span.output) props.$ai_output_state = span.output;
312
+ if (span.type === _mastra_core_observability.SpanType.MODEL_CHUNK) {
313
+ const attrs = span.attributes;
314
+ if (attrs?.chunkType) props.chunk_type = attrs.chunkType;
315
+ if (attrs?.sequenceNumber !== void 0) props.chunk_sequence_number = attrs.sequenceNumber;
316
+ }
317
+ if (span.attributes) Object.assign(props, span.attributes);
318
+ return {
319
+ ...props,
320
+ ...this.extractErrorProperties(span.errorInfo),
321
+ ...this.extractCustomMetadata(span.metadata)
322
+ };
323
+ }
324
+ formatMessages(data, defaultRole = "user") {
325
+ if (typeof data === "object" && data !== null && !Array.isArray(data) && "messages" in data) {
326
+ const wrapped = data.messages;
327
+ if (this.isMessageArray(wrapped)) return wrapped.map((msg) => this.normalizeMessage(msg));
328
+ }
329
+ if (this.isMessageArray(data)) return data.map((msg) => this.normalizeMessage(msg));
330
+ if (typeof data === "string") return [{
331
+ role: defaultRole,
332
+ content: [{
333
+ type: "text",
334
+ text: data
335
+ }]
336
+ }];
337
+ if (this.isSpanOutputWithToolCalls(data)) {
338
+ const content = [];
339
+ if (data.text) content.push({
340
+ type: "text",
341
+ text: data.text
342
+ });
343
+ for (const tc of data.toolCalls) content.push({
344
+ type: "tool-call",
345
+ id: tc.toolCallId,
346
+ function: {
347
+ name: tc.toolName,
348
+ arguments: tc.args
349
+ }
350
+ });
351
+ return [{
352
+ role: "assistant",
353
+ content
354
+ }];
355
+ }
356
+ if (typeof data === "object" && data !== null && !Array.isArray(data) && "text" in data) {
357
+ const text = data.text;
358
+ if (typeof text === "string") return [{
359
+ role: defaultRole,
360
+ content: [{
361
+ type: "text",
362
+ text
363
+ }]
364
+ }];
365
+ }
366
+ return [{
367
+ role: defaultRole,
368
+ content: [{
369
+ type: "text",
370
+ text: this.safeStringify(data)
371
+ }]
372
+ }];
373
+ }
374
+ isSpanOutputWithToolCalls(data) {
375
+ if (typeof data !== "object" || data === null || !("toolCalls" in data)) return false;
376
+ const { toolCalls } = data;
377
+ return Array.isArray(toolCalls) && toolCalls.length > 0;
378
+ }
379
+ isMessageArray(data) {
380
+ if (!Array.isArray(data)) return false;
381
+ return data.every((item) => typeof item === "object" && item !== null && "role" in item && "content" in item);
382
+ }
383
+ normalizeMessage(msg) {
384
+ if (typeof msg.content === "string") return {
385
+ role: msg.role,
386
+ content: [{
387
+ type: "text",
388
+ text: msg.content
389
+ }]
390
+ };
391
+ return {
392
+ role: msg.role,
393
+ content: msg.content
394
+ };
395
+ }
396
+ safeStringify(data) {
397
+ try {
398
+ return JSON.stringify(data);
399
+ } catch {
400
+ if (typeof data === "object" && data !== null) return `[Non-serializable ${data.constructor?.name || "Object"}]`;
401
+ return String(data);
402
+ }
403
+ }
404
+ /**
405
+ * Force flush any buffered data to PostHog without shutting down.
406
+ */
407
+ async _flush() {
408
+ if (this.#client) await this.#client.flush();
409
+ }
410
+ async _postShutdown() {
411
+ if (this.#client) await this.#client.shutdown();
412
+ }
432
413
  };
433
-
414
+ //#endregion
434
415
  exports.PosthogExporter = PosthogExporter;
435
416
  exports.formatUsageMetrics = formatUsageMetrics;
436
- //# sourceMappingURL=index.cjs.map
417
+
437
418
  //# sourceMappingURL=index.cjs.map