@posthog/ai 8.17.1 → 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.
- package/README.md +22 -0
- package/dist/adk/index.cjs +1 -1
- package/dist/adk/index.mjs +1 -1
- package/dist/anthropic/index.cjs +1 -1
- package/dist/anthropic/index.mjs +1 -1
- package/dist/claude-agent-sdk/index.cjs +1 -1
- package/dist/claude-agent-sdk/index.mjs +1 -1
- package/dist/gemini/index.cjs +1 -1
- package/dist/gemini/index.mjs +1 -1
- package/dist/genkit/index.cjs +1002 -0
- package/dist/genkit/index.cjs.map +1 -0
- package/dist/genkit/index.d.ts +95 -0
- package/dist/genkit/index.mjs +1001 -0
- package/dist/genkit/index.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/langchain/index.cjs +1 -1
- package/dist/langchain/index.mjs +1 -1
- package/dist/langchain/middleware/index.cjs +1 -1
- package/dist/langchain/middleware/index.mjs +1 -1
- package/dist/openai/index.cjs +1 -1
- package/dist/openai/index.mjs +1 -1
- package/dist/openai-agents/index.cjs +1 -1
- package/dist/openai-agents/index.mjs +1 -1
- package/dist/otel/index.cjs +16 -2
- package/dist/otel/index.cjs.map +1 -1
- package/dist/otel/index.d.ts +12 -3
- package/dist/otel/index.mjs +16 -2
- package/dist/otel/index.mjs.map +1 -1
- package/dist/vercel/index.cjs +1 -1
- package/dist/vercel/index.mjs +1 -1
- package/package.json +19 -4
|
@@ -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
|