feat(plan): telemetry to track adoption and usage of plan mode (#16863)

This commit is contained in:
Adib234
2026-01-20 13:57:34 -05:00
committed by GitHub
parent 12b0fe1cc2
commit e5745f16cb
6 changed files with 181 additions and 2 deletions

View File

@@ -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.

View File

@@ -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<void> {
this.logCurrentModeDuration(this.getApprovalMode());
coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed);
this.agentRegistry?.dispose();
this.geminiClient?.dispose();

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -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(() => {

View File

@@ -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;