@posthog/ai 8.7.1 → 8.8.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.
@@ -12,6 +12,33 @@ const isObject = value => {
12
12
  return value !== null && typeof value === 'object' && !Array.isArray(value);
13
13
  };
14
14
 
15
+ /** @internal */
16
+
17
+ /** @internal */
18
+
19
+ /** @internal */
20
+ function isFullAiCaptureEnabled(client) {
21
+ return client?.enableFullAiCapture === true;
22
+ }
23
+
24
+ /** @internal */
25
+ function captureAiEvent(client, event) {
26
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
27
+ client.captureAi(event);
28
+ return;
29
+ }
30
+ client.capture(event);
31
+ }
32
+
33
+ /** @internal */
34
+ async function captureAiEventImmediate(client, event) {
35
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
36
+ await client.captureAiImmediate(event);
37
+ return;
38
+ }
39
+ await client.captureImmediate(event);
40
+ }
41
+
15
42
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
16
43
  const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
17
44
  class Base64Recognizer {
@@ -117,7 +144,6 @@ class BinaryContentRedactor {
117
144
  this.recognizer = recognizer;
118
145
  }
119
146
  redact(value, mediaType) {
120
- if (this.isMultimodalEnabled()) return value;
121
147
  this.visited = new WeakSet();
122
148
  return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
123
149
  }
@@ -161,17 +187,14 @@ class BinaryContentRedactor {
161
187
  if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
162
188
  return `[base64 ${mediaType} redacted]`;
163
189
  }
164
- isMultimodalEnabled() {
165
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
166
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
167
- }
168
190
  }
169
191
 
170
192
  const redactor = new BinaryContentRedactor();
171
193
  function redactBase64DataUrl(str, mediaType) {
172
194
  return redactor.redact(str, mediaType);
173
195
  }
174
- const sanitizeVercel = data => redactor.redact(data);
196
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
197
+ const sanitizeVercel = (data, client) => sanitize(data, client);
175
198
 
176
199
  const TOKEN_PROPERTY_KEYS = new Set(['$ai_input_tokens', '$ai_output_tokens', '$ai_cache_read_input_tokens', '$ai_cache_creation_input_tokens', '$ai_total_tokens', '$ai_reasoning_tokens']);
177
200
 
@@ -253,11 +276,14 @@ function toSafeString(input) {
253
276
  return '';
254
277
  }
255
278
  }
256
- const truncate = input => {
279
+ const truncate = (input, client) => {
257
280
  const str = toSafeString(input);
258
281
  if (str === '') {
259
282
  return '';
260
283
  }
284
+ if (isFullAiCaptureEnabled(client)) {
285
+ return str;
286
+ }
261
287
 
262
288
  // Check if we need to truncate and ensure STRING_FORMAT is respected
263
289
  const buffer = sharedTextEncoder.encode(str);
@@ -413,7 +439,7 @@ function sanitizeValues(obj) {
413
439
  return jsonSafe;
414
440
  }
415
441
 
416
- var version = "8.7.1";
442
+ var version = "8.8.0";
417
443
 
418
444
  const DEFAULT_MAX_DEPTH = 3;
419
445
  const MAX_STACK_LINES = 20;
@@ -662,9 +688,9 @@ const captureAiGeneration = async (client, options) => {
662
688
  groups: options.groups
663
689
  };
664
690
  if (options.captureImmediate) {
665
- await client.captureImmediate(event);
691
+ await captureAiEventImmediate(client, event);
666
692
  } else {
667
- client.capture(event);
693
+ captureAiEvent(client, event);
668
694
  }
669
695
  } catch (error) {
670
696
  // Telemetry failures must never affect the instrumented provider call.
@@ -687,12 +713,12 @@ function getSpecificationVersion(model) {
687
713
 
688
714
  // Content types for the output array
689
715
 
690
- const redactFileData = (data, mediaType) => {
716
+ const redactFileData = (data, mediaType, client) => {
691
717
  if (data instanceof URL) {
692
- return redactBase64DataUrl(data.toString(), data.protocol === 'data:' ? mediaType : undefined);
718
+ return isFullAiCaptureEnabled(client) ? data.toString() : redactBase64DataUrl(data.toString(), data.protocol === 'data:' ? mediaType : undefined);
693
719
  }
694
720
  if (isString(data)) {
695
- return redactBase64DataUrl(data, mediaType);
721
+ return isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mediaType);
696
722
  }
697
723
  return undefined;
698
724
  };
@@ -707,7 +733,7 @@ const mapVercelParams = params => {
707
733
  stream: params.stream
708
734
  };
709
735
  };
710
- const mapVercelPrompt = messages => {
736
+ const mapVercelPrompt = (messages, client) => {
711
737
  // Map and truncate individual content
712
738
  const inputs = messages.map(message => {
713
739
  let content;
@@ -716,7 +742,7 @@ const mapVercelPrompt = messages => {
716
742
  if (message.role === 'system') {
717
743
  content = [{
718
744
  type: 'text',
719
- text: truncate(toContentString(message.content))
745
+ text: truncate(toContentString(message.content), client)
720
746
  }];
721
747
  } else {
722
748
  // Handle other roles which have array content
@@ -725,11 +751,11 @@ const mapVercelPrompt = messages => {
725
751
  if (c.type === 'text') {
726
752
  return {
727
753
  type: 'text',
728
- text: truncate(c.text)
754
+ text: truncate(c.text, client)
729
755
  };
730
756
  } else if (c.type === 'file') {
731
757
  // Redact base64 data URLs and raw base64 to prevent oversized events
732
- const fileData = redactFileData(c.data, c.mediaType) ?? 'raw files not supported';
758
+ const fileData = redactFileData(c.data, c.mediaType, client) ?? 'raw files not supported';
733
759
  return {
734
760
  type: 'file',
735
761
  file: fileData,
@@ -738,7 +764,7 @@ const mapVercelPrompt = messages => {
738
764
  } else if (c.type === 'reasoning') {
739
765
  return {
740
766
  type: 'reasoning',
741
- text: truncate(c.text)
767
+ text: truncate(c.text, client)
742
768
  };
743
769
  } else if (c.type === 'tool-call') {
744
770
  return {
@@ -752,7 +778,7 @@ const mapVercelPrompt = messages => {
752
778
  type: 'tool-result',
753
779
  toolCallId: c.toolCallId,
754
780
  toolName: c.toolName,
755
- output: sanitizeVercel(c.output),
781
+ output: sanitizeVercel(c.output, client),
756
782
  isError: c.isError
757
783
  };
758
784
  }
@@ -765,7 +791,7 @@ const mapVercelPrompt = messages => {
765
791
  // Fallback for non-array content
766
792
  content = [{
767
793
  type: 'text',
768
- text: truncate(toContentString(message.content))
794
+ text: truncate(toContentString(message.content), client)
769
795
  }];
770
796
  }
771
797
  }
@@ -774,6 +800,12 @@ const mapVercelPrompt = messages => {
774
800
  content
775
801
  };
776
802
  });
803
+
804
+ // Full AI capture means no truncation of any kind; the aggregate trim below exists
805
+ // only to keep the default-mode payload under MAX_OUTPUT_SIZE.
806
+ if (isFullAiCaptureEnabled(client)) {
807
+ return inputs;
808
+ }
777
809
  try {
778
810
  // Trim the inputs array until its serialized JSON size fits within MAX_OUTPUT_SIZE.
779
811
  // Pre-compute each message's byte size once so we can shift by accumulated budget
@@ -810,12 +842,12 @@ const mapVercelPrompt = messages => {
810
842
  }
811
843
  return inputs;
812
844
  };
813
- const mapVercelOutput = result => {
845
+ const mapVercelOutput = (result, client) => {
814
846
  const content = result.map(item => {
815
847
  if (item.type === 'text') {
816
848
  return {
817
849
  type: 'text',
818
- text: truncate(item.text)
850
+ text: truncate(item.text, client)
819
851
  };
820
852
  }
821
853
  if (item.type === 'tool-call') {
@@ -833,15 +865,15 @@ const mapVercelOutput = result => {
833
865
  if (item.type === 'reasoning') {
834
866
  return {
835
867
  type: 'reasoning',
836
- text: truncate(item.text)
868
+ text: truncate(item.text, client)
837
869
  };
838
870
  }
839
871
  if (item.type === 'file') {
840
872
  // Handle files similar to input mapping - avoid large base64 data
841
- let fileData = redactFileData(item.data, item.mediaType) ?? `[binary ${item.mediaType} file]`;
873
+ let fileData = redactFileData(item.data, item.mediaType, client) ?? `[binary ${item.mediaType} file]`;
842
874
 
843
- // If not redacted and still large, replace with size indicator
844
- if (typeof item.data === 'string' && fileData === item.data && item.data.length > 1000) {
875
+ // Skipped under full AI capture: media stays untouched, so no placeholder swap either.
876
+ if (!isFullAiCaptureEnabled(client) && typeof item.data === 'string' && fileData === item.data && item.data.length > 1000) {
845
877
  fileData = `[${item.mediaType} file - ${item.data.length} bytes]`;
846
878
  }
847
879
  return {
@@ -863,7 +895,7 @@ const mapVercelOutput = result => {
863
895
  // Fallback for unknown types - try to extract text if possible
864
896
  return {
865
897
  type: 'text',
866
- text: truncate(JSON.stringify(item))
898
+ text: truncate(JSON.stringify(item), client)
867
899
  };
868
900
  });
869
901
  if (content.length > 0) {
@@ -876,7 +908,7 @@ const mapVercelOutput = result => {
876
908
  try {
877
909
  const jsonOutput = JSON.stringify(result);
878
910
  return [{
879
- content: truncate(jsonOutput),
911
+ content: truncate(jsonOutput, client),
880
912
  role: 'assistant'
881
913
  }];
882
914
  } catch {
@@ -1072,7 +1104,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1072
1104
  const modelId = mergedOptions.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
1073
1105
  const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
1074
1106
  // result.content is undefined when the model returns only tool calls with no text output
1075
- const content = mapVercelOutput(result.content ?? []);
1107
+ const content = mapVercelOutput(result.content ?? [], phClient);
1076
1108
  const latency = (Date.now() - startTime) / 1000;
1077
1109
  const providerMetadata = result.providerMetadata;
1078
1110
  const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, result.usage);
@@ -1115,7 +1147,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1115
1147
  ...baseOptions,
1116
1148
  model: modelId,
1117
1149
  provider: provider,
1118
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1150
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1119
1151
  output: content,
1120
1152
  latency,
1121
1153
  baseURL,
@@ -1132,7 +1164,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1132
1164
  ...baseOptions,
1133
1165
  model: modelId,
1134
1166
  provider: model.provider,
1135
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1167
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1136
1168
  output: [],
1137
1169
  latency: 0,
1138
1170
  baseURL,
@@ -1266,13 +1298,13 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1266
1298
  if (reasoningText) {
1267
1299
  content.push({
1268
1300
  type: 'reasoning',
1269
- text: truncate(reasoningText)
1301
+ text: truncate(reasoningText, phClient)
1270
1302
  });
1271
1303
  }
1272
1304
  if (generatedText) {
1273
1305
  content.push({
1274
1306
  type: 'text',
1275
- text: truncate(generatedText)
1307
+ text: truncate(generatedText, phClient)
1276
1308
  });
1277
1309
  }
1278
1310
  for (const toolCall of toolCallsInProgress.values()) {
@@ -1307,7 +1339,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1307
1339
  ...baseOptions,
1308
1340
  model: modelId,
1309
1341
  provider: provider,
1310
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1342
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1311
1343
  output,
1312
1344
  latency,
1313
1345
  timeToFirstToken,
@@ -1363,7 +1395,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1363
1395
  ...baseOptions,
1364
1396
  model: modelId,
1365
1397
  provider: provider,
1366
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1398
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1367
1399
  output: [],
1368
1400
  latency: 0,
1369
1401
  baseURL,