mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 22:33:05 -07:00
fix(telemetry): implement bounded structural truncation
This commit is contained in:
@@ -52,23 +52,37 @@ describe('truncateForTelemetry', () => {
|
||||
});
|
||||
|
||||
it('should correctly truncate strings with multi-byte unicode characters (emojis)', () => {
|
||||
// 5 emojis, each is multiple bytes in UTF-16
|
||||
// 5 emojis, each is a single grapheme cluster
|
||||
const emojis = '👋🌍🚀🔥🎉';
|
||||
|
||||
// Truncating to length 5 (which is 2.5 emojis in UTF-16 length terms)
|
||||
// truncateString will stop after the full grapheme clusters that fit within 5
|
||||
const result = truncateForTelemetry(emojis, 5);
|
||||
// Truncating to 2 graphemes
|
||||
const result = truncateForTelemetry(emojis, 2);
|
||||
|
||||
expect(result).toBe('👋🌍...[TRUNCATED: original length 10]');
|
||||
expect(result).toBe('👋🌍...[TRUNCATED: original length 5]');
|
||||
});
|
||||
|
||||
it('should stringify and truncate objects if exceeding maxLength', () => {
|
||||
it('should stringify and structurally truncate objects if exceeding limits', () => {
|
||||
const obj = { message: 'hello world', nested: { a: 1 } };
|
||||
const stringified = JSON.stringify(obj);
|
||||
const result = truncateForTelemetry(obj, 10);
|
||||
expect(result).toBe(
|
||||
stringified.substring(0, 10) +
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
JSON.stringify({
|
||||
message: 'hello worl...[TRUNCATED: original length 11]',
|
||||
nested: { a: 1 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should structurally truncate arrays and depth', () => {
|
||||
const obj = {
|
||||
arr: [1, 2, 3],
|
||||
deep: { level1: { level2: { level3: { a: 1 } } } },
|
||||
};
|
||||
const result = truncateForTelemetry(obj, 100, 2, 3);
|
||||
expect(result).toBe(
|
||||
JSON.stringify({
|
||||
arr: [1, 2, '[TRUNCATED: Array of length 3]'],
|
||||
deep: { level1: { level2: '[TRUNCATED: Max Depth Reached]' } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { truncateString } from '../utils/textUtils.js';
|
||||
import {
|
||||
GEN_AI_AGENT_DESCRIPTION,
|
||||
GEN_AI_AGENT_NAME,
|
||||
@@ -54,27 +53,78 @@ export const spanRegistry = new FinalizationRegistry((endSpan: () => void) => {
|
||||
*/
|
||||
export function truncateForTelemetry(
|
||||
value: unknown,
|
||||
maxLength = 10000,
|
||||
maxStringLength = 10000,
|
||||
maxArrayLength = 100,
|
||||
maxDepth = 4,
|
||||
): AttributeValue | undefined {
|
||||
if (typeof value === 'string') {
|
||||
return truncateString(
|
||||
value,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${value.length}]`,
|
||||
) as AttributeValue;
|
||||
const truncateObj = (v: unknown, depth: number): unknown => {
|
||||
if (typeof v === 'string') {
|
||||
const graphemes = Array.from(v);
|
||||
if (graphemes.length > maxStringLength) {
|
||||
return (
|
||||
graphemes.slice(0, maxStringLength).join('') +
|
||||
`...[TRUNCATED: original length ${graphemes.length}]`
|
||||
);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if (
|
||||
typeof v === 'number' ||
|
||||
typeof v === 'boolean' ||
|
||||
v === null ||
|
||||
v === undefined
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
if (depth >= maxDepth) {
|
||||
return `[TRUNCATED: Max Depth Reached]`;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
if (v.length > maxArrayLength) {
|
||||
const truncatedArray = v
|
||||
.slice(0, maxArrayLength)
|
||||
.map((item) => truncateObj(item, depth + 1));
|
||||
truncatedArray.push(`[TRUNCATED: Array of length ${v.length}]`);
|
||||
return truncatedArray;
|
||||
}
|
||||
return v.map((item) => truncateObj(item, depth + 1));
|
||||
}
|
||||
|
||||
const newObj: Record<string, unknown> = {};
|
||||
let numKeys = 0;
|
||||
const MAX_KEYS = 100;
|
||||
for (const key in v) {
|
||||
if (!Object.prototype.hasOwnProperty.call(v, key)) continue;
|
||||
if (numKeys >= MAX_KEYS) {
|
||||
newObj['__truncated'] = `[TRUNCATED: Object with >${MAX_KEYS} keys]`;
|
||||
break;
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(v, key);
|
||||
if (descriptor) {
|
||||
newObj[key] = truncateObj(descriptor.value, depth + 1);
|
||||
}
|
||||
numKeys++;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const truncated = truncateObj(value, 0);
|
||||
|
||||
if (
|
||||
typeof truncated === 'string' ||
|
||||
typeof truncated === 'number' ||
|
||||
typeof truncated === 'boolean'
|
||||
) {
|
||||
return truncated as AttributeValue;
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const stringified = safeJsonStringify(value);
|
||||
return truncateString(
|
||||
stringified,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
) as AttributeValue;
|
||||
if (truncated === null || truncated === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value as AttributeValue;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return safeJsonStringify(truncated) as AttributeValue;
|
||||
}
|
||||
|
||||
function isAsyncIterable<T>(value: T): value is T & AsyncIterable<unknown> {
|
||||
|
||||
Reference in New Issue
Block a user