diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 4265805e09..c4575c89fd 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -98,6 +98,7 @@ export function createMockConfig( getMcpServers: vi.fn().mockReturnValue({}), }), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getGitService: vi.fn(), validatePathAccess: vi.fn().mockReturnValue(undefined), getShellExecutionConfig: vi.fn().mockReturnValue({ diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index fcfd604e3a..9f36de0fa7 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -2966,6 +2966,11 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record< description: 'Protocol for OTLP exporters.', enum: ['grpc', 'http'], }, + traces: { + type: 'boolean', + description: + 'Whether detailed traces with large attributes are captured.', + }, logPrompts: { type: 'boolean', description: 'Whether prompts are logged in telemetry payloads.', diff --git a/packages/cli/src/gemini_cleanup.test.tsx b/packages/cli/src/gemini_cleanup.test.tsx index 2df1ab4d82..a13e36fb65 100644 --- a/packages/cli/src/gemini_cleanup.test.tsx +++ b/packages/cli/src/gemini_cleanup.test.tsx @@ -241,6 +241,7 @@ describe('gemini.tsx main function cleanup', () => { getCoreTools: vi.fn(() => []), getTelemetryEnabled: vi.fn(() => false), getTelemetryLogPromptsEnabled: vi.fn(() => false), + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: vi.fn(() => true), getOutputFormat: vi.fn(() => 'text'), getUsageStatisticsEnabled: vi.fn(() => false), @@ -325,6 +326,7 @@ describe('gemini.tsx main function cleanup', () => { getCoreTools: vi.fn(() => []), getTelemetryEnabled: vi.fn(() => false), getTelemetryLogPromptsEnabled: vi.fn(() => false), + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: vi.fn(() => true), getOutputFormat: vi.fn(() => 'text'), getUsageStatisticsEnabled: vi.fn(() => false), diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts index 6561ac1db0..e8c6744e8a 100644 --- a/packages/cli/src/test-utils/mockConfig.ts +++ b/packages/cli/src/test-utils/mockConfig.ts @@ -87,6 +87,7 @@ export const createMockConfig = (overrides: Partial = {}): Config => getAccessibility: vi.fn().mockReturnValue({}), getTelemetryEnabled: vi.fn().mockReturnValue(false), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getTelemetryOtlpEndpoint: vi.fn().mockReturnValue(''), getTelemetryOtlpProtocol: vi.fn().mockReturnValue('grpc'), getTelemetryTarget: vi.fn().mockReturnValue(''), diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index d6c68ec880..09c88ab2ea 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -308,6 +308,8 @@ describe('useGeminiStream', () => { sandbox: false, targetDir: '/test/dir', debugMode: false, + getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), question: undefined, coreTools: [], toolDiscoveryCommand: undefined, diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index eee0241a58..f399913eac 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1559,6 +1559,8 @@ export const useGeminiStream = ( operation: options?.isContinuation ? GeminiCliOperation.SystemPrompt : GeminiCliOperation.UserPrompt, + logPrompts: config.getTelemetryLogPromptsEnabled(), + tracesEnabled: config.getTelemetryTracesEnabled(), sessionId: config.getSessionId(), }, async ({ metadata: spanMetadata }) => { diff --git a/packages/core/src/agents/agent-tool.ts b/packages/core/src/agents/agent-tool.ts index d24636915c..899266f77f 100644 --- a/packages/core/src/agents/agent-tool.ts +++ b/packages/core/src/agents/agent-tool.ts @@ -194,6 +194,7 @@ class DelegateInvocation extends BaseToolInvocation< { operation: GeminiCliOperation.AgentCall, logPrompts: this.context.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.context.config.getTelemetryTracesEnabled(), sessionId: this.context.config.getSessionId(), attributes: { [GEN_AI_AGENT_NAME]: this.definition.name, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 258e58ea35..6d11d68116 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -202,6 +202,7 @@ export interface PlanSettings { export interface TelemetrySettings { enabled?: boolean; + traces?: boolean; target?: TelemetryTarget; otlpEndpoint?: string; otlpProtocol?: 'grpc' | 'http'; @@ -1050,6 +1051,7 @@ export class Config implements McpContext, AgentLoopContext { this.accessibility = params.accessibility ?? {}; this.telemetrySettings = { enabled: params.telemetry?.enabled ?? false, + traces: params.telemetry?.traces ?? false, target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET, otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT, otlpProtocol: params.telemetry?.otlpProtocol, @@ -2660,6 +2662,10 @@ export class Config implements McpContext, AgentLoopContext { return this.telemetrySettings.enabled ?? false; } + getTelemetryTracesEnabled(): boolean { + return this.telemetrySettings.traces ?? false; + } + getTelemetryLogPromptsEnabled(): boolean { return this.telemetrySettings.logPrompts ?? true; } diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index d4a3f40aad..4beb14ea06 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -153,6 +153,7 @@ describe('GeminiChat', () => { promptId: 'test-session-id', getSessionId: () => 'test-session-id', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getContentGeneratorConfig: vi.fn().mockImplementation(() => ({ diff --git a/packages/core/src/core/geminiChat_network_retry.test.ts b/packages/core/src/core/geminiChat_network_retry.test.ts index 4683e29261..8d8d7778d1 100644 --- a/packages/core/src/core/geminiChat_network_retry.test.ts +++ b/packages/core/src/core/geminiChat_network_retry.test.ts @@ -96,6 +96,7 @@ describe('GeminiChat Network Retries', () => { promptId: 'test-session-id', getSessionId: () => 'test-session-id', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getContentGeneratorConfig: vi.fn().mockReturnValue({ diff --git a/packages/core/src/core/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator.test.ts index 7f3b1a9f33..872841dd89 100644 --- a/packages/core/src/core/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator.test.ts @@ -73,6 +73,7 @@ describe('LoggingContentGenerator', () => { authType: 'API_KEY', }), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true), + getTelemetryTracesEnabled: () => false, refreshUserQuotaIfStale: vi.fn().mockResolvedValue(undefined), getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Config; diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator.ts index 1c8579df9a..d27b8a8f32 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator.ts @@ -361,6 +361,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, @@ -452,6 +453,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, @@ -607,6 +609,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, diff --git a/packages/core/src/scheduler/policy.test.ts b/packages/core/src/scheduler/policy.test.ts index c228ead10d..f89d2b3c1b 100644 --- a/packages/core/src/scheduler/policy.test.ts +++ b/packages/core/src/scheduler/policy.test.ts @@ -858,6 +858,7 @@ describe('Plan Mode Denial Consistency', () => { getEnableHooks: vi.fn().mockReturnValue(false), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: () => false, setApprovalMode: vi.fn(), getSessionId: vi.fn().mockReturnValue('test-session-id'), getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), diff --git a/packages/core/src/scheduler/scheduler.test.ts b/packages/core/src/scheduler/scheduler.test.ts index aaa5d48f5d..9523a0507a 100644 --- a/packages/core/src/scheduler/scheduler.test.ts +++ b/packages/core/src/scheduler/scheduler.test.ts @@ -178,6 +178,7 @@ describe('Scheduler (Orchestrator)', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: () => false, getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; @@ -1517,6 +1518,7 @@ describe('Scheduler MCP Progress', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: () => false, getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index fef22968e1..709bdc2bf5 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -196,6 +196,7 @@ export class Scheduler { { operation: GeminiCliOperation.ScheduleToolCalls, logPrompts: this.context.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.context.config.getTelemetryTracesEnabled(), sessionId: this.context.config.getSessionId(), }, async ({ metadata: spanMetadata }) => { diff --git a/packages/core/src/scheduler/scheduler_hooks.test.ts b/packages/core/src/scheduler/scheduler_hooks.test.ts index 3134ccd701..e3dc824d8b 100644 --- a/packages/core/src/scheduler/scheduler_hooks.test.ts +++ b/packages/core/src/scheduler/scheduler_hooks.test.ts @@ -71,6 +71,7 @@ function createMockConfig(overrides: Partial = {}): Config { getEnableHooks: () => true, getExperiments: () => {}, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getPolicyEngine: () => ({ check: async () => ({ decision: 'allow' }), diff --git a/packages/core/src/scheduler/scheduler_parallel.test.ts b/packages/core/src/scheduler/scheduler_parallel.test.ts index 9229a94550..63f1f89715 100644 --- a/packages/core/src/scheduler/scheduler_parallel.test.ts +++ b/packages/core/src/scheduler/scheduler_parallel.test.ts @@ -218,6 +218,7 @@ describe('Scheduler Parallel Execution', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: () => false, getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index 3910aaee47..3d9ad1e063 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -84,6 +84,7 @@ export class ToolExecutor { { operation: GeminiCliOperation.ToolCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_TOOL_NAME]: toolName, diff --git a/packages/core/src/telemetry/config.ts b/packages/core/src/telemetry/config.ts index bd7cbdf09c..9fd4bacfc3 100644 --- a/packages/core/src/telemetry/config.ts +++ b/packages/core/src/telemetry/config.ts @@ -60,6 +60,10 @@ export async function resolveTelemetrySettings(options: { parseBooleanEnvFlag(env['GEMINI_TELEMETRY_ENABLED']) ?? settings.enabled; + const traces = + parseBooleanEnvFlag(env['GEMINI_TELEMETRY_TRACES_ENABLED']) ?? + settings.traces; + const rawTarget = argv.telemetryTarget ?? env['GEMINI_TELEMETRY_TARGET'] ?? @@ -110,6 +114,7 @@ export async function resolveTelemetrySettings(options: { return { enabled, + traces, target, otlpEndpoint, otlpProtocol, diff --git a/packages/core/src/telemetry/conseca-logger.test.ts b/packages/core/src/telemetry/conseca-logger.test.ts index 0eac29276f..1b6dec35c8 100644 --- a/packages/core/src/telemetry/conseca-logger.test.ts +++ b/packages/core/src/telemetry/conseca-logger.test.ts @@ -37,6 +37,7 @@ describe('conseca-logger', () => { getTelemetryEnabled: vi.fn().mockReturnValue(true), getSessionId: vi.fn().mockReturnValue('test-session-id'), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true), + getTelemetryTracesEnabled: () => false, isInteractive: vi.fn().mockReturnValue(true), getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }), getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }), diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index b21fc606e2..869e190079 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -216,6 +216,7 @@ describe('loggers', () => { getTelemetryEnabled: () => true, getUsageStatisticsEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: () => true, getFileFilteringAllowBuildArtifacts: () => false, getDebugMode: () => true, @@ -313,6 +314,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, isInteractive: () => false, getExperiments: () => undefined, @@ -352,6 +354,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getTargetDir: () => 'target-dir', getUsageStatisticsEnabled: () => true, isInteractive: () => false, @@ -392,6 +395,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -596,6 +600,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -756,6 +761,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -834,6 +840,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, // Enabled + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -923,6 +930,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => false, // Disabled + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -978,6 +986,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -1140,6 +1149,7 @@ describe('loggers', () => { getCoreTools: () => ['ls', 'read-file'], getApprovalMode: () => 'default', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: () => true, getFileFilteringAllowBuildArtifacts: () => false, getDebugMode: () => true, @@ -1170,6 +1180,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -1829,6 +1840,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -2423,6 +2435,7 @@ describe('loggers', () => { getExperiments: () => undefined, getExperimentsAsync: async () => undefined, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getContentGeneratorConfig: () => undefined, } as unknown as Config; diff --git a/packages/core/src/telemetry/sanitize.test.ts b/packages/core/src/telemetry/sanitize.test.ts index 71863011c0..996f1392e3 100644 --- a/packages/core/src/telemetry/sanitize.test.ts +++ b/packages/core/src/telemetry/sanitize.test.ts @@ -26,6 +26,7 @@ import type { Config } from '../config/config.js'; function createMockConfig(logPromptsEnabled: boolean): Config { return { getTelemetryLogPromptsEnabled: () => logPromptsEnabled, + getTelemetryTracesEnabled: () => false, getSessionId: () => 'test-session-id', getExperiments: () => undefined, getExperimentsAsync: async () => undefined, diff --git a/packages/core/src/telemetry/trace.test.ts b/packages/core/src/telemetry/trace.test.ts index 87a1419080..79319cacd2 100644 --- a/packages/core/src/telemetry/trace.test.ts +++ b/packages/core/src/telemetry/trace.test.ts @@ -62,13 +62,28 @@ describe('truncateForTelemetry', () => { expect(result).toBe('👋🌍...[TRUNCATED: original length 10]'); }); - 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]' } }, + }), ); }); diff --git a/packages/core/src/telemetry/trace.ts b/packages/core/src/telemetry/trace.ts index fd3082c3cd..9ec2de4736 100644 --- a/packages/core/src/telemetry/trace.ts +++ b/packages/core/src/telemetry/trace.ts @@ -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 = {}; + 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(value: T): value is T & AsyncIterable { @@ -125,13 +175,14 @@ export async function runInDevTraceSpan( operation: GeminiCliOperation; logPrompts?: boolean; sessionId: string; + tracesEnabled?: boolean; }, fn: ({ metadata }: { metadata: SpanMetadata }) => Promise, ): Promise { - const { operation, logPrompts, sessionId, ...restOfSpanOpts } = opts; + const { operation, logPrompts, sessionId, tracesEnabled, ...restOfSpanOpts } = + opts; - const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION); - return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => { + if (tracesEnabled === false) { const meta: SpanMetadata = { name: operation, attributes: { @@ -141,87 +192,113 @@ export async function runInDevTraceSpan( [GEN_AI_CONVERSATION_ID]: sessionId, }, }; - let spanEnded = false; - const endSpan = () => { - if (spanEnded) { - return; - } - spanEnded = true; - try { - if (logPrompts !== false) { - if (meta.input !== undefined) { - const truncated = truncateForTelemetry(meta.input); - if (truncated !== undefined) { - span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated); + return fn({ metadata: meta }); + } + + const spanOptsWithSession: SpanOptions = { + ...restOfSpanOpts, + attributes: { + ...restOfSpanOpts.attributes, + [GEN_AI_CONVERSATION_ID]: sessionId, + }, + }; + + const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION); + return tracer.startActiveSpan( + operation, + spanOptsWithSession, + async (span) => { + const meta: SpanMetadata = { + name: operation, + attributes: { + [GEN_AI_OPERATION_NAME]: operation, + [GEN_AI_AGENT_NAME]: SERVICE_NAME, + [GEN_AI_AGENT_DESCRIPTION]: SERVICE_DESCRIPTION, + [GEN_AI_CONVERSATION_ID]: sessionId, + }, + }; + let spanEnded = false; + const endSpan = () => { + if (spanEnded) { + return; + } + spanEnded = true; + try { + if (logPrompts !== false) { + if (meta.input !== undefined) { + const truncated = truncateForTelemetry(meta.input); + if (truncated !== undefined) { + span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated); + } + } + if (meta.output !== undefined) { + const truncated = truncateForTelemetry(meta.output); + if (truncated !== undefined) { + span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated); + } } } - if (meta.output !== undefined) { - const truncated = truncateForTelemetry(meta.output); + for (const [key, value] of Object.entries(meta.attributes)) { + const truncated = truncateForTelemetry(value); if (truncated !== undefined) { - span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated); + span.setAttribute(key, truncated); } } - } - for (const [key, value] of Object.entries(meta.attributes)) { - const truncated = truncateForTelemetry(value); - if (truncated !== undefined) { - span.setAttribute(key, truncated); + if (meta.error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: getErrorMessage(meta.error), + }); + if (meta.error instanceof Error) { + span.recordException(meta.error); + } + } else { + span.setStatus({ code: SpanStatusCode.OK }); } - } - if (meta.error) { + } catch (e) { + // Log the error but don't rethrow, to ensure span.end() is called. + diag.error('Error setting span attributes in endSpan', e); span.setStatus({ code: SpanStatusCode.ERROR, - message: getErrorMessage(meta.error), + message: `Error in endSpan: ${getErrorMessage(e)}`, }); - if (meta.error instanceof Error) { - span.recordException(meta.error); - } - } else { - span.setStatus({ code: SpanStatusCode.OK }); + } finally { + span.end(); } - } catch (e) { - // Log the error but don't rethrow, to ensure span.end() is called. - diag.error('Error setting span attributes in endSpan', e); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: `Error in endSpan: ${getErrorMessage(e)}`, - }); + }; + + let isStream = false; + try { + const result = await fn({ metadata: meta }); + + if (isAsyncIterable(result)) { + isStream = true; + const streamWrapper = (async function* () { + try { + yield* result; + } catch (e: unknown) { + meta.error = e; + throw e; + } finally { + endSpan(); + } + })(); + + const finalResult = Object.assign(streamWrapper, result); + spanRegistry.register(finalResult, endSpan); + return finalResult; + } + return result; + } catch (e: unknown) { + meta.error = e; + throw e; } finally { - span.end(); + if (!isStream) { + endSpan(); + } } - }; - - let isStream = false; - try { - const result = await fn({ metadata: meta }); - - if (isAsyncIterable(result)) { - isStream = true; - const streamWrapper = (async function* () { - try { - yield* result; - } catch (e: unknown) { - meta.error = e; - throw e; - } finally { - endSpan(); - } - })(); - - const finalResult = Object.assign(streamWrapper, result); - spanRegistry.register(finalResult, endSpan); - return finalResult; - } - return result; - } catch (e: unknown) { - meta.error = e; - throw e; - } finally { - if (!isStream) { - endSpan(); - } - } - }); + }, + ); } /** diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 98bc786410..ad090051cb 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -3561,6 +3561,10 @@ "description": "Protocol for OTLP exporters.", "enum": ["grpc", "http"] }, + "traces": { + "type": "boolean", + "description": "Whether detailed traces with large attributes are captured." + }, "logPrompts": { "type": "boolean", "description": "Whether prompts are logged in telemetry payloads."