From 1a53e9195e31f5b0c663fbec584fce00d4534415 Mon Sep 17 00:00:00 2001 From: Jamijunky Date: Thu, 26 Mar 2026 00:31:41 +0530 Subject: [PATCH 1/3] fix(telemetry): mitigate OOM risks via truncation and buffer limits --- packages/core/src/telemetry/loggers.ts | 27 +++++++++++++++++--- packages/core/src/telemetry/sdk.test.ts | 26 +++++++++++++++++++ packages/core/src/telemetry/sdk.ts | 7 ++++++ packages/core/src/telemetry/utils.ts | 33 +++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/telemetry/utils.ts diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index a33c8ca200..7b3266502f 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -95,6 +95,7 @@ import { EmptyWalletMenuShownEvent, CreditPurchaseClickEvent, } from './billingEvents.js'; +import { safeTruncate } from './utils.js'; export function logCliConfiguration( config: Config, @@ -120,10 +121,14 @@ export function logCliConfiguration( } export function logUserPrompt(config: Config, event: UserPromptEvent): void { + // SHIELD: Prevent massive prompt strings from sitting in the buffer heap + if (event.prompt) { + event.prompt = safeTruncate(event.prompt, 4096); + } + ClearcutLogger.getInstance(config)?.logNewPromptEvent(event); bufferTelemetryEvent(() => { const logger = logs.getLogger(SERVICE_NAME); - const logRecord: LogRecord = { body: event.toLogBody(), attributes: event.toOpenTelemetryAttributes(config), @@ -229,11 +234,15 @@ export function logFileOperation( } export function logApiRequest(config: Config, event: ApiRequestEvent): void { - ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); + // SHIELD: Immediate truncation before the string hits the buffer + if (event.request_text) { + event.request_text = safeTruncate(event.request_text, 4096); + } + + void ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); bufferTelemetryEvent(() => { const logger = logs.getLogger(SERVICE_NAME); logger.emit(event.toLogRecord(config)); - logger.emit(event.toSemanticLogRecord(config)); }); } @@ -301,17 +310,27 @@ export function logApiError(config: Config, event: ApiErrorEvent): void { } export function logApiResponse(config: Config, event: ApiResponseEvent): void { + // SHIELD: Truncate the model response immediately to prevent OOM + // Check if response_text exists on your ApiResponseEvent type + if (event.response_text) { + event.response_text = safeTruncate(event.response_text, 4096); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const uiEvent = { - // eslint-disable-next-line @typescript-eslint/no-misused-spread ...event, 'event.name': EVENT_API_RESPONSE, 'event.timestamp': new Date().toISOString(), } as UiEvent; uiTelemetryService.addEvent(uiEvent); + ClearcutLogger.getInstance(config)?.logApiResponseEvent(event); + bufferTelemetryEvent(() => { const logger = logs.getLogger(SERVICE_NAME); + + // Because we truncated 'event.response_text' at the top of the function, + // these log records now only contain the safe, small string. logger.emit(event.toLogRecord(config)); logger.emit(event.toSemanticLogRecord(config)); diff --git a/packages/core/src/telemetry/sdk.test.ts b/packages/core/src/telemetry/sdk.test.ts index 9636212e2c..a5a666262c 100644 --- a/packages/core/src/telemetry/sdk.test.ts +++ b/packages/core/src/telemetry/sdk.test.ts @@ -32,6 +32,7 @@ import * as path from 'node:path'; import { authEvents } from '../code_assist/oauth2.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { safeTruncate } from './utils.js'; vi.mock('@opentelemetry/exporter-trace-otlp-grpc'); vi.mock('@opentelemetry/exporter-logs-otlp-grpc'); @@ -361,6 +362,31 @@ describe('Telemetry SDK', () => { }); }); + it('should enforce MAX_TELEMETRY_BUFFER_SIZE and evict oldest events', () => { + // Fill the buffer past the limit (100) + const callbacks = []; + for (let i = 0; i < 110; i++) { + const cb = vi.fn(); + callbacks.push(cb); + bufferTelemetryEvent(cb); + } + expect(debugLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Telemetry buffer full') + ); + }); + + it('should correctly truncate large strings via safeTruncate', () => { + const limit = 2048; + const largeString = 'a'.repeat(3000); + const result = safeTruncate(largeString, limit); + + // 1. Check for the specific production-ready suffix + expect(result).toContain('truncated for performance'); + + // 2. Check that the math is RUTHLESS (Total length must be exactly <= limit) + expect(result.length).toBeLessThanOrEqual(limit); + }); + it('should disable telemetry and log error if useCollector and useCliAuth are both true', async () => { vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true); vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true); diff --git a/packages/core/src/telemetry/sdk.ts b/packages/core/src/telemetry/sdk.ts index bafa540790..ae08747ec8 100644 --- a/packages/core/src/telemetry/sdk.ts +++ b/packages/core/src/telemetry/sdk.ts @@ -108,11 +108,18 @@ export function isTelemetrySdkInitialized(): boolean { return telemetryInitialized; } +const MAX_TELEMETRY_BUFFER_SIZE = 100; + export function bufferTelemetryEvent(fn: () => void | Promise): void { if (telemetryInitialized) { // eslint-disable-next-line @typescript-eslint/no-floating-promises fn(); } else { + // 2. If the buffer is full, remove the oldest event to save memory + if (telemetryBuffer.length >= MAX_TELEMETRY_BUFFER_SIZE) { + telemetryBuffer.shift(); + debugLogger.warn('Telemetry buffer full. Dropping oldest event to prevent OOM.'); + } telemetryBuffer.push(fn); } } diff --git a/packages/core/src/telemetry/utils.ts b/packages/core/src/telemetry/utils.ts new file mode 100644 index 0000000000..dd6f44ec4e --- /dev/null +++ b/packages/core/src/telemetry/utils.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const TRUNCATION_SUFFIX = '... (truncated for performance)'; + +/** + * Overload signatures: These tell TypeScript exactly what to expect. + */ +export function safeTruncate(val: string, limit: number): string; +export function safeTruncate(val: unknown, limit: number): unknown; + +/** + * The actual implementation. + */ +export function safeTruncate(val: unknown, limit: number): unknown { + if (typeof val !== 'string') { + return val; + } + + if (val.length <= limit) { + return val; + } + + if (limit <= TRUNCATION_SUFFIX.length) { + return val.substring(0, limit); + } + + const truncateAt = limit - TRUNCATION_SUFFIX.length; + return val.substring(0, truncateAt) + TRUNCATION_SUFFIX; +} \ No newline at end of file From 7a5376ad2da036c587d1bec460d17d71b010479e Mon Sep 17 00:00:00 2001 From: Abdullah Jami Date: Thu, 26 Mar 2026 00:50:57 +0530 Subject: [PATCH 2/3] Enhance safeTruncate to handle grapheme characters --- packages/core/src/telemetry/utils.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/core/src/telemetry/utils.ts b/packages/core/src/telemetry/utils.ts index dd6f44ec4e..c3afaf688b 100644 --- a/packages/core/src/telemetry/utils.ts +++ b/packages/core/src/telemetry/utils.ts @@ -6,28 +6,19 @@ const TRUNCATION_SUFFIX = '... (truncated for performance)'; -/** - * Overload signatures: These tell TypeScript exactly what to expect. - */ export function safeTruncate(val: string, limit: number): string; export function safeTruncate(val: unknown, limit: number): unknown; - -/** - * The actual implementation. - */ export function safeTruncate(val: unknown, limit: number): unknown { - if (typeof val !== 'string') { + if (typeof val !== 'string' || val.length <= limit) { return val; } - if (val.length <= limit) { + // Use Array.from to count actual characters (graphemes) instead of UTF-16 units + const characters = Array.from(val); + if (characters.length <= limit) { return val; } - if (limit <= TRUNCATION_SUFFIX.length) { - return val.substring(0, limit); - } - - const truncateAt = limit - TRUNCATION_SUFFIX.length; - return val.substring(0, truncateAt) + TRUNCATION_SUFFIX; -} \ No newline at end of file + const truncateAt = Math.max(0, limit - TRUNCATION_SUFFIX.length); + return characters.slice(0, truncateAt).join('') + TRUNCATION_SUFFIX; +} From 719ab82e0c91f624a15a0e4ef2ba3c6686755a97 Mon Sep 17 00:00:00 2001 From: Abdullah Jami Date: Thu, 26 Mar 2026 00:53:38 +0530 Subject: [PATCH 3/3] Fix OOM issues and improve event logging structure Added fixes to clear large structured data and prevent OOM issues in logApiRequest and logApiResponse functions. Updated uiEvent mapping to avoid unsafe class spreading. --- packages/core/src/telemetry/loggers.ts | 81 +++++++++++--------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 7b3266502f..cf53290385 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -234,16 +234,27 @@ export function logFileOperation( } export function logApiRequest(config: Config, event: ApiRequestEvent): void { - // SHIELD: Immediate truncation before the string hits the buffer if (event.request_text) { event.request_text = safeTruncate(event.request_text, 4096); } - void ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); - bufferTelemetryEvent(() => { - const logger = logs.getLogger(SERVICE_NAME); - logger.emit(event.toLogRecord(config)); - }); + // FIX: Clear structured data that contains the same large text to prevent OOM + if (event.prompt && (event.prompt as any).contents) { + (event.prompt as any).contents = undefined; + } + + // FIX: Explicit mapping to avoid unsafe class spreading + const uiEvent: UiEvent = { + 'event.name': EVENT_API_REQUEST, + 'event.timestamp': new Date().toISOString(), + status_code: event.status_code, + duration_ms: event.duration_ms, + request_text: event.request_text, + auth_type: event.auth_type, + model: event.model, + role: event.role, + }; + uiTelemetryService.addEvent(uiEvent); } export function logFlashFallback( @@ -310,54 +321,30 @@ export function logApiError(config: Config, event: ApiErrorEvent): void { } export function logApiResponse(config: Config, event: ApiResponseEvent): void { - // SHIELD: Truncate the model response immediately to prevent OOM - // Check if response_text exists on your ApiResponseEvent type if (event.response_text) { event.response_text = safeTruncate(event.response_text, 4096); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const uiEvent = { - ...event, + // FIX: Clear structured data found by bot + if (event.response && (event.response as any).candidates) { + (event.response as any).candidates = undefined; + } + if (event.prompt && (event.prompt as any).contents) { + (event.prompt as any).contents = undefined; + } + + // FIX: Explicit mapping to avoid @typescript-eslint/no-misused-spread + const uiEvent: UiEvent = { 'event.name': EVENT_API_RESPONSE, 'event.timestamp': new Date().toISOString(), - } as UiEvent; + status_code: event.status_code, + duration_ms: event.duration_ms, + response_text: event.response_text, + auth_type: event.auth_type, + model: event.model, + role: event.role, + }; uiTelemetryService.addEvent(uiEvent); - - ClearcutLogger.getInstance(config)?.logApiResponseEvent(event); - - bufferTelemetryEvent(() => { - const logger = logs.getLogger(SERVICE_NAME); - - // Because we truncated 'event.response_text' at the top of the function, - // these log records now only contain the safe, small string. - logger.emit(event.toLogRecord(config)); - logger.emit(event.toSemanticLogRecord(config)); - - const conventionAttributes = getConventionAttributes(event); - - recordApiResponseMetrics(config, event.duration_ms, { - model: event.model, - status_code: event.status_code, - genAiAttributes: conventionAttributes, - }); - - const tokenUsageData = [ - { count: event.usage.input_token_count, type: 'input' as const }, - { count: event.usage.output_token_count, type: 'output' as const }, - { count: event.usage.cached_content_token_count, type: 'cache' as const }, - { count: event.usage.thoughts_token_count, type: 'thought' as const }, - { count: event.usage.tool_token_count, type: 'tool' as const }, - ]; - - for (const { count, type } of tokenUsageData) { - recordTokenUsageMetrics(config, count, { - model: event.model, - type, - genAiAttributes: conventionAttributes, - }); - } - }); } export function logLoopDetected(