/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { diag, metrics, ValueType, type Attributes, type Meter, type Counter, type Histogram, } from '@opentelemetry/api'; import { SERVICE_NAME } from './constants.js'; import type { Config } from '../config/config.js'; import type { ModelRoutingEvent, ModelSlashCommandEvent, AgentFinishEvent, RecoveryAttemptEvent, KeychainAvailabilityEvent, TokenStorageInitializationEvent, } from './types.js'; import { AuthType } from '../core/contentGenerator.js'; import { getCommonMetricAttributes } from './telemetryAttributes.js'; import { sanitizeHookName } from './sanitize.js'; const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression'; const TOOL_CALL_COUNT = 'gemini_cli.tool.call.count'; const TOOL_CALL_LATENCY = 'gemini_cli.tool.call.latency'; const API_REQUEST_COUNT = 'gemini_cli.api.request.count'; const API_REQUEST_LATENCY = 'gemini_cli.api.request.latency'; const TOKEN_USAGE = 'gemini_cli.token.usage'; const SESSION_COUNT = 'gemini_cli.session.count'; const FILE_OPERATION_COUNT = 'gemini_cli.file.operation.count'; const LINES_CHANGED = 'gemini_cli.lines.changed'; const INVALID_CHUNK_COUNT = 'gemini_cli.chat.invalid_chunk.count'; const CONTENT_RETRY_COUNT = 'gemini_cli.chat.content_retry.count'; const CONTENT_RETRY_FAILURE_COUNT = 'gemini_cli.chat.content_retry_failure.count'; const NETWORK_RETRY_COUNT = 'gemini_cli.network_retry.count'; const MODEL_ROUTING_LATENCY = 'gemini_cli.model_routing.latency'; const MODEL_ROUTING_FAILURE_COUNT = 'gemini_cli.model_routing.failure.count'; const MODEL_SLASH_COMMAND_CALL_COUNT = 'gemini_cli.slash_command.model.call_count'; const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count'; const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency'; const KEYCHAIN_AVAILABILITY_COUNT = 'gemini_cli.keychain.availability.count'; const TOKEN_STORAGE_TYPE_COUNT = 'gemini_cli.token_storage.type.count'; const OVERAGE_OPTION_COUNT = 'gemini_cli.overage_option.count'; const CREDIT_PURCHASE_COUNT = 'gemini_cli.credit_purchase.count'; const EVENT_ONBOARDING_START = 'gemini_cli.onboarding.start'; const EVENT_ONBOARDING_SUCCESS = 'gemini_cli.onboarding.success'; const EVENT_ONBOARDING_DURATION_MS = 'gemini_cli.onboarding.duration'; // Agent Metrics const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count'; const AGENT_DURATION_MS = 'gemini_cli.agent.duration'; const AGENT_TURNS = 'gemini_cli.agent.turns'; const AGENT_RECOVERY_ATTEMPT_COUNT = 'gemini_cli.agent.recovery_attempt.count'; const AGENT_RECOVERY_ATTEMPT_DURATION = 'gemini_cli.agent.recovery_attempt.duration'; // Browser Agent Metrics const BROWSER_AGENT_CONNECTION_DURATION = 'gemini_cli.browser_agent.connection.duration'; const BROWSER_AGENT_CONNECTION_FAILURE_COUNT = 'gemini_cli.browser_agent.connection.failure.count'; const BROWSER_AGENT_TOOLS_DISCOVERED = 'gemini_cli.browser_agent.tools.discovered'; const BROWSER_AGENT_TOOLS_MISSING_SEMANTIC = 'gemini_cli.browser_agent.tools.missing_semantic'; const BROWSER_AGENT_VISION_STATUS = 'gemini_cli.browser_agent.vision.status'; const BROWSER_AGENT_TASK_OUTCOME = 'gemini_cli.browser_agent.task.outcome'; const BROWSER_AGENT_TASK_DURATION = 'gemini_cli.browser_agent.task.duration'; const BROWSER_AGENT_CLEANUP_DURATION = 'gemini_cli.browser_agent.cleanup.duration'; const BROWSER_AGENT_CLEANUP_FAILURE_COUNT = 'gemini_cli.browser_agent.cleanup.failure.count'; // OpenTelemetry GenAI Semantic Convention Metrics const GEN_AI_CLIENT_TOKEN_USAGE = 'gen_ai.client.token.usage'; const GEN_AI_CLIENT_OPERATION_DURATION = 'gen_ai.client.operation.duration'; // Performance Monitoring Metrics const STARTUP_TIME = 'gemini_cli.startup.duration'; const MEMORY_USAGE = 'gemini_cli.memory.usage'; const CPU_USAGE = 'gemini_cli.cpu.usage'; const EVENT_LOOP_DELAY = 'gemini_cli.event_loop.delay'; const TOOL_QUEUE_DEPTH = 'gemini_cli.tool.queue.depth'; const TOOL_EXECUTION_BREAKDOWN = 'gemini_cli.tool.execution.breakdown'; const TOKEN_EFFICIENCY = 'gemini_cli.token.efficiency'; const API_REQUEST_BREAKDOWN = 'gemini_cli.api.request.breakdown'; const PERFORMANCE_SCORE = 'gemini_cli.performance.score'; const REGRESSION_DETECTION = 'gemini_cli.performance.regression'; const REGRESSION_PERCENTAGE_CHANGE = 'gemini_cli.performance.regression.percentage_change'; const BASELINE_COMPARISON = 'gemini_cli.performance.baseline.comparison'; const FLICKER_FRAME_COUNT = 'gemini_cli.ui.flicker.count'; const SLOW_RENDER_LATENCY = 'gemini_cli.ui.slow_render.latency'; const EXIT_FAIL_COUNT = 'gemini_cli.exit.fail.count'; const PLAN_EXECUTION_COUNT = 'gemini_cli.plan.execution.count'; const baseMetricDefinition = { getCommonAttributes: getCommonMetricAttributes, }; const COUNTER_DEFINITIONS = { [TOOL_CALL_COUNT]: { description: 'Counts tool calls, tagged by function name and success.', valueType: ValueType.INT, assign: (c: Counter) => (toolCallCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { function_name: string; success: boolean; decision?: 'accept' | 'reject' | 'modify' | 'auto_accept'; tool_type?: 'native' | 'mcp'; }, }, [API_REQUEST_COUNT]: { description: 'Counts API requests, tagged by model and status.', valueType: ValueType.INT, assign: (c: Counter) => (apiRequestCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; status_code?: number | string; error_type?: string; }, }, [TOKEN_USAGE]: { description: 'Counts the total number of tokens used.', valueType: ValueType.INT, assign: (c: Counter) => (tokenUsageCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; type: 'input' | 'output' | 'thought' | 'cache' | 'tool'; }, }, [SESSION_COUNT]: { description: 'Count of CLI sessions started.', valueType: ValueType.INT, assign: (c: Counter) => (sessionCounter = c), attributes: {} as Record, }, [FILE_OPERATION_COUNT]: { description: 'Counts file operations (create, read, update).', valueType: ValueType.INT, assign: (c: Counter) => (fileOperationCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { operation: FileOperation; lines?: number; mimetype?: string; extension?: string; programming_language?: string; }, }, [LINES_CHANGED]: { description: 'Number of lines changed (from file diffs).', valueType: ValueType.INT, assign: (c: Counter) => (linesChangedCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { function_name?: string; type: 'added' | 'removed'; }, }, [INVALID_CHUNK_COUNT]: { description: 'Counts invalid chunks received from a stream.', valueType: ValueType.INT, assign: (c: Counter) => (invalidChunkCounter = c), attributes: {} as Record, }, [CONTENT_RETRY_COUNT]: { description: 'Counts retries due to content errors (e.g., empty stream).', valueType: ValueType.INT, assign: (c: Counter) => (contentRetryCounter = c), attributes: {} as Record, }, [CONTENT_RETRY_FAILURE_COUNT]: { description: 'Counts occurrences of all content retries failing.', valueType: ValueType.INT, assign: (c: Counter) => (contentRetryFailureCounter = c), attributes: {} as Record, }, [NETWORK_RETRY_COUNT]: { description: 'Counts network retries.', valueType: ValueType.INT, assign: (c: Counter) => (networkRetryCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; attempt: number; }, }, [MODEL_ROUTING_FAILURE_COUNT]: { description: 'Counts model routing failures.', valueType: ValueType.INT, assign: (c: Counter) => (modelRoutingFailureCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { 'routing.decision_source': string; 'routing.error_message': string; }, }, [MODEL_SLASH_COMMAND_CALL_COUNT]: { description: 'Counts model slash command calls.', valueType: ValueType.INT, assign: (c: Counter) => (modelSlashCommandCallCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { 'slash_command.model.model_name': string; }, }, [EVENT_CHAT_COMPRESSION]: { description: 'Counts chat compression events.', valueType: ValueType.INT, assign: (c: Counter) => (chatCompressionCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { tokens_before: number; tokens_after: number; }, }, [AGENT_RUN_COUNT]: { description: 'Counts agent runs, tagged by name and termination reason.', valueType: ValueType.INT, assign: (c: Counter) => (agentRunCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { agent_name: string; terminate_reason: string; }, }, [AGENT_RECOVERY_ATTEMPT_COUNT]: { description: 'Counts agent recovery attempts.', valueType: ValueType.INT, assign: (c: Counter) => (agentRecoveryAttemptCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { agent_name: string; reason: string; success: boolean; }, }, [FLICKER_FRAME_COUNT]: { description: 'Counts UI frames that flicker (render taller than the terminal).', valueType: ValueType.INT, assign: (c: Counter) => (flickerFrameCounter = c), attributes: {} as Record, }, [EXIT_FAIL_COUNT]: { description: 'Counts CLI exit failures.', valueType: ValueType.INT, assign: (c: Counter) => (exitFailCounter = c), attributes: {} as Record, }, [PLAN_EXECUTION_COUNT]: { description: 'Counts plan executions (switching from Plan Mode).', valueType: ValueType.INT, assign: (c: Counter) => (planExecutionCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { approval_mode: string; }, }, [EVENT_HOOK_CALL_COUNT]: { description: 'Counts hook calls, tagged by hook event name and success.', valueType: ValueType.INT, assign: (c: Counter) => (hookCallCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { hook_event_name: string; hook_name: string; success: boolean; }, }, [KEYCHAIN_AVAILABILITY_COUNT]: { description: 'Counts keychain availability checks.', valueType: ValueType.INT, assign: (c: Counter) => (keychainAvailabilityCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { available: boolean; }, }, [TOKEN_STORAGE_TYPE_COUNT]: { description: 'Counts token storage type initializations.', valueType: ValueType.INT, assign: (c: Counter) => (tokenStorageTypeCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { type: string; forced: boolean; }, }, [OVERAGE_OPTION_COUNT]: { description: 'Counts overage option selections.', valueType: ValueType.INT, assign: (c: Counter) => (overageOptionCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { selected_option: string; model: string; }, }, [CREDIT_PURCHASE_COUNT]: { description: 'Counts credit purchase link clicks.', valueType: ValueType.INT, assign: (c: Counter) => (creditPurchaseCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { source: string; model: string; }, }, [BROWSER_AGENT_CONNECTION_FAILURE_COUNT]: { description: 'Counts browser agent MCP connection failures.', valueType: ValueType.INT, assign: (c: Counter) => (browserAgentConnectionFailureCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { session_mode: 'persistent' | 'isolated' | 'existing'; headless: boolean; error_type: | 'profile_locked' | 'timeout' | 'connection_refused' | 'unknown'; }, }, [BROWSER_AGENT_TOOLS_MISSING_SEMANTIC]: { description: 'Counts missing required semantic tools discovered from MCP.', valueType: ValueType.INT, assign: (c: Counter) => (browserAgentToolsMissingSemanticCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { tool_name: string }, }, [BROWSER_AGENT_VISION_STATUS]: { description: 'Counts browser agent invocations by vision status.', valueType: ValueType.INT, assign: (c: Counter) => (browserAgentVisionStatusCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { enabled: boolean; disabled_reason?: | 'no_visual_model' | 'missing_visual_tools' | 'blocked_auth_type'; }, }, [BROWSER_AGENT_TASK_OUTCOME]: { description: 'Counts browser agent task outcomes.', valueType: ValueType.INT, assign: (c: Counter) => (browserAgentTaskOutcomeCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { success: boolean; session_mode: 'persistent' | 'isolated' | 'existing'; vision_enabled: boolean; headless: boolean; }, }, [BROWSER_AGENT_CLEANUP_FAILURE_COUNT]: { description: 'Counts browser agent cleanup failures.', valueType: ValueType.INT, assign: (c: Counter) => (browserAgentCleanupFailureCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { session_mode: 'persistent' | 'isolated' | 'existing'; }, }, [EVENT_ONBOARDING_START]: { description: 'Counts onboarding started', valueType: ValueType.INT, assign: (c: Counter) => (onboardingStartCounter = c), attributes: {} as Record, }, [EVENT_ONBOARDING_SUCCESS]: { description: 'Counts onboarding succeeded', valueType: ValueType.INT, assign: (c: Counter) => (onboardingSuccessCounter = c), attributes: {} as { user_tier?: string; }, }, } as const; const HISTOGRAM_DEFINITIONS = { [TOOL_CALL_LATENCY]: { description: 'Latency of tool calls in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (toolCallLatencyHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { function_name: string; }, }, [API_REQUEST_LATENCY]: { description: 'Latency of API requests in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (apiRequestLatencyHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; }, }, [MODEL_ROUTING_LATENCY]: { description: 'Latency of model routing decisions in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (modelRoutingLatencyHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { 'routing.decision_model': string; 'routing.decision_source': string; }, }, [AGENT_DURATION_MS]: { description: 'Duration of agent runs in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (agentDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { agent_name: string; }, }, [SLOW_RENDER_LATENCY]: { description: 'Counts UI frames that take too long to render.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (slowRenderHistogram = h), attributes: {} as Record, }, [AGENT_TURNS]: { description: 'Number of turns taken by agents.', unit: 'turns', valueType: ValueType.INT, assign: (h: Histogram) => (agentTurnsHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { agent_name: string; }, }, [AGENT_RECOVERY_ATTEMPT_DURATION]: { description: 'Duration of agent recovery attempts in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (agentRecoveryAttemptDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { agent_name: string; }, }, [GEN_AI_CLIENT_TOKEN_USAGE]: { description: 'Number of input and output tokens used.', unit: 'token', valueType: ValueType.INT, assign: (h: Histogram) => (genAiClientTokenUsageHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { 'gen_ai.operation.name': string; 'gen_ai.provider.name': string; 'gen_ai.token.type': 'input' | 'output'; 'gen_ai.request.model'?: string; 'gen_ai.response.model'?: string; 'server.address'?: string; 'server.port'?: number; }, }, [GEN_AI_CLIENT_OPERATION_DURATION]: { description: 'GenAI operation duration.', unit: 's', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (genAiClientOperationDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { 'gen_ai.operation.name': string; 'gen_ai.provider.name': string; 'gen_ai.request.model'?: string; 'gen_ai.response.model'?: string; 'server.address'?: string; 'server.port'?: number; 'error.type'?: string; }, }, [EVENT_HOOK_CALL_LATENCY]: { description: 'Latency of hook calls in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (c: Histogram) => (hookCallLatencyHistogram = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { hook_event_name: string; hook_name: string; success: boolean; }, }, [BROWSER_AGENT_CONNECTION_DURATION]: { description: 'Duration of browser agent MCP connection setup in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (browserAgentConnectionDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { session_mode: 'persistent' | 'isolated' | 'existing'; headless: boolean; success: boolean; }, }, [BROWSER_AGENT_TOOLS_DISCOVERED]: { description: 'Count of tools discovered from chrome-devtools-mcp.', unit: 'tools', valueType: ValueType.INT, assign: (h: Histogram) => (browserAgentToolsDiscoveredHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { session_mode: 'persistent' | 'isolated' | 'existing'; }, }, [BROWSER_AGENT_TASK_DURATION]: { description: 'Full invocation duration of browser agent (connect + run + cleanup) in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (browserAgentTaskDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { success: boolean; session_mode: 'persistent' | 'isolated' | 'existing'; }, }, [BROWSER_AGENT_CLEANUP_DURATION]: { description: 'Duration of browser agent cleanup in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (browserAgentCleanupDurationHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { session_mode: 'persistent' | 'isolated' | 'existing'; }, }, [EVENT_ONBOARDING_DURATION_MS]: { description: 'Duration of onboarding in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (onboardingDurationHistogram = h), attributes: {} as { user_tier?: string; }, }, } as const; const PERFORMANCE_COUNTER_DEFINITIONS = { [REGRESSION_DETECTION]: { description: 'Performance regression detection events.', valueType: ValueType.INT, assign: (c: Counter) => (regressionDetectionCounter = c), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { metric: string; severity: 'low' | 'medium' | 'high'; current_value: number; baseline_value: number; }, }, } as const; const PERFORMANCE_HISTOGRAM_DEFINITIONS = { [STARTUP_TIME]: { description: 'CLI startup time in milliseconds, broken down by initialization phase.', unit: 'ms', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (startupTimeHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { phase: string; details?: Record; }, }, [MEMORY_USAGE]: { description: 'Memory usage in bytes.', unit: 'bytes', valueType: ValueType.INT, assign: (h: Histogram) => (memoryUsageGauge = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { memory_type: MemoryMetricType; component?: string; }, }, [CPU_USAGE]: { description: 'CPU usage percentage.', unit: 'percent', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (cpuUsageGauge = h), attributes: {} as { component?: string; }, }, [EVENT_LOOP_DELAY]: { description: 'Event loop delay in milliseconds.', unit: 'ms', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (eventLoopDelayHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { percentile: string; component?: string; }, }, [TOOL_QUEUE_DEPTH]: { description: 'Number of tools in execution queue.', unit: 'count', valueType: ValueType.INT, assign: (h: Histogram) => (toolQueueDepthGauge = h), attributes: {} as Record, }, [TOOL_EXECUTION_BREAKDOWN]: { description: 'Tool execution time breakdown by phase in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (toolExecutionBreakdownHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { function_name: string; phase: ToolExecutionPhase; }, }, [TOKEN_EFFICIENCY]: { description: 'Token efficiency metrics (tokens per operation, cache hit rate, etc.).', unit: 'ratio', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (tokenEfficiencyHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; metric: string; context?: string; }, }, [API_REQUEST_BREAKDOWN]: { description: 'API request time breakdown by phase in milliseconds.', unit: 'ms', valueType: ValueType.INT, assign: (h: Histogram) => (apiRequestBreakdownHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { model: string; phase: ApiRequestPhase; }, }, [PERFORMANCE_SCORE]: { description: 'Composite performance score (0-100).', unit: 'score', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (performanceScoreGauge = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { category: string; baseline?: number; }, }, [REGRESSION_PERCENTAGE_CHANGE]: { description: 'Percentage change compared to baseline for detected regressions.', unit: 'percent', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (regressionPercentageChangeHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { metric: string; severity: 'low' | 'medium' | 'high'; current_value: number; baseline_value: number; }, }, [BASELINE_COMPARISON]: { description: 'Performance comparison to established baseline (percentage change).', unit: 'percent', valueType: ValueType.DOUBLE, assign: (h: Histogram) => (baselineComparisonHistogram = h), // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion attributes: {} as { metric: string; category: string; current_value: number; baseline_value: number; }, }, } as const; type AllMetricDefs = typeof COUNTER_DEFINITIONS & typeof HISTOGRAM_DEFINITIONS & typeof PERFORMANCE_COUNTER_DEFINITIONS & typeof PERFORMANCE_HISTOGRAM_DEFINITIONS; export type MetricDefinitions = { [K in keyof AllMetricDefs]: { attributes: AllMetricDefs[K]['attributes']; }; }; export enum FileOperation { CREATE = 'create', READ = 'read', UPDATE = 'update', } export enum PerformanceMetricType { STARTUP = 'startup', MEMORY = 'memory', CPU = 'cpu', TOOL_EXECUTION = 'tool_execution', API_REQUEST = 'api_request', TOKEN_EFFICIENCY = 'token_efficiency', } export enum MemoryMetricType { HEAP_USED = 'heap_used', HEAP_TOTAL = 'heap_total', EXTERNAL = 'external', RSS = 'rss', } export enum ToolExecutionPhase { VALIDATION = 'validation', PREPARATION = 'preparation', EXECUTION = 'execution', RESULT_PROCESSING = 'result_processing', } export enum ApiRequestPhase { REQUEST_PREPARATION = 'request_preparation', NETWORK_LATENCY = 'network_latency', RESPONSE_PROCESSING = 'response_processing', TOKEN_PROCESSING = 'token_processing', } export enum GenAiOperationName { GENERATE_CONTENT = 'generate_content', } export enum GenAiProviderName { GCP_GEN_AI = 'gcp.gen_ai', GCP_VERTEX_AI = 'gcp.vertex_ai', } export enum GenAiTokenType { INPUT = 'input', OUTPUT = 'output', } let cliMeter: Meter | undefined; let toolCallCounter: Counter | undefined; let toolCallLatencyHistogram: Histogram | undefined; let apiRequestCounter: Counter | undefined; let apiRequestLatencyHistogram: Histogram | undefined; let tokenUsageCounter: Counter | undefined; let sessionCounter: Counter | undefined; let fileOperationCounter: Counter | undefined; let linesChangedCounter: Counter | undefined; let chatCompressionCounter: Counter | undefined; let invalidChunkCounter: Counter | undefined; let contentRetryCounter: Counter | undefined; let contentRetryFailureCounter: Counter | undefined; let networkRetryCounter: Counter | undefined; let modelRoutingLatencyHistogram: Histogram | undefined; let modelRoutingFailureCounter: Counter | undefined; let modelSlashCommandCallCounter: Counter | undefined; let agentRunCounter: Counter | undefined; let agentDurationHistogram: Histogram | undefined; let agentTurnsHistogram: Histogram | undefined; let agentRecoveryAttemptCounter: Counter | undefined; let agentRecoveryAttemptDurationHistogram: Histogram | undefined; let flickerFrameCounter: Counter | undefined; let exitFailCounter: Counter | undefined; let planExecutionCounter: Counter | undefined; let slowRenderHistogram: Histogram | undefined; let hookCallCounter: Counter | undefined; let hookCallLatencyHistogram: Histogram | undefined; let keychainAvailabilityCounter: Counter | undefined; let tokenStorageTypeCounter: Counter | undefined; let overageOptionCounter: Counter | undefined; let creditPurchaseCounter: Counter | undefined; let onboardingStartCounter: Counter | undefined; let onboardingSuccessCounter: Counter | undefined; let onboardingDurationHistogram: Histogram | undefined; let browserAgentConnectionDurationHistogram: Histogram | undefined; let browserAgentConnectionFailureCounter: Counter | undefined; let browserAgentToolsDiscoveredHistogram: Histogram | undefined; let browserAgentToolsMissingSemanticCounter: Counter | undefined; let browserAgentVisionStatusCounter: Counter | undefined; let browserAgentTaskOutcomeCounter: Counter | undefined; let browserAgentTaskDurationHistogram: Histogram | undefined; let browserAgentCleanupDurationHistogram: Histogram | undefined; let browserAgentCleanupFailureCounter: Counter | undefined; // OpenTelemetry GenAI Semantic Convention Metrics let genAiClientTokenUsageHistogram: Histogram | undefined; let genAiClientOperationDurationHistogram: Histogram | undefined; // Performance Monitoring Metrics let startupTimeHistogram: Histogram | undefined; let memoryUsageGauge: Histogram | undefined; // Using Histogram until ObservableGauge is available let cpuUsageGauge: Histogram | undefined; let eventLoopDelayHistogram: Histogram | undefined; let toolQueueDepthGauge: Histogram | undefined; let toolExecutionBreakdownHistogram: Histogram | undefined; let tokenEfficiencyHistogram: Histogram | undefined; let apiRequestBreakdownHistogram: Histogram | undefined; let performanceScoreGauge: Histogram | undefined; let regressionDetectionCounter: Counter | undefined; let regressionPercentageChangeHistogram: Histogram | undefined; let baselineComparisonHistogram: Histogram | undefined; let isMetricsInitialized = false; let isPerformanceMonitoringEnabled = false; function getMeter(): Meter | undefined { if (!cliMeter) { cliMeter = metrics.getMeter(SERVICE_NAME); } return cliMeter; } export function initializeMetrics(config: Config): void { if (isMetricsInitialized) return; const meter = getMeter(); if (!meter) return; // Initialize core metrics Object.entries(COUNTER_DEFINITIONS).forEach( ([name, { description, valueType, assign }]) => { assign(meter.createCounter(name, { description, valueType })); }, ); Object.entries(HISTOGRAM_DEFINITIONS).forEach( ([name, { description, unit, valueType, assign }]) => { assign(meter.createHistogram(name, { description, unit, valueType })); }, ); // Increment session counter after all metrics are initialized sessionCounter?.add(1, baseMetricDefinition.getCommonAttributes(config)); // Initialize performance monitoring metrics if enabled initializePerformanceMonitoring(config); isMetricsInitialized = true; } export function recordChatCompressionMetrics( config: Config, attributes: MetricDefinitions[typeof EVENT_CHAT_COMPRESSION]['attributes'], ) { if (!chatCompressionCounter || !isMetricsInitialized) return; chatCompressionCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } export function recordToolCallMetrics( config: Config, durationMs: number, attributes: MetricDefinitions[typeof TOOL_CALL_COUNT]['attributes'], ): void { if (!toolCallCounter || !toolCallLatencyHistogram || !isMetricsInitialized) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; toolCallCounter.add(1, metricAttributes); toolCallLatencyHistogram.record(durationMs, { ...baseMetricDefinition.getCommonAttributes(config), function_name: attributes.function_name, }); } export function recordCustomTokenUsageMetrics( config: Config, tokenCount: number, attributes: MetricDefinitions[typeof TOKEN_USAGE]['attributes'], ): void { if (!tokenUsageCounter || !isMetricsInitialized) return; tokenUsageCounter.add(tokenCount, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } export function recordCustomApiResponseMetrics( config: Config, durationMs: number, attributes: MetricDefinitions[typeof API_REQUEST_COUNT]['attributes'], ): void { if ( !apiRequestCounter || !apiRequestLatencyHistogram || !isMetricsInitialized ) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), model: attributes.model, status_code: attributes.status_code ?? 'ok', }; apiRequestCounter.add(1, metricAttributes); apiRequestLatencyHistogram.record(durationMs, { ...baseMetricDefinition.getCommonAttributes(config), model: attributes.model, }); } export function recordApiErrorMetrics( config: Config, durationMs: number, attributes: MetricDefinitions[typeof API_REQUEST_COUNT]['attributes'], ): void { if ( !apiRequestCounter || !apiRequestLatencyHistogram || !isMetricsInitialized ) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), model: attributes.model, status_code: attributes.status_code ?? 'error', error_type: attributes.error_type ?? 'unknown', }; apiRequestCounter.add(1, metricAttributes); apiRequestLatencyHistogram.record(durationMs, { ...baseMetricDefinition.getCommonAttributes(config), model: attributes.model, }); } export function recordFileOperationMetric( config: Config, attributes: MetricDefinitions[typeof FILE_OPERATION_COUNT]['attributes'], ): void { if (!fileOperationCounter || !isMetricsInitialized) return; fileOperationCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } export function recordLinesChanged( config: Config, lines: number, changeType: 'added' | 'removed', attributes?: { function_name?: string }, ): void { if (!linesChangedCounter || !isMetricsInitialized) return; if (!Number.isFinite(lines) || lines <= 0) return; linesChangedCounter.add(lines, { ...baseMetricDefinition.getCommonAttributes(config), type: changeType, ...(attributes ?? {}), }); } // --- New Metric Recording Functions --- /** * Records a metric for when the Google auth process starts. */ export function recordOnboardingStart(config: Config): void { if (!onboardingStartCounter || !isMetricsInitialized) return; onboardingStartCounter.add( 1, baseMetricDefinition.getCommonAttributes(config), ); } /** * Records a metric for when the Google auth process ends successfully. */ export function recordOnboardingSuccess( config: Config, userTier?: string, durationMs?: number, ): void { if (!isMetricsInitialized) return; const attributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...(userTier && { user_tier: userTier }), }; if (onboardingSuccessCounter) { onboardingSuccessCounter.add(1, attributes); } if (durationMs !== undefined && onboardingDurationHistogram) { onboardingDurationHistogram.record(durationMs, attributes); } } /** * Records a metric for when a UI frame flickers. */ export function recordFlickerFrame(config: Config): void { if (!flickerFrameCounter || !isMetricsInitialized) return; flickerFrameCounter.add(1, baseMetricDefinition.getCommonAttributes(config)); } /** * Records a metric for when user failed to exit */ export function recordExitFail(config: Config): void { if (!exitFailCounter || !isMetricsInitialized) return; exitFailCounter.add(1, baseMetricDefinition.getCommonAttributes(config)); } /** * Records a metric for when a plan is executed. */ export function recordPlanExecution( config: Config, attributes: MetricDefinitions[typeof PLAN_EXECUTION_COUNT]['attributes'], ): void { if (!planExecutionCounter || !isMetricsInitialized) return; planExecutionCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } /** * Records a metric for when a UI frame is slow in rendering */ export function recordSlowRender(config: Config, renderLatency: number): void { if (!slowRenderHistogram || !isMetricsInitialized) return; slowRenderHistogram.record(renderLatency, { ...baseMetricDefinition.getCommonAttributes(config), }); } /** * Records a metric for when an invalid chunk is received from a stream. */ export function recordInvalidChunk(config: Config): void { if (!invalidChunkCounter || !isMetricsInitialized) return; invalidChunkCounter.add(1, baseMetricDefinition.getCommonAttributes(config)); } export function recordRetryAttemptMetrics( config: Config, attributes: { model: string; attempt: number; }, ): void { if (!networkRetryCounter || !isMetricsInitialized) return; networkRetryCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } /** * Records a metric for when a retry is triggered due to a content error. */ export function recordContentRetry(config: Config): void { if (!contentRetryCounter || !isMetricsInitialized) return; contentRetryCounter.add(1, baseMetricDefinition.getCommonAttributes(config)); } /** * Records a metric for when all content error retries have failed for a request. */ export function recordContentRetryFailure(config: Config): void { if (!contentRetryFailureCounter || !isMetricsInitialized) return; contentRetryFailureCounter.add( 1, baseMetricDefinition.getCommonAttributes(config), ); } export function recordModelSlashCommand( config: Config, event: ModelSlashCommandEvent, ): void { if (!modelSlashCommandCallCounter || !isMetricsInitialized) return; modelSlashCommandCallCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), 'slash_command.model.model_name': event.model_name, }); } export function recordModelRoutingMetrics( config: Config, event: ModelRoutingEvent, ): void { if ( !modelRoutingLatencyHistogram || !modelRoutingFailureCounter || !isMetricsInitialized ) return; const attributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), 'routing.decision_model': event.decision_model, 'routing.decision_source': event.decision_source, 'routing.failed': event.failed, 'routing.approval_mode': event.approval_mode, }; if (event.reasoning) { attributes['routing.reasoning'] = event.reasoning; } if (event.enable_numerical_routing !== undefined) { attributes['routing.enable_numerical_routing'] = event.enable_numerical_routing; } if (event.classifier_threshold) { attributes['routing.classifier_threshold'] = event.classifier_threshold; } modelRoutingLatencyHistogram.record(event.routing_latency_ms, attributes); if (event.failed) { modelRoutingFailureCounter.add(1, { ...attributes, 'routing.error_message': event.error_message, }); } } export function recordAgentRunMetrics( config: Config, event: AgentFinishEvent, ): void { if ( !agentRunCounter || !agentDurationHistogram || !agentTurnsHistogram || !isMetricsInitialized ) return; const commonAttributes = baseMetricDefinition.getCommonAttributes(config); agentRunCounter.add(1, { ...commonAttributes, agent_name: event.agent_name, terminate_reason: event.terminate_reason, }); agentDurationHistogram.record(event.duration_ms, { ...commonAttributes, agent_name: event.agent_name, }); agentTurnsHistogram.record(event.turn_count, { ...commonAttributes, agent_name: event.agent_name, }); } export function recordRecoveryAttemptMetrics( config: Config, event: RecoveryAttemptEvent, ): void { if ( !agentRecoveryAttemptCounter || !agentRecoveryAttemptDurationHistogram || !isMetricsInitialized ) return; const commonAttributes = baseMetricDefinition.getCommonAttributes(config); agentRecoveryAttemptCounter.add(1, { ...commonAttributes, agent_name: event.agent_name, reason: event.reason, success: event.success, }); agentRecoveryAttemptDurationHistogram.record(event.duration_ms, { ...commonAttributes, agent_name: event.agent_name, }); } // OpenTelemetry GenAI Semantic Convention Recording Functions export function recordGenAiClientTokenUsage( config: Config, tokenCount: number, attributes: MetricDefinitions[typeof GEN_AI_CLIENT_TOKEN_USAGE]['attributes'], ): void { if (!genAiClientTokenUsageHistogram || !isMetricsInitialized) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; genAiClientTokenUsageHistogram.record(tokenCount, metricAttributes); } export function recordGenAiClientOperationDuration( config: Config, durationSeconds: number, attributes: MetricDefinitions[typeof GEN_AI_CLIENT_OPERATION_DURATION]['attributes'], ): void { if (!genAiClientOperationDurationHistogram || !isMetricsInitialized) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; genAiClientOperationDurationHistogram.record( durationSeconds, metricAttributes, ); } export function getConventionAttributes(event: { model: string; auth_type?: string; }): { 'gen_ai.operation.name': GenAiOperationName; 'gen_ai.provider.name': GenAiProviderName; 'gen_ai.request.model': string; 'gen_ai.response.model': string; } { const operationName = getGenAiOperationName(); const provider = getGenAiProvider(event.auth_type); return { 'gen_ai.operation.name': operationName, 'gen_ai.provider.name': provider, 'gen_ai.request.model': event.model, 'gen_ai.response.model': event.model, }; } /** * Maps authentication type to GenAI provider name following OpenTelemetry conventions */ function getGenAiProvider(authType?: string): GenAiProviderName { switch (authType) { case AuthType.USE_VERTEX_AI: case AuthType.COMPUTE_ADC: case AuthType.LOGIN_WITH_GOOGLE: return GenAiProviderName.GCP_VERTEX_AI; case AuthType.USE_GEMINI: default: return GenAiProviderName.GCP_GEN_AI; } } function getGenAiOperationName(): GenAiOperationName { return GenAiOperationName.GENERATE_CONTENT; } // Performance Monitoring Functions function initializePerformanceMonitoring(config: Config): void { const meter = getMeter(); if (!meter) return; // Check if performance monitoring is enabled in config // For now, enable performance monitoring when telemetry is enabled // TODO: Add specific performance monitoring settings to config isPerformanceMonitoringEnabled = config.getTelemetryEnabled(); if (!isPerformanceMonitoringEnabled) return; Object.entries(PERFORMANCE_COUNTER_DEFINITIONS).forEach( ([name, { description, valueType, assign }]) => { assign(meter.createCounter(name, { description, valueType })); }, ); Object.entries(PERFORMANCE_HISTOGRAM_DEFINITIONS).forEach( ([name, { description, unit, valueType, assign }]) => { assign(meter.createHistogram(name, { description, unit, valueType })); }, ); } export function recordStartupPerformance( config: Config, durationMs: number, attributes: MetricDefinitions[typeof STARTUP_TIME]['attributes'], ): void { if (!startupTimeHistogram || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), phase: attributes.phase, ...attributes.details, }; startupTimeHistogram.record(durationMs, metricAttributes); } export function recordMemoryUsage( config: Config, bytes: number, attributes: MetricDefinitions[typeof MEMORY_USAGE]['attributes'], ): void { if (!memoryUsageGauge || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; memoryUsageGauge.record(bytes, metricAttributes); } export function recordCpuUsage( config: Config, percentage: number, attributes: MetricDefinitions[typeof CPU_USAGE]['attributes'], ): void { if (!cpuUsageGauge || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; cpuUsageGauge.record(percentage, metricAttributes); } export function recordEventLoopDelay( config: Config, delayMs: number, attributes: MetricDefinitions[typeof EVENT_LOOP_DELAY]['attributes'], ): void { if (!eventLoopDelayHistogram || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; eventLoopDelayHistogram.record(delayMs, metricAttributes); } export function recordToolQueueDepth(config: Config, queueDepth: number): void { if (!toolQueueDepthGauge || !isPerformanceMonitoringEnabled) return; const attributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), }; toolQueueDepthGauge.record(queueDepth, attributes); } export function recordToolExecutionBreakdown( config: Config, durationMs: number, attributes: MetricDefinitions[typeof TOOL_EXECUTION_BREAKDOWN]['attributes'], ): void { if (!toolExecutionBreakdownHistogram || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; toolExecutionBreakdownHistogram.record(durationMs, metricAttributes); } export function recordTokenEfficiency( config: Config, value: number, attributes: MetricDefinitions[typeof TOKEN_EFFICIENCY]['attributes'], ): void { if (!tokenEfficiencyHistogram || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; tokenEfficiencyHistogram.record(value, metricAttributes); } export function recordApiRequestBreakdown( config: Config, durationMs: number, attributes: MetricDefinitions[typeof API_REQUEST_BREAKDOWN]['attributes'], ): void { if (!apiRequestBreakdownHistogram || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; apiRequestBreakdownHistogram.record(durationMs, metricAttributes); } export function recordPerformanceScore( config: Config, score: number, attributes: MetricDefinitions[typeof PERFORMANCE_SCORE]['attributes'], ): void { if (!performanceScoreGauge || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; performanceScoreGauge.record(score, metricAttributes); } export function recordPerformanceRegression( config: Config, attributes: MetricDefinitions[typeof REGRESSION_DETECTION]['attributes'], ): void { if (!regressionDetectionCounter || !isPerformanceMonitoringEnabled) return; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; regressionDetectionCounter.add(1, metricAttributes); if (attributes.baseline_value !== 0 && regressionPercentageChangeHistogram) { const percentageChange = ((attributes.current_value - attributes.baseline_value) / attributes.baseline_value) * 100; regressionPercentageChangeHistogram.record( percentageChange, metricAttributes, ); } } export function recordBaselineComparison( config: Config, attributes: MetricDefinitions[typeof BASELINE_COMPARISON]['attributes'], ): void { if (!baselineComparisonHistogram || !isPerformanceMonitoringEnabled) return; if (attributes.baseline_value === 0) { diag.warn('Baseline value is zero, skipping comparison.'); return; } const percentageChange = ((attributes.current_value - attributes.baseline_value) / attributes.baseline_value) * 100; const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }; baselineComparisonHistogram.record(percentageChange, metricAttributes); } // Utility function to check if performance monitoring is enabled export function isPerformanceMonitoringActive(): boolean { return isPerformanceMonitoringEnabled && isMetricsInitialized; } /** * Token usage recording that emits both custom and convention metrics. */ export function recordTokenUsageMetrics( config: Config, tokenCount: number, attributes: { model: string; type: 'input' | 'output' | 'thought' | 'cache' | 'tool'; genAiAttributes?: { 'gen_ai.operation.name': string; 'gen_ai.provider.name': string; 'gen_ai.request.model'?: string; 'gen_ai.response.model'?: string; 'server.address'?: string; 'server.port'?: number; }; }, ): void { recordCustomTokenUsageMetrics(config, tokenCount, { model: attributes.model, type: attributes.type, }); if ( (attributes.type === 'input' || attributes.type === 'output') && attributes.genAiAttributes ) { recordGenAiClientTokenUsage(config, tokenCount, { ...attributes.genAiAttributes, 'gen_ai.token.type': attributes.type, }); } } /** * Operation latency recording that emits both custom and convention metrics. */ export function recordApiResponseMetrics( config: Config, durationMs: number, attributes: { model: string; status_code?: number | string; genAiAttributes?: { 'gen_ai.operation.name': string; 'gen_ai.provider.name': string; 'gen_ai.request.model'?: string; 'gen_ai.response.model'?: string; 'server.address'?: string; 'server.port'?: number; 'error.type'?: string; }; }, ): void { recordCustomApiResponseMetrics(config, durationMs, { model: attributes.model, status_code: attributes.status_code, }); if (attributes.genAiAttributes) { const durationSeconds = durationMs / 1000; recordGenAiClientOperationDuration(config, durationSeconds, { ...attributes.genAiAttributes, }); } } export function recordHookCallMetrics( config: Config, hookEventName: string, hookName: string, durationMs: number, success: boolean, ): void { if (!hookCallCounter || !hookCallLatencyHistogram || !isMetricsInitialized) return; // Always sanitize hook names in metrics (metrics are aggregated and exposed) const sanitizedHookName = sanitizeHookName(hookName); const metricAttributes: Attributes = { ...baseMetricDefinition.getCommonAttributes(config), hook_event_name: hookEventName, hook_name: sanitizedHookName, success, }; hookCallCounter.add(1, metricAttributes); hookCallLatencyHistogram.record(durationMs, metricAttributes); } /** * Records a metric for keychain availability. */ export function recordKeychainAvailability( config: Config, event: KeychainAvailabilityEvent, ): void { if (!keychainAvailabilityCounter || !isMetricsInitialized) return; keychainAvailabilityCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), available: event.available, }); } /** * Records a metric for token storage type initialization. */ export function recordTokenStorageInitialization( config: Config, event: TokenStorageInitializationEvent, ): void { if (!tokenStorageTypeCounter || !isMetricsInitialized) return; tokenStorageTypeCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), type: event.type, forced: event.forced, }); } /** * Records a metric for an overage option selection. */ export function recordOverageOptionSelected( config: Config, attributes: MetricDefinitions[typeof OVERAGE_OPTION_COUNT]['attributes'], ): void { if (!overageOptionCounter || !isMetricsInitialized) return; overageOptionCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } /** * Records a metric for a credit purchase link click. */ export function recordCreditPurchaseClick( config: Config, attributes: MetricDefinitions[typeof CREDIT_PURCHASE_COUNT]['attributes'], ): void { if (!creditPurchaseCounter || !isMetricsInitialized) return; creditPurchaseCounter.add(1, { ...baseMetricDefinition.getCommonAttributes(config), ...attributes, }); } export function recordBrowserAgentConnection( config: Config, durationMs: number, attributes: { session_mode: 'persistent' | 'isolated' | 'existing'; headless: boolean; success: boolean; error_type?: | 'profile_locked' | 'timeout' | 'connection_refused' | 'unknown'; tool_count?: number; }, ): void { if (!isMetricsInitialized) return; if (!browserAgentConnectionDurationHistogram) return; const commonAttribs = baseMetricDefinition.getCommonAttributes(config); browserAgentConnectionDurationHistogram.record(durationMs, { ...commonAttribs, session_mode: attributes.session_mode, headless: attributes.headless, success: attributes.success, tool_count: attributes.tool_count, }); if (!attributes.success && browserAgentConnectionFailureCounter) { browserAgentConnectionFailureCounter.add(1, { ...commonAttribs, session_mode: attributes.session_mode, headless: attributes.headless, error_type: attributes.error_type ?? 'unknown', }); } } export function recordBrowserAgentToolDiscovery( config: Config, toolCount: number, missingSemanticTools: string[], sessionMode: 'persistent' | 'isolated' | 'existing', ): void { if (!isMetricsInitialized) return; const commonAttribs = baseMetricDefinition.getCommonAttributes(config); if (browserAgentToolsDiscoveredHistogram) { browserAgentToolsDiscoveredHistogram.record(toolCount, { ...commonAttribs, session_mode: sessionMode, }); } if (browserAgentToolsMissingSemanticCounter) { for (const tool of missingSemanticTools) { browserAgentToolsMissingSemanticCounter.add(1, { ...commonAttribs, tool_name: tool, }); } } } export function recordBrowserAgentVisionStatus( config: Config, attributes: { enabled: boolean; disabled_reason?: | 'no_visual_model' | 'missing_visual_tools' | 'blocked_auth_type'; }, ): void { if (!isMetricsInitialized || !browserAgentVisionStatusCounter) return; const metricAttributes: Record = { ...baseMetricDefinition.getCommonAttributes(config), enabled: attributes.enabled, }; if (attributes.disabled_reason) { metricAttributes['disabled_reason'] = attributes.disabled_reason; } browserAgentVisionStatusCounter.add(1, metricAttributes); } export function recordBrowserAgentTaskOutcome( config: Config, attributes: { success: boolean; session_mode: 'persistent' | 'isolated' | 'existing'; vision_enabled: boolean; headless: boolean; duration_ms: number; }, ): void { if (!isMetricsInitialized) return; const commonAttribs = baseMetricDefinition.getCommonAttributes(config); if (browserAgentTaskOutcomeCounter) { browserAgentTaskOutcomeCounter.add(1, { ...commonAttribs, success: attributes.success, session_mode: attributes.session_mode, vision_enabled: attributes.vision_enabled, headless: attributes.headless, }); } if (browserAgentTaskDurationHistogram) { browserAgentTaskDurationHistogram.record(attributes.duration_ms, { ...commonAttribs, success: attributes.success, session_mode: attributes.session_mode, }); } } export function recordBrowserAgentCleanup( config: Config, durationMs: number, attributes: { session_mode: 'persistent' | 'isolated' | 'existing'; success: boolean; }, ): void { if (!isMetricsInitialized) return; const commonAttribs = baseMetricDefinition.getCommonAttributes(config); if (browserAgentCleanupDurationHistogram) { browserAgentCleanupDurationHistogram.record(durationMs, { ...commonAttribs, session_mode: attributes.session_mode, }); } if (!attributes.success && browserAgentCleanupFailureCounter) { browserAgentCleanupFailureCounter.add(1, { ...commonAttribs, session_mode: attributes.session_mode, }); } }