From e5745f16cb43b53bcd1346b6befb0c5c08584933 Mon Sep 17 00:00:00 2001 From: Adib234 <30782825+Adib234@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:57:34 -0500 Subject: [PATCH] feat(plan): telemetry to track adoption and usage of plan mode (#16863) --- docs/cli/telemetry.md | 15 ++++++ packages/core/src/config/config.ts | 31 +++++++++++ .../clearcut-logger/clearcut-logger.ts | 40 ++++++++++++++ .../clearcut-logger/event-metadata-key.ts | 15 +++++- packages/core/src/telemetry/loggers.ts | 30 ++++++++++- packages/core/src/telemetry/types.ts | 52 +++++++++++++++++++ 6 files changed, 181 insertions(+), 2 deletions(-) diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md index 40f234335d..9bf662b2a1 100644 --- a/docs/cli/telemetry.md +++ b/docs/cli/telemetry.md @@ -18,6 +18,7 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI. - [Logs and metrics](#logs-and-metrics) - [Logs](#logs) - [Sessions](#sessions) + - [Approval Mode](#approval-mode) - [Tools](#tools) - [Files](#files) - [API](#api) @@ -315,6 +316,20 @@ Captures startup configuration and user prompt submissions. - `prompt` (string; excluded if `telemetry.logPrompts` is `false`) - `auth_type` (string) +#### Approval Mode + +Tracks changes and duration of approval modes. + +- `approval_mode_switch`: Approval mode was changed. + - **Attributes**: + - `from_mode` (string) + - `to_mode` (string) + +- `approval_mode_duration`: Duration spent in an approval mode. + - **Attributes**: + - `mode` (string) + - `duration_ms` (int) + #### Tools Captures tool executions, output truncation, and Edit behavior. diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 097146276e..3183077556 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -64,6 +64,8 @@ import { logRipgrepFallback, logFlashFallback } from '../telemetry/loggers.js'; import { RipgrepFallbackEvent, FlashFallbackEvent, + ApprovalModeSwitchEvent, + ApprovalModeDurationEvent, } from '../telemetry/types.js'; import type { FallbackModelHandler } from '../fallback/types.js'; import { ModelAvailabilityService } from '../availability/modelAvailabilityService.js'; @@ -105,6 +107,10 @@ import { debugLogger } from '../utils/debugLogger.js'; import { SkillManager, type SkillDefinition } from '../skills/skillManager.js'; import { startupProfiler } from '../telemetry/startupProfiler.js'; import type { AgentDefinition } from '../agents/types.js'; +import { + logApprovalModeSwitch, + logApprovalModeDuration, +} from '../telemetry/loggers.js'; import { fetchAdminControls } from '../code_assist/admin/admin_controls.js'; export interface AccessibilitySettings { @@ -544,6 +550,7 @@ export class Config { private terminalBackground: string | undefined = undefined; private remoteAdminSettings: FetchAdminControlsResponse | undefined; private latestApiRequest: GenerateContentParameters | undefined; + private lastModeSwitchTime: number = Date.now(); constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -1348,9 +1355,32 @@ export class Config { 'Cannot enable privileged approval modes in an untrusted folder.', ); } + + const currentMode = this.getApprovalMode(); + if (currentMode !== mode) { + this.logCurrentModeDuration(this.getApprovalMode()); + logApprovalModeSwitch( + this, + new ApprovalModeSwitchEvent(currentMode, mode), + ); + this.lastModeSwitchTime = Date.now(); + } + this.policyEngine.setApprovalMode(mode); } + /** + * Logs the duration of the current approval mode. + */ + logCurrentModeDuration(mode: ApprovalMode): void { + const now = Date.now(); + const duration = now - this.lastModeSwitchTime; + logApprovalModeDuration( + this, + new ApprovalModeDurationEvent(mode, duration), + ); + } + isYoloModeDisabled(): boolean { return this.disableYoloMode || !this.isTrustedFolder(); } @@ -2054,6 +2084,7 @@ export class Config { * Disposes of resources and removes event listeners. */ async dispose(): Promise { + this.logCurrentModeDuration(this.getApprovalMode()); coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); this.agentRegistry?.dispose(); this.geminiClient?.dispose(); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 46e5828f70..9417bbe983 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -42,6 +42,8 @@ import type { ExtensionUpdateEvent, LlmLoopCheckEvent, HookCallEvent, + ApprovalModeSwitchEvent, + ApprovalModeDurationEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; @@ -100,6 +102,8 @@ export enum EventNames { WEB_FETCH_FALLBACK_ATTEMPT = 'web_fetch_fallback_attempt', LLM_LOOP_CHECK = 'llm_loop_check', HOOK_CALL = 'hook_call', + APPROVAL_MODE_SWITCH = 'approval_mode_switch', + APPROVAL_MODE_DURATION = 'approval_mode_duration', } export interface LogResponse { @@ -1467,6 +1471,42 @@ export class ClearcutLogger { this.flushIfNeeded(); } + logApprovalModeSwitchEvent(event: ApprovalModeSwitchEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE, + value: event.from_mode, + }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_TO, + value: event.to_mode, + }, + ]; + + this.enqueueLogEvent( + this.createLogEvent(EventNames.APPROVAL_MODE_SWITCH, data), + ); + this.flushIfNeeded(); + } + + logApprovalModeDurationEvent(event: ApprovalModeDurationEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE, + value: event.mode, + }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_DURATION_MS, + value: event.duration_ms.toString(), + }, + ]; + + this.enqueueLogEvent( + this.createLogEvent(EventNames.APPROVAL_MODE_DURATION, data), + ); + this.flushIfNeeded(); + } + /** * Adds default fields to data, and returns a new data array. This fields * should exist on all log events. diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index 5f12b8442e..a3b22ce58e 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -7,7 +7,7 @@ // Defines valid event metadata keys for Clearcut logging. export enum EventMetadataKey { // Deleted enums: 24 - // Next ID: 137 + // Next ID: 144 GEMINI_CLI_KEY_UNKNOWN = 0, @@ -529,4 +529,17 @@ export enum EventMetadataKey { // Logs total RAM in GB of user machine. GEMINI_CLI_RAM_TOTAL_GB = 140, + + // ========================================================================== + // Approval Mode Event Keys + // ========================================================================== + + // Logs the active approval mode in the session. + GEMINI_CLI_ACTIVE_APPROVAL_MODE = 141, + + // Logs the new approval mode. + GEMINI_CLI_APPROVAL_MODE_TO = 142, + + // Logs the duration spent in an approval mode in milliseconds. + GEMINI_CLI_APPROVAL_MODE_DURATION_MS = 143, } diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index eef2fe6db7..ae25424464 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -48,9 +48,11 @@ import type { RecoveryAttemptEvent, WebFetchFallbackAttemptEvent, ExtensionUpdateEvent, - LlmLoopCheckEvent, + ApprovalModeSwitchEvent, + ApprovalModeDurationEvent, HookCallEvent, StartupStatsEvent, + LlmLoopCheckEvent, } from './types.js'; import { recordApiErrorMetrics, @@ -671,6 +673,32 @@ export function logLlmLoopCheck( }); } +export function logApprovalModeSwitch( + config: Config, + event: ApprovalModeSwitchEvent, +) { + ClearcutLogger.getInstance(config)?.logApprovalModeSwitchEvent(event); + bufferTelemetryEvent(() => { + logs.getLogger(SERVICE_NAME).emit({ + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), + }); + }); +} + +export function logApprovalModeDuration( + config: Config, + event: ApprovalModeDurationEvent, +) { + ClearcutLogger.getInstance(config)?.logApprovalModeDurationEvent(event); + bufferTelemetryEvent(() => { + logs.getLogger(SERVICE_NAME).emit({ + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), + }); + }); +} + export function logHookCall(config: Config, event: HookCallEvent): void { ClearcutLogger.getInstance(config)?.logHookCallEvent(event); bufferTelemetryEvent(() => { diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 0549ae3aa2..eb7fc0096e 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -1845,6 +1845,58 @@ export class WebFetchFallbackAttemptEvent implements BaseTelemetryEvent { } export const EVENT_HOOK_CALL = 'gemini_cli.hook_call'; +export class ApprovalModeSwitchEvent implements BaseTelemetryEvent { + eventName = 'approval_mode_switch'; + from_mode: ApprovalMode; + to_mode: ApprovalMode; + + constructor(fromMode: ApprovalMode, toMode: ApprovalMode) { + this.from_mode = fromMode; + this.to_mode = toMode; + } + 'event.name': string; + 'event.timestamp': string; + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + event_name: this.eventName, + from_mode: this.from_mode, + to_mode: this.to_mode, + }; + } + + toLogBody(): string { + return `Approval mode switched from ${this.from_mode} to ${this.to_mode}.`; + } +} + +export class ApprovalModeDurationEvent implements BaseTelemetryEvent { + eventName = 'approval_mode_duration'; + mode: ApprovalMode; + duration_ms: number; + + constructor(mode: ApprovalMode, durationMs: number) { + this.mode = mode; + this.duration_ms = durationMs; + } + 'event.name': string; + 'event.timestamp': string; + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + event_name: this.eventName, + mode: this.mode, + duration_ms: this.duration_ms, + }; + } + + toLogBody(): string { + return `Approval mode ${this.mode} was active for ${this.duration_ms}ms.`; + } +} + export class HookCallEvent implements BaseTelemetryEvent { 'event.name': string; 'event.timestamp': string;