@posthog/ai 8.17.2 → 8.18.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,1002 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let uuid = require("uuid");
3
+ let _posthog_core = require("@posthog/core");
4
+ //#region package.json
5
+ var version = "8.18.0";
6
+ //#endregion
7
+ //#region src/captureAiEvent.ts
8
+ /** @internal */
9
+ function isFullAiCaptureEnabled(client) {
10
+ return client?.enableFullAiCapture === true;
11
+ }
12
+ /** @internal */
13
+ function captureAiEvent(client, event) {
14
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === "function") {
15
+ client.captureAi(event);
16
+ return;
17
+ }
18
+ client.capture(event);
19
+ }
20
+ /** @internal */
21
+ async function captureAiEventImmediate(client, event) {
22
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === "function") {
23
+ await client.captureAiImmediate(event);
24
+ return;
25
+ }
26
+ await client.captureImmediate(event);
27
+ }
28
+ //#endregion
29
+ //#region src/typeGuards.ts
30
+ const isObject = (value) => {
31
+ return value !== null && typeof value === "object" && !Array.isArray(value);
32
+ };
33
+ //#endregion
34
+ //#region src/sanitization/base64_recognizer.ts
35
+ const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
36
+ const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
37
+ var Base64Recognizer = class {
38
+ recognize(value, minLength) {
39
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
40
+ if (dataUrl) return {
41
+ kind: "data-url",
42
+ mediaType: dataUrl[1]
43
+ };
44
+ if (value.length < minLength) return { kind: "none" };
45
+ const confidencePrefix = value.slice(0, minLength);
46
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) return { kind: "raw" };
47
+ else return { kind: "none" };
48
+ }
49
+ };
50
+ //#endregion
51
+ //#region src/sanitization/media_type_context.ts
52
+ const MIME_HINT_KEYS = [
53
+ "mediaType",
54
+ "media_type",
55
+ "mimeType",
56
+ "mime_type"
57
+ ];
58
+ const STRONG_CONTEXT_KEYS = /* @__PURE__ */ new Set([
59
+ "data",
60
+ "file_data",
61
+ "fileData",
62
+ "image_url",
63
+ "imageUrl",
64
+ "video_url",
65
+ "videoUrl",
66
+ "audio",
67
+ "audio_data",
68
+ "audioData",
69
+ "inline_data",
70
+ "inlineData",
71
+ "source",
72
+ "result"
73
+ ]);
74
+ const STRONG_CONTEXT_TYPES = /* @__PURE__ */ new Set([
75
+ "image",
76
+ "image_url",
77
+ "input_image",
78
+ "audio",
79
+ "input_audio",
80
+ "video",
81
+ "video_url",
82
+ "file",
83
+ "input_file",
84
+ "document",
85
+ "media",
86
+ "file-data"
87
+ ]);
88
+ const FILE_FAMILY_TYPES = /* @__PURE__ */ new Set([
89
+ "file",
90
+ "input_file",
91
+ "document",
92
+ "media",
93
+ "file-data"
94
+ ]);
95
+ const TYPED_BINARY_DATA_TYPES = /* @__PURE__ */ new Set([
96
+ "image",
97
+ "audio",
98
+ "video",
99
+ "document"
100
+ ]);
101
+ const KNOWN_AUDIO_FORMATS = /* @__PURE__ */ new Set([
102
+ "wav",
103
+ "mp3",
104
+ "ogg",
105
+ "flac",
106
+ "m4a",
107
+ "aac",
108
+ "webm"
109
+ ]);
110
+ var MediaTypeContext = class MediaTypeContext {
111
+ static {
112
+ this.EMPTY = new MediaTypeContext(void 0, void 0);
113
+ }
114
+ constructor(parent, key, explicitMediaType) {
115
+ this.parent = parent;
116
+ this.key = key;
117
+ this.explicitMediaType = explicitMediaType;
118
+ }
119
+ inferMediaType() {
120
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
121
+ }
122
+ inferFromSiblingMime() {
123
+ if (this.explicitMediaType) return this.explicitMediaType;
124
+ if (!this.parent) return void 0;
125
+ for (const hint of MIME_HINT_KEYS) {
126
+ const v = this.parent[hint];
127
+ if (typeof v === "string") return v;
128
+ }
129
+ }
130
+ inferFromSiblingFormat() {
131
+ if (!this.parent) return void 0;
132
+ const fmt = this.parent.format;
133
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return `audio/${fmt.toLowerCase()}`;
134
+ }
135
+ inferFromParentType() {
136
+ if (!this.parent) return void 0;
137
+ const t = this.parent.type;
138
+ if (typeof t !== "string") return void 0;
139
+ if (t === "image" || t === "image_url" || t === "input_image") return "image";
140
+ if (t === "audio" || t === "input_audio") return "audio";
141
+ if (t === "video" || t === "video_url") return "video";
142
+ if (FILE_FAMILY_TYPES.has(t)) return "application/octet-stream";
143
+ }
144
+ inferFromKey() {
145
+ if (!this.key) return void 0;
146
+ const key = this.key.toLowerCase();
147
+ if (key.includes("audio")) return "audio";
148
+ if (key.includes("video")) return "video";
149
+ if (key.includes("image")) return "image";
150
+ if (key.includes("file") || key.includes("document")) return "application/octet-stream";
151
+ }
152
+ hasDefiniteBinaryData() {
153
+ if (this.parent && this.key === "data" && typeof this.parent.type === "string" && TYPED_BINARY_DATA_TYPES.has(this.parent.type)) return true;
154
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
155
+ const mediaType = this.inferFromSiblingMime();
156
+ return mediaType !== void 0 && !mediaType.toLowerCase().startsWith("text/");
157
+ }
158
+ signalsBinary() {
159
+ if (this.explicitMediaType) return true;
160
+ if (this.parent) {
161
+ for (const hint of MIME_HINT_KEYS) if (typeof this.parent[hint] === "string") return true;
162
+ const fmt = this.parent.format;
163
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
164
+ const t = this.parent.type;
165
+ if (typeof t === "string" && STRONG_CONTEXT_TYPES.has(t)) return true;
166
+ }
167
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
168
+ return false;
169
+ }
170
+ };
171
+ //#endregion
172
+ //#region src/sanitization/binary_content_redactor.ts
173
+ const STRONG_CONTEXT_MIN_LENGTH = 64;
174
+ const WEAK_CONTEXT_MIN_LENGTH = 1024;
175
+ var BinaryContentRedactor = class {
176
+ constructor(recognizer = new Base64Recognizer()) {
177
+ this.recognizer = recognizer;
178
+ this.visited = /* @__PURE__ */ new WeakSet();
179
+ }
180
+ redact(value, mediaType) {
181
+ this.visited = /* @__PURE__ */ new WeakSet();
182
+ return this.walk(value, mediaType ? new MediaTypeContext(void 0, void 0, mediaType) : MediaTypeContext.EMPTY);
183
+ }
184
+ /** Redact one leaf while a caller owns bounded, type-preserving traversal. @internal */
185
+ redactLeaf(value, context) {
186
+ return typeof value === "string" ? this.redactString(value, context) : this.placeholderFor(context.inferMediaType());
187
+ }
188
+ walk(value, ctx) {
189
+ if (value === null || value === void 0) return value;
190
+ if (typeof value === "string") return this.redactLeaf(value, ctx);
191
+ if (typeof value !== "object") return value;
192
+ if (typeof Uint8Array !== "undefined" && value instanceof Uint8Array) return this.redactLeaf(value, ctx);
193
+ if (this.visited.has(value)) return null;
194
+ this.visited.add(value);
195
+ if (Array.isArray(value)) return value.map((item) => this.walk(item, ctx));
196
+ const obj = value;
197
+ const out = {};
198
+ for (const k of Object.keys(obj)) Object.defineProperty(out, k, {
199
+ value: this.walk(obj[k], new MediaTypeContext(obj, k)),
200
+ enumerable: true,
201
+ writable: true,
202
+ configurable: true
203
+ });
204
+ return out;
205
+ }
206
+ redactString(value, ctx) {
207
+ const hasDefiniteBinaryData = ctx.hasDefiniteBinaryData();
208
+ const recognitionValue = hasDefiniteBinaryData ? value.replace(/[\r\n]/g, "") : value;
209
+ const minLength = hasDefiniteBinaryData ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
210
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
211
+ switch (recognition.kind) {
212
+ case "data-url": return this.placeholderFor(recognition.mediaType);
213
+ case "raw": return this.placeholderFor(ctx.inferMediaType());
214
+ case "none": return value;
215
+ }
216
+ }
217
+ placeholderFor(mediaType) {
218
+ if (!mediaType) return "[base64 redacted]";
219
+ if (mediaType === "application/octet-stream") return "[base64 file redacted]";
220
+ return `[base64 ${mediaType} redacted]`;
221
+ }
222
+ };
223
+ //#endregion
224
+ //#region src/sanitization.ts
225
+ const redactor$1 = new BinaryContentRedactor();
226
+ function redactBase64DataUrl(str, mediaType) {
227
+ return redactor$1.redact(str, mediaType);
228
+ }
229
+ //#endregion
230
+ //#region src/privacyMode.ts
231
+ /** Per-call settings can turn redaction on but never off. */
232
+ const isPrivacyModeEnabled = (client, privacyMode = false) => privacyMode === true || client?.privacyMode === true || client?.privacy_mode === true || client?.options?.privacyMode === true;
233
+ //#endregion
234
+ //#region src/utils.ts
235
+ const TOKEN_PROPERTY_KEYS = /* @__PURE__ */ new Set([
236
+ "$ai_input_tokens",
237
+ "$ai_output_tokens",
238
+ "$ai_cache_read_input_tokens",
239
+ "$ai_cache_creation_input_tokens",
240
+ "$ai_total_tokens",
241
+ "$ai_reasoning_tokens"
242
+ ]);
243
+ /**
244
+ * Whether the caller supplied their own token counts, which override the ones the SDK
245
+ * derived from the provider response.
246
+ */
247
+ function hasTokenOverrides(posthogProperties) {
248
+ return !!posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key));
249
+ }
250
+ function getTokensSource(posthogProperties) {
251
+ return hasTokenOverrides(posthogProperties) ? "passthrough" : "sdk";
252
+ }
253
+ const MAX_OUTPUT_SIZE = 2e5;
254
+ const STRING_FORMAT = "utf8";
255
+ const sharedTextEncoder = new TextEncoder();
256
+ const sharedTextDecoder = new TextDecoder(STRING_FORMAT, { fatal: false });
257
+ const buildInlineDataBlock = (mimeType, data) => {
258
+ if (mimeType.startsWith("audio/")) return {
259
+ type: "audio",
260
+ mime_type: mimeType,
261
+ data
262
+ };
263
+ if (mimeType.startsWith("image/")) return {
264
+ type: "image",
265
+ inline_data: {
266
+ mime_type: mimeType,
267
+ data
268
+ }
269
+ };
270
+ return {
271
+ type: "document",
272
+ inline_data: {
273
+ mime_type: mimeType,
274
+ data
275
+ }
276
+ };
277
+ };
278
+ const formatInlineDataBlock = (inlineData, client) => {
279
+ const mimeType = inlineData.mimeType || inlineData.mime_type || "application/octet-stream";
280
+ let data = inlineData.data;
281
+ if (data instanceof Uint8Array) {
282
+ if (typeof Buffer !== "undefined") data = Buffer.from(data).toString("base64");
283
+ else {
284
+ let binary = "";
285
+ for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]);
286
+ data = btoa(binary);
287
+ }
288
+ }
289
+ data = isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mimeType);
290
+ return buildInlineDataBlock(mimeType, String(data ?? ""));
291
+ };
292
+ function toSafeString(input) {
293
+ if (input === void 0 || input === null) return "";
294
+ if (typeof input === "string") return input;
295
+ try {
296
+ return JSON.stringify(input);
297
+ } catch {
298
+ console.warn("Failed to stringify input", input);
299
+ return "";
300
+ }
301
+ }
302
+ const truncate = (input, client, maxBytes = MAX_OUTPUT_SIZE) => {
303
+ const str = toSafeString(input);
304
+ if (str === "") return "";
305
+ if (isFullAiCaptureEnabled(client)) return str;
306
+ const buffer = sharedTextEncoder.encode(str);
307
+ if (buffer.length <= maxBytes) return sharedTextDecoder.decode(buffer);
308
+ const truncatedBuffer = buffer.slice(0, maxBytes);
309
+ let truncatedStr = sharedTextDecoder.decode(truncatedBuffer);
310
+ if (truncatedStr.endsWith("�")) truncatedStr = truncatedStr.slice(0, -1);
311
+ return `${truncatedStr}... [truncated]`;
312
+ };
313
+ function sanitizeValues(obj) {
314
+ if (obj === void 0 || obj === null) return obj;
315
+ const jsonSafe = JSON.parse(JSON.stringify(obj));
316
+ if (typeof jsonSafe === "string") return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
317
+ else if (Array.isArray(jsonSafe)) return jsonSafe.map(sanitizeValues);
318
+ else if (jsonSafe && typeof jsonSafe === "object") return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
319
+ return jsonSafe;
320
+ }
321
+ //#endregion
322
+ //#region src/serializeError.ts
323
+ const DEFAULT_MAX_DEPTH = 3;
324
+ const MAX_STACK_LINES = 20;
325
+ const OMITTED_FIELD = "requestBodyValues";
326
+ function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
327
+ if (depth < 0 || value === null || typeof value !== "object") return value;
328
+ if (value instanceof Error) {
329
+ const out = {
330
+ name: value.name,
331
+ message: value.message,
332
+ stack: truncateStack(value.stack)
333
+ };
334
+ for (const key of Object.keys(value)) if (key !== OMITTED_FIELD) out[key] = serializeError(Reflect.get(value, key), depth - 1);
335
+ if (value.cause !== void 0) out.cause = serializeError(value.cause, depth - 1);
336
+ return out;
337
+ }
338
+ if (Array.isArray(value)) return value.map((item) => serializeError(item, depth - 1));
339
+ return value;
340
+ }
341
+ const omitRequestBody = (key, value) => key === OMITTED_FIELD ? void 0 : value;
342
+ function stringifyError(error) {
343
+ try {
344
+ return JSON.stringify(sanitizeValues(serializeError(error)), omitRequestBody);
345
+ } catch {
346
+ if (error instanceof Error) return JSON.stringify({
347
+ name: error.name,
348
+ message: error.message
349
+ });
350
+ return JSON.stringify({ message: String(error) });
351
+ }
352
+ }
353
+ const CLASS_NAME = /^[A-Za-z_$][A-Za-z0-9_$.]{0,127}$/;
354
+ const PROVIDER_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
355
+ const PARAM = /^[A-Za-z0-9_.[\]-]{1,128}$/;
356
+ const REQUEST_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
357
+ const MAX_CAUSES = 3;
358
+ const readField = (value, key) => {
359
+ if (value === null || typeof value !== "object" && typeof value !== "function") return;
360
+ try {
361
+ return Reflect.get(value, key);
362
+ } catch {
363
+ return;
364
+ }
365
+ };
366
+ const matching = (value, grammar) => typeof value === "string" && grammar.test(value) ? value : void 0;
367
+ const firstMatching = (values, grammar) => {
368
+ for (const value of values) {
369
+ const match = matching(value, grammar);
370
+ if (match !== void 0) return match;
371
+ }
372
+ };
373
+ const className = (value) => {
374
+ if (!(value instanceof Error)) return "NonErrorThrown";
375
+ const name = readField(value, "name");
376
+ return firstMatching([
377
+ name === "Error" ? void 0 : name,
378
+ readField(readField(value, "constructor"), "name"),
379
+ name
380
+ ], CLASS_NAME) ?? "Error";
381
+ };
382
+ const httpStatus = (error) => {
383
+ for (const key of ["status", "statusCode"]) {
384
+ const value = readField(error, key);
385
+ if (typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599) return value;
386
+ }
387
+ };
388
+ const unwrapEnvelope = (body) => {
389
+ const inner = readField(body, "error");
390
+ return inner !== null && typeof inner === "object" ? inner : body;
391
+ };
392
+ const parsedResponseBody = (error) => {
393
+ const responseBody = readField(error, "responseBody");
394
+ if (typeof responseBody !== "string") return;
395
+ try {
396
+ return unwrapEnvelope(JSON.parse(responseBody));
397
+ } catch {
398
+ return;
399
+ }
400
+ };
401
+ const causeNames = (error) => {
402
+ const names = [];
403
+ let cause = readField(error, "cause");
404
+ while (cause !== void 0 && cause !== null && names.length < MAX_CAUSES) {
405
+ names.push(className(cause));
406
+ cause = readField(cause, "cause");
407
+ }
408
+ return names;
409
+ };
410
+ const describeError = (error) => {
411
+ const sources = [
412
+ error,
413
+ unwrapEnvelope(readField(error, "error")),
414
+ parsedResponseBody(error)
415
+ ];
416
+ const codes = /* @__PURE__ */ new Set();
417
+ for (const source of sources) for (const key of [
418
+ "code",
419
+ "type",
420
+ "status"
421
+ ]) {
422
+ const code = matching(readField(source, key), PROVIDER_CODE);
423
+ if (code !== void 0) codes.add(code);
424
+ }
425
+ const status = httpStatus(error);
426
+ const param = firstMatching(sources.map((source) => readField(source, "param")), PARAM);
427
+ const requestId = firstMatching([readField(error, "requestID"), readField(error, "request_id")], REQUEST_ID);
428
+ const causes = causeNames(error);
429
+ let summary = className(error);
430
+ if (status !== void 0) summary += `: ${status}`;
431
+ for (const code of codes) summary += ` ${code}`;
432
+ if (param !== void 0) summary += ` param=${param}`;
433
+ if (requestId !== void 0) summary += ` request_id=${requestId}`;
434
+ if (causes.length > 0) summary += ` (caused by ${causes.join(", ")})`;
435
+ return summary;
436
+ };
437
+ /**
438
+ * Describes an error for `$ai_error` without its message, stack, or arbitrary fields, which
439
+ * routinely echo the prompt. Only structured fields that pass a strict grammar are kept.
440
+ * The format matches the Python SDK.
441
+ */
442
+ function summarizeErrorForPrivacy(error) {
443
+ try {
444
+ return describeError(error);
445
+ } catch {
446
+ return error instanceof Error ? "Error" : "NonErrorThrown";
447
+ }
448
+ }
449
+ function formatAiError(error, privacyMode) {
450
+ return privacyMode ? summarizeErrorForPrivacy(error) : stringifyError(error);
451
+ }
452
+ function truncateStack(stack) {
453
+ if (!stack) return stack;
454
+ const lines = stack.split("\n");
455
+ if (lines.length <= MAX_STACK_LINES) return stack;
456
+ return [...lines.slice(0, MAX_STACK_LINES), "... (truncated)"].join("\n");
457
+ }
458
+ //#endregion
459
+ //#region src/mcpCredentials.ts
460
+ const MCP_CREDENTIAL_FIELDS = ["headers", "authorization"];
461
+ const MCP_PROVIDER_TOOL_IDS = /* @__PURE__ */ new Set([
462
+ "openai.mcp",
463
+ "xai.mcp",
464
+ "google.mcp_server"
465
+ ]);
466
+ const isMcpProviderTool = (tool) => (tool.type === "provider" || tool.type === "provider-defined") && typeof tool.id === "string" && MCP_PROVIDER_TOOL_IDS.has(tool.id);
467
+ const withoutMcpCredentials = (config) => {
468
+ const redacted = { ...config };
469
+ for (const field of MCP_CREDENTIAL_FIELDS) if (field in redacted) redacted[field] = "[REDACTED]";
470
+ return redacted;
471
+ };
472
+ /** Hides the credentials of a remote MCP tool definition; everything else is kept as sent. */
473
+ function redactMcpToolDefinition(tool) {
474
+ if (!isObject(tool)) return tool;
475
+ if (tool.type === "mcp") return withoutMcpCredentials(tool);
476
+ if (isMcpProviderTool(tool) && isObject(tool.args)) return {
477
+ ...tool,
478
+ args: withoutMcpCredentials(tool.args)
479
+ };
480
+ return tool;
481
+ }
482
+ function redactMcpToolCredentials(tools) {
483
+ return Array.isArray(tools) ? tools.map(redactMcpToolDefinition) : tools;
484
+ }
485
+ //#endregion
486
+ //#region src/gatewayWarning.ts
487
+ const POSTHOG_AI_GATEWAY_HOSTS = [
488
+ "gateway.posthog.com",
489
+ "gateway.us.posthog.com",
490
+ "gateway.eu.posthog.com",
491
+ "ai-gateway.us.posthog.com",
492
+ "ai-gateway.eu.posthog.com"
493
+ ];
494
+ const GATEWAY_DOCS_URL = "https://posthog.com/docs/ai-observability";
495
+ const extractHost = (baseURL) => {
496
+ try {
497
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
498
+ return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
499
+ } catch {
500
+ return;
501
+ }
502
+ };
503
+ const isPostHogAiGatewayUrl = (baseURL) => {
504
+ if (!baseURL) return false;
505
+ const host = extractHost(baseURL);
506
+ return host !== void 0 && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
507
+ };
508
+ const warnIfPostHogAiGateway = (baseURL) => {
509
+ if (!isPostHogAiGatewayUrl(baseURL)) return;
510
+ console.warn(`[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. Both capture \$ai_generation, so every call is double-counted and double-billed. Use one or the other — see ${GATEWAY_DOCS_URL}.`);
511
+ };
512
+ //#endregion
513
+ //#region src/captureAiGeneration.ts
514
+ /**
515
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
516
+ *
517
+ * This is the canonical primitive that every `@posthog/ai` wrapper
518
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
519
+ * external code can use it directly to instrument LLM calls made through
520
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
521
+ * same events the SDK wrappers produce.
522
+ *
523
+ * When `error` is set, the event is captured as an error. If the error is an
524
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
525
+ * so callers can re-throw the original error reference safely.
526
+ */
527
+ const captureAiGeneration = async (client, options) => {
528
+ try {
529
+ if (!client.capture) return;
530
+ warnIfPostHogAiGateway(options.baseURL);
531
+ const traceId = options.traceId ?? (0, uuid.v4)();
532
+ const eventType = options.eventType ?? "$ai_generation";
533
+ const privacyMode = options.privacyMode ?? false;
534
+ const usage = options.usage ?? {};
535
+ const shouldRedact = isPrivacyModeEnabled(client, privacyMode);
536
+ const safeInput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.input);
537
+ const safeOutput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.output);
538
+ let httpStatus = options.httpStatus;
539
+ let errorData = {};
540
+ if (options.error) {
541
+ if (httpStatus === void 0) {
542
+ if (typeof options.error === "object" && "status" in options.error && typeof options.error.status === "number") httpStatus = options.error.status;
543
+ else if (typeof options.error === "object" && "statusCode" in options.error && typeof options.error.statusCode === "number") httpStatus = options.error.statusCode;
544
+ else httpStatus = 500;
545
+ }
546
+ let exceptionId;
547
+ if (client.options?.enableExceptionAutocapture) {
548
+ if (!shouldRedact) {
549
+ exceptionId = (0, _posthog_core.uuidv7)();
550
+ client.captureException(options.error, void 0, { $ai_trace_id: traceId }, exceptionId);
551
+ }
552
+ if (typeof options.error === "object") options.error.__posthog_previously_captured_error = true;
553
+ }
554
+ errorData = {
555
+ $ai_is_error: true,
556
+ $ai_error: formatAiError(options.error, shouldRedact),
557
+ $exception_event_id: exceptionId
558
+ };
559
+ }
560
+ httpStatus = httpStatus ?? 200;
561
+ const costOverrideData = {};
562
+ if (options.costOverride) {
563
+ if (usage.inputTokens !== void 0) costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
564
+ if (usage.outputTokens !== void 0) costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
565
+ if (Object.keys(costOverrideData).length > 0) costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
566
+ }
567
+ const tokensOverridden = hasTokenOverrides(options.properties);
568
+ const additionalTokenValues = {
569
+ ...usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {},
570
+ ...usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {},
571
+ ...usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {},
572
+ ...usage.cacheReportingExclusive !== void 0 && !tokensOverridden ? { $ai_cache_reporting_exclusive: usage.cacheReportingExclusive } : {},
573
+ ...usage.webSearchCount ? { $ai_web_search_count: usage.webSearchCount } : {},
574
+ ...usage.rawUsage ? { $ai_usage: usage.rawUsage } : {}
575
+ };
576
+ const properties = {
577
+ $ai_lib: "posthog-ai",
578
+ $ai_lib_version: version,
579
+ $ai_provider: options.providerOverride ?? options.provider,
580
+ $ai_model: options.modelOverride ?? options.model,
581
+ $ai_model_parameters: options.modelParameters ?? {},
582
+ ...options.servedServiceTier != null ? { $ai_service_tier: options.servedServiceTier } : {},
583
+ $ai_input: safeInput,
584
+ $ai_output_choices: safeOutput,
585
+ $ai_http_status: httpStatus,
586
+ ...usage.inputTokens !== void 0 ? { $ai_input_tokens: usage.inputTokens } : {},
587
+ ...usage.outputTokens !== void 0 ? { $ai_output_tokens: usage.outputTokens } : {},
588
+ ...additionalTokenValues,
589
+ ...options.latency !== void 0 ? { $ai_latency: options.latency } : {},
590
+ ...options.timeToFirstToken !== void 0 ? { $ai_time_to_first_token: options.timeToFirstToken } : {},
591
+ $ai_trace_id: traceId,
592
+ ...options.baseURL === null ? {} : { $ai_base_url: options.baseURL ?? "" },
593
+ ...options.properties,
594
+ $ai_tokens_source: getTokensSource(options.properties),
595
+ ...options.distinctId ? {} : { $process_person_profile: false },
596
+ ...options.stopReason ? { $ai_stop_reason: options.stopReason } : {},
597
+ ...options.tools ? { $ai_tools: redactMcpToolCredentials(options.tools) } : {},
598
+ ...options.completionId ? { $ai_completion_id: options.completionId } : {},
599
+ ...options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? { $ai_provider_metadata: options.providerMetadata } : {},
600
+ ...errorData,
601
+ ...costOverrideData
602
+ };
603
+ const event = {
604
+ distinctId: options.distinctId ?? traceId,
605
+ event: eventType,
606
+ properties,
607
+ groups: options.groups
608
+ };
609
+ if (options.captureImmediate) await captureAiEventImmediate(client, event);
610
+ else captureAiEvent(client, event);
611
+ } catch (error) {
612
+ try {
613
+ options.onError?.(error);
614
+ } catch {}
615
+ console.warn("[PostHog AI] Failed to capture generation telemetry:", error);
616
+ }
617
+ };
618
+ //#endregion
619
+ //#region src/toolResult.ts
620
+ const MAX_DEPTH = 20;
621
+ const MAX_ITEMS = 1e3;
622
+ const MAX_NODES = 1e4;
623
+ const TRUNCATED = "[Truncated]";
624
+ const UNSERIALIZABLE = "[Unserializable]";
625
+ const redactor = new BinaryContentRedactor();
626
+ const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
627
+ /** @internal Preserve tool-result structure while bounding each string value. */
628
+ function formatToolResult(content, client, maxBytes = 5e3) {
629
+ try {
630
+ if (isFullAiCaptureEnabled(client)) return (0, _posthog_core.toJsonSafeValue)(content);
631
+ const ancestors = /* @__PURE__ */ new WeakSet();
632
+ let remainingNodes = MAX_NODES;
633
+ const convert = (value, depth, context) => {
634
+ if (remainingNodes <= 0) return TRUNCATED;
635
+ remainingNodes--;
636
+ try {
637
+ if (value === null || typeof value !== "object") {
638
+ const safe = (0, _posthog_core.toJsonSafeValue)(value);
639
+ return typeof safe === "string" ? truncate(redactor.redactLeaf(safe, context), void 0, maxBytes) : safe;
640
+ }
641
+ if (depth >= MAX_DEPTH) return TRUNCATED;
642
+ if (typeof Uint8Array !== "undefined" && value instanceof Uint8Array) return redactor.redactLeaf(value, context);
643
+ if (ancestors.has(value)) return "[Circular]";
644
+ ancestors.add(value);
645
+ try {
646
+ if (value instanceof Date) {
647
+ const safe = (0, _posthog_core.toJsonSafeValue)(value);
648
+ return typeof safe === "string" ? truncate(redactor.redactLeaf(safe, context), void 0, maxBytes) : safe;
649
+ }
650
+ let hasToJSONResult = false;
651
+ let toJSONResult;
652
+ try {
653
+ const toJSON = value.toJSON;
654
+ if (typeof toJSON === "function") {
655
+ toJSONResult = toJSON.call(value);
656
+ hasToJSONResult = true;
657
+ }
658
+ } catch {}
659
+ if (hasToJSONResult) return convert(toJSONResult, depth + 1, context);
660
+ if (Array.isArray(value)) {
661
+ const output = [];
662
+ const count = Math.min(value.length, MAX_ITEMS);
663
+ let index = 0;
664
+ for (; index < count && remainingNodes > 0; index++) output.push(convert(value[index], depth + 1, context));
665
+ if (value.length > index) output.push(TRUNCATED);
666
+ return output;
667
+ }
668
+ const input = value;
669
+ const output = {};
670
+ let count = 0;
671
+ let truncated = false;
672
+ for (const key in input) {
673
+ if (!propertyIsEnumerable.call(input, key)) break;
674
+ if (count >= MAX_ITEMS || remainingNodes <= 0) {
675
+ truncated = true;
676
+ break;
677
+ }
678
+ const converted = convert(input[key], depth + 1, new MediaTypeContext(input, key));
679
+ Object.defineProperty(output, key, {
680
+ value: converted,
681
+ enumerable: true,
682
+ writable: true,
683
+ configurable: true
684
+ });
685
+ count++;
686
+ }
687
+ if (truncated) output[TRUNCATED] = "Additional properties omitted";
688
+ return output;
689
+ } finally {
690
+ ancestors.delete(value);
691
+ }
692
+ } catch {
693
+ return UNSERIALIZABLE;
694
+ }
695
+ };
696
+ return convert(content, 0, MediaTypeContext.EMPTY);
697
+ } catch {
698
+ return UNSERIALIZABLE;
699
+ }
700
+ }
701
+ //#endregion
702
+ //#region src/genkit/processor.ts
703
+ const SPAN_STATUS_ERROR = 2;
704
+ const MODEL_PARAMETER_KEYS = [
705
+ "temperature",
706
+ "maxOutputTokens",
707
+ "topK",
708
+ "topP",
709
+ "stopSequences"
710
+ ];
711
+ const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
712
+ "apikey",
713
+ "accesstoken",
714
+ "authorization",
715
+ "clientsecret",
716
+ "headers",
717
+ "password",
718
+ "secret",
719
+ "token"
720
+ ]);
721
+ const PROVIDER_NAMES = /* @__PURE__ */ new Map([["googleai", "gemini"], ["vertexai", "vertex"]]);
722
+ const STACK_ERROR_NAME = /^([A-Za-z_$][\w$]*)(?=:|\n|$)/;
723
+ /**
724
+ * An OpenTelemetry span processor that turns Genkit spans into PostHog AI events.
725
+ *
726
+ * - The root span of a Genkit trace (usually a flow) becomes an `$ai_trace`.
727
+ * - Each model action becomes an `$ai_generation` with input, output, token usage, latency and tools.
728
+ * - Each embedder action becomes an `$ai_embedding`.
729
+ * - Every other Genkit span (tools, `generate` steps, retrievers, ...) becomes an `$ai_span`.
730
+ *
731
+ * Spans without Genkit attributes are ignored.
732
+ *
733
+ * @example
734
+ * ```typescript
735
+ * import { enableTelemetry } from 'genkit/tracing'
736
+ * import { PostHogGenkitSpanProcessor } from '@posthog/ai/genkit'
737
+ * import { PostHog } from 'posthog-node'
738
+ *
739
+ * const phClient = new PostHog('<POSTHOG_API_KEY>')
740
+ *
741
+ * await enableTelemetry({
742
+ * spanProcessors: [new PostHogGenkitSpanProcessor({ client: phClient })],
743
+ * })
744
+ * ```
745
+ *
746
+ * In serverless functions, call `await flushTracing()` from `genkit/tracing` before the function
747
+ * returns. It calls {@link PostHogGenkitSpanProcessor.forceFlush}, which flushes the PostHog client.
748
+ */
749
+ var PostHogGenkitSpanProcessor = class {
750
+ constructor(options) {
751
+ this._spanDistinctIds = /* @__PURE__ */ new WeakMap();
752
+ this._client = options.client;
753
+ this._distinctId = options.distinctId;
754
+ this._privacyMode = options.privacyMode ?? false;
755
+ this._groups = options.groups;
756
+ this._properties = options.properties ?? {};
757
+ this._onError = options.onError;
758
+ }
759
+ onStart(span) {
760
+ try {
761
+ const distinctId = this._distinctId ?? this._client.getContext?.()?.distinctId;
762
+ if (distinctId !== void 0) this._spanDistinctIds.set(span, distinctId);
763
+ } catch (error) {
764
+ this._handleError(error);
765
+ }
766
+ }
767
+ onEnd(span) {
768
+ const distinctId = this._distinctId ?? this._spanDistinctIds.get(span);
769
+ this._spanDistinctIds.delete(span);
770
+ try {
771
+ if (typeof span.attributes["genkit:type"] !== "string") return;
772
+ const subtype = span.attributes["genkit:metadata:subtype"];
773
+ if (subtype === "model") this._captureGeneration(span, distinctId);
774
+ else if (subtype === "embedder") this._captureEmbedding(span, distinctId);
775
+ else this._captureSpan(span, distinctId);
776
+ } catch (error) {
777
+ this._handleError(error);
778
+ }
779
+ }
780
+ async forceFlush() {
781
+ try {
782
+ await this._client.flush();
783
+ } catch (error) {
784
+ this._handleError(error);
785
+ }
786
+ }
787
+ async shutdown() {
788
+ await this.forceFlush();
789
+ }
790
+ _captureGeneration(span, distinctId) {
791
+ const request = parseAttribute(span, "genkit:input");
792
+ const response = parseAttribute(span, "genkit:output");
793
+ const usage = response?.usage;
794
+ const message = response?.message ?? response?.candidates?.[0]?.message;
795
+ const finishReason = response?.finishReason ?? response?.candidates?.[0]?.finishReason;
796
+ const shouldRedact = isPrivacyModeEnabled(this._client, this._privacyMode);
797
+ const { traceId, spanId, parentId } = spanIds(span);
798
+ captureAiGeneration(this._client, {
799
+ distinctId,
800
+ traceId,
801
+ ...modelAndProvider(span.name),
802
+ baseURL: null,
803
+ input: shouldRedact ? null : this._formatMessages(request?.messages ?? []),
804
+ output: shouldRedact ? null : this._formatMessages(message ? [message] : []),
805
+ latency: latencySeconds(span),
806
+ modelParameters: pickModelParameters(request?.config),
807
+ tools: request?.tools?.length ? request.tools : null,
808
+ usage: usage && {
809
+ inputTokens: usage.inputTokens,
810
+ outputTokens: usage.outputTokens,
811
+ reasoningTokens: usage.thoughtsTokens,
812
+ cacheReadInputTokens: usage.cachedContentTokens,
813
+ rawUsage: usage
814
+ },
815
+ stopReason: finishReason,
816
+ groups: this._groups,
817
+ privacyMode: this._privacyMode,
818
+ onError: this._onError,
819
+ error: spanError(span),
820
+ properties: {
821
+ $ai_framework: "genkit",
822
+ $ai_span_id: spanId,
823
+ $ai_parent_id: parentId,
824
+ $ai_span_name: span.name,
825
+ ...this._properties
826
+ }
827
+ });
828
+ }
829
+ _captureEmbedding(span, distinctId) {
830
+ const request = parseAttribute(span, "genkit:input");
831
+ const shouldRedact = isPrivacyModeEnabled(this._client, this._privacyMode);
832
+ const { traceId, spanId, parentId } = spanIds(span);
833
+ const input = (request?.input ?? []).map((document) => formatDocument(document.content ?? [], this._client));
834
+ captureAiGeneration(this._client, {
835
+ eventType: "$ai_embedding",
836
+ distinctId,
837
+ traceId,
838
+ ...modelAndProvider(span.name),
839
+ baseURL: null,
840
+ input: shouldRedact ? null : formatToolResult(input, this._client, MAX_OUTPUT_SIZE),
841
+ output: null,
842
+ latency: latencySeconds(span),
843
+ groups: this._groups,
844
+ privacyMode: this._privacyMode,
845
+ onError: this._onError,
846
+ error: spanError(span),
847
+ properties: {
848
+ $ai_framework: "genkit",
849
+ $ai_span_id: spanId,
850
+ $ai_parent_id: parentId,
851
+ $ai_span_name: span.name,
852
+ ...this._properties
853
+ }
854
+ });
855
+ }
856
+ _captureSpan(span, distinctId) {
857
+ const isRoot = span.attributes["genkit:isRoot"] === true;
858
+ const { traceId, spanId, parentId } = spanIds(span);
859
+ const error = spanError(span);
860
+ captureAiEvent(this._client, {
861
+ distinctId: distinctId ?? traceId,
862
+ event: isRoot ? "$ai_trace" : "$ai_span",
863
+ properties: {
864
+ $ai_lib: "posthog-ai",
865
+ $ai_lib_version: version,
866
+ $ai_framework: "genkit",
867
+ $ai_trace_id: traceId,
868
+ $ai_span_id: spanId,
869
+ ...!isRoot && parentId ? { $ai_parent_id: parentId } : {},
870
+ $ai_span_name: span.name,
871
+ $ai_span_type: span.attributes["genkit:metadata:subtype"] ?? span.attributes["genkit:type"],
872
+ $ai_input_state: this._formatState(span, "genkit:input"),
873
+ $ai_output_state: this._formatState(span, "genkit:output"),
874
+ $ai_latency: latencySeconds(span),
875
+ ...error ? {
876
+ $ai_is_error: true,
877
+ $ai_error: formatAiError(error, isPrivacyModeEnabled(this._client, this._privacyMode))
878
+ } : {},
879
+ ...this._properties,
880
+ ...distinctId ? {} : { $process_person_profile: false }
881
+ },
882
+ groups: this._groups
883
+ });
884
+ }
885
+ _handleError(error) {
886
+ try {
887
+ this._onError?.(error);
888
+ } catch {}
889
+ }
890
+ _formatState(span, key) {
891
+ if (isPrivacyModeEnabled(this._client, this._privacyMode)) return null;
892
+ return formatToolResult(redactCredentials(parseAttribute(span, key)), this._client, MAX_OUTPUT_SIZE);
893
+ }
894
+ _formatMessages(messages) {
895
+ return formatToolResult(messages.map((message) => ({
896
+ role: message.role === "model" ? "assistant" : message.role,
897
+ content: (message.content ?? []).map((part) => formatPart(part, this._client))
898
+ })), this._client, MAX_OUTPUT_SIZE);
899
+ }
900
+ };
901
+ function formatPart(part, client) {
902
+ if (typeof part.text === "string") return {
903
+ type: "text",
904
+ text: part.text
905
+ };
906
+ if (typeof part.reasoning === "string") return {
907
+ type: "reasoning",
908
+ text: part.reasoning
909
+ };
910
+ if (part.toolRequest) return {
911
+ type: "function",
912
+ id: part.toolRequest.ref,
913
+ function: {
914
+ name: part.toolRequest.name,
915
+ arguments: part.toolRequest.input ?? {}
916
+ }
917
+ };
918
+ if (part.toolResponse) return {
919
+ type: "tool_result",
920
+ tool_use_id: part.toolResponse.ref,
921
+ name: part.toolResponse.name,
922
+ content: part.toolResponse.output
923
+ };
924
+ if (part.media) return formatMedia(part.media, client);
925
+ return part;
926
+ }
927
+ function formatMedia(media, client) {
928
+ if (media.url.startsWith("data:")) {
929
+ const comma = media.url.indexOf(",");
930
+ const metadata = media.url.slice(5, comma);
931
+ if (comma !== -1 && metadata.endsWith(";base64")) return formatInlineDataBlock({
932
+ mimeType: media.contentType || metadata.split(";", 1)[0],
933
+ data: media.url.slice(comma + 1)
934
+ }, client);
935
+ }
936
+ if (!media.contentType || media.contentType.startsWith("image/")) return {
937
+ type: "image",
938
+ image: media.url
939
+ };
940
+ return { media };
941
+ }
942
+ /** A text-only document becomes one string, like the string input of the other embedding integrations. */
943
+ function formatDocument(content, client) {
944
+ if (content.every((part) => typeof part.text === "string")) return content.map((part) => part.text).join("");
945
+ return content.map((part) => formatPart(part, client));
946
+ }
947
+ function redactCredentials(value) {
948
+ if (Array.isArray(value)) return value.map(redactCredentials);
949
+ if (!isObject(value)) return value;
950
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, CREDENTIAL_KEYS.has(key.toLowerCase().replace(/[-_]/g, "")) ? "[REDACTED]" : redactCredentials(item)]));
951
+ }
952
+ /** Genkit stores span input and output as JSON strings. */
953
+ function parseAttribute(span, key) {
954
+ const value = span.attributes[key];
955
+ if (typeof value !== "string") return;
956
+ try {
957
+ return JSON.parse(value);
958
+ } catch {
959
+ return value;
960
+ }
961
+ }
962
+ function spanIds(span) {
963
+ const { traceId, spanId } = span.spanContext();
964
+ return {
965
+ traceId,
966
+ spanId,
967
+ parentId: span.parentSpanContext?.spanId ?? span.parentSpanId
968
+ };
969
+ }
970
+ function latencySeconds(span) {
971
+ return span.duration[0] + span.duration[1] / 1e9;
972
+ }
973
+ /** Rebuilds the error from the span's `exception` event, so error tracking gets a real `Error` with a stack. */
974
+ function spanError(span) {
975
+ if (span.status.code !== SPAN_STATUS_ERROR) return;
976
+ const exception = span.events?.find((event) => event.name === "exception")?.attributes ?? {};
977
+ const message = exception["exception.message"];
978
+ const stack = exception["exception.stacktrace"];
979
+ const error = new Error(typeof message === "string" ? message : span.status.message);
980
+ error.stack = typeof stack === "string" ? stack : void 0;
981
+ const name = STACK_ERROR_NAME.exec(error.stack ?? "")?.[1] ?? exception["exception.type"];
982
+ if (typeof name === "string") error.name = name;
983
+ return error;
984
+ }
985
+ /** Genkit names models and embedders `<plugin>/<name>`, for example `vertexai/gemini-2.5-flash`. */
986
+ function modelAndProvider(name) {
987
+ const slash = name.indexOf("/");
988
+ const plugin = slash > 0 ? name.slice(0, slash) : "";
989
+ return {
990
+ model: name.slice(slash + 1),
991
+ provider: PROVIDER_NAMES.get(plugin) ?? plugin
992
+ };
993
+ }
994
+ function pickModelParameters(config) {
995
+ const parameters = {};
996
+ for (const key of MODEL_PARAMETER_KEYS) if (config?.[key] !== void 0) parameters[key] = config[key];
997
+ return parameters;
998
+ }
999
+ //#endregion
1000
+ exports.PostHogGenkitSpanProcessor = PostHogGenkitSpanProcessor;
1001
+
1002
+ //# sourceMappingURL=index.cjs.map