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