From 98461ff6678d286ea25c44df0ec8fb5f97d52add Mon Sep 17 00:00:00 2001 From: shishu314 Date: Tue, 23 Sep 2025 14:37:35 -0400 Subject: [PATCH] metrics(extension) - Add logging for disable extension (#9238) Co-authored-by: Shi Shu --- packages/cli/src/config/extension.test.ts | 14 +++++++ packages/cli/src/config/extension.ts | 4 ++ packages/core/index.ts | 2 + .../clearcut-logger/clearcut-logger.ts | 21 ++++++++++ .../clearcut-logger/event-metadata-key.ts | 3 ++ packages/core/src/telemetry/constants.ts | 1 + packages/core/src/telemetry/loggers.test.ts | 40 +++++++++++++++++++ packages/core/src/telemetry/loggers.ts | 24 +++++++++++ packages/core/src/telemetry/types.ts | 14 +++++++ 9 files changed, 123 insertions(+) diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 895f4d2769..54e9c751da 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -25,6 +25,7 @@ import { GEMINI_DIR, type GeminiCLIExtension, ExtensionUninstallEvent, + ExtensionDisableEvent, ExtensionEnableEvent, } from '@google/gemini-cli-core'; import { execSync } from 'node:child_process'; @@ -71,6 +72,7 @@ vi.mock('./trustedFolders.js', async (importOriginal) => { const mockLogExtensionEnable = vi.hoisted(() => vi.fn()); const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn()); const mockLogExtensionUninstall = vi.hoisted(() => vi.fn()); +const mockLogExtensionDisable = vi.hoisted(() => vi.fn()); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = await importOriginal(); @@ -79,9 +81,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { logExtensionEnable: mockLogExtensionEnable, logExtensionInstallEvent: mockLogExtensionInstallEvent, logExtensionUninstall: mockLogExtensionUninstall, + logExtensionDisable: mockLogExtensionDisable, ExtensionEnableEvent: vi.fn(), ExtensionInstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(), + ExtensionDisableEvent: vi.fn(), }; }); @@ -1178,6 +1182,16 @@ This extension will run the following MCP servers: disableExtension('my-extension', SettingScope.System), ).toThrow('System and SystemDefaults scopes are not supported.'); }); + + it('should log a disable event', () => { + disableExtension('ext1', SettingScope.Workspace); + + expect(mockLogExtensionDisable).toHaveBeenCalled(); + expect(ExtensionDisableEvent).toHaveBeenCalledWith( + 'ext1', + SettingScope.Workspace, + ); + }); }); describe('enableExtension', () => { diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 523adf8e81..9c44ad4043 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -15,10 +15,12 @@ import { Config, ExtensionInstallEvent, ExtensionUninstallEvent, + ExtensionDisableEvent, ExtensionEnableEvent, logExtensionEnable, logExtensionInstallEvent, logExtensionUninstall, + logExtensionDisable, } from '@google/gemini-cli-core'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -648,6 +650,7 @@ export function disableExtension( scope: SettingScope, cwd: string = process.cwd(), ) { + const config = getTelemetryConfig(cwd); if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) { throw new Error('System and SystemDefaults scopes are not supported.'); } @@ -657,6 +660,7 @@ export function disableExtension( ); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); manager.disable(name, true, scopePath); + logExtensionDisable(config, new ExtensionDisableEvent(name, scope)); } export function enableExtension( diff --git a/packages/core/index.ts b/packages/core/index.ts index e451ac10ba..0c62b0e5e9 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -27,12 +27,14 @@ export { detectIdeFromEnv } from './src/ide/detect-ide.js'; export { logExtensionEnable, logIdeConnection, + logExtensionDisable, } from './src/telemetry/loggers.js'; export { IdeConnectionEvent, IdeConnectionType, ExtensionInstallEvent, + ExtensionDisableEvent, ExtensionEnableEvent, ExtensionUninstallEvent, } from './src/telemetry/types.js'; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index acd5c28a06..5dbd3dc10f 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -29,6 +29,7 @@ import type { ExtensionUninstallEvent, ModelRoutingEvent, ExtensionEnableEvent, + ExtensionDisableEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; @@ -63,6 +64,7 @@ export enum EventNames { CONTENT_RETRY = 'content_retry', CONTENT_RETRY_FAILURE = 'content_retry_failure', EXTENSION_ENABLE = 'extension_enable', + EXTENSION_DISABLE = 'extension_disable', EXTENSION_INSTALL = 'extension_install', EXTENSION_UNINSTALL = 'extension_uninstall', TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', @@ -984,6 +986,25 @@ export class ClearcutLogger { this.flushIfNeeded(); } + logExtensionDisableEvent(event: ExtensionDisableEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, + value: event.extension_name, + }, + { + gemini_cli_key: + EventMetadataKey.GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE, + value: event.setting_scope, + }, + ]; + + this.enqueueLogEvent( + this.createLogEvent(EventNames.EXTENSION_DISABLE, 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 5df2c240f4..9f46987540 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -362,6 +362,9 @@ export enum EventMetadataKey { // Logs the setting scope for an extension enablement. GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE = 102, + // Logs the setting scope for an extension disablement. + GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE = 107, + // ========================================================================== // Tool Output Truncated Event Keys // =========================================================================== diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index fcff8d0334..40a9ab5a8b 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -12,6 +12,7 @@ export const EVENT_API_REQUEST = 'gemini_cli.api_request'; export const EVENT_API_ERROR = 'gemini_cli.api_error'; export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; export const EVENT_CLI_CONFIG = 'gemini_cli.config'; +export const EVENT_EXTENSION_DISABLE = 'gemini_cli.extension_disable'; export const EVENT_EXTENSION_ENABLE = 'gemini_cli.extension_enable'; export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install'; export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall'; diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index a2066441b8..1997602480 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -34,6 +34,7 @@ import { EVENT_RIPGREP_FALLBACK, EVENT_MODEL_ROUTING, EVENT_EXTENSION_ENABLE, + EVENT_EXTENSION_DISABLE, EVENT_EXTENSION_INSTALL, EVENT_EXTENSION_UNINSTALL, } from './constants.js'; @@ -51,6 +52,7 @@ import { logToolOutputTruncated, logModelRouting, logExtensionEnable, + logExtensionDisable, logExtensionInstallEvent, logExtensionUninstall, } from './loggers.js'; @@ -69,6 +71,7 @@ import { ToolOutputTruncatedEvent, ModelRoutingEvent, ExtensionEnableEvent, + ExtensionDisableEvent, ExtensionInstallEvent, ExtensionUninstallEvent, } from './types.js'; @@ -1308,4 +1311,41 @@ describe('loggers', () => { }); }); }); + + describe('logExtensionDisable', () => { + const mockConfig = { + getSessionId: () => 'test-session-id', + getUsageStatisticsEnabled: () => true, + } as unknown as Config; + + beforeEach(() => { + vi.spyOn(ClearcutLogger.prototype, 'logExtensionDisableEvent'); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('should log extension disable event', () => { + const event = new ExtensionDisableEvent('vscode', 'user'); + + logExtensionDisable(mockConfig, event); + + expect( + ClearcutLogger.prototype.logExtensionDisableEvent, + ).toHaveBeenCalledWith(event); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Disabled extension vscode', + attributes: { + 'session.id': 'test-session-id', + 'user.email': 'test-user@example.com', + 'event.name': EVENT_EXTENSION_DISABLE, + 'event.timestamp': '2025-01-01T00:00:00.000Z', + extension_name: 'vscode', + setting_scope: 'user', + }, + }); + }); + }); }); diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 153b4477b0..e38278ce44 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -32,6 +32,7 @@ import { EVENT_RIPGREP_FALLBACK, EVENT_MODEL_ROUTING, EVENT_EXTENSION_INSTALL, + EVENT_EXTENSION_DISABLE, } from './constants.js'; import type { ApiErrorEvent, @@ -57,6 +58,7 @@ import type { RipgrepFallbackEvent, ToolOutputTruncatedEvent, ModelRoutingEvent, + ExtensionDisableEvent, ExtensionEnableEvent, ExtensionUninstallEvent, ExtensionInstallEvent, @@ -767,3 +769,25 @@ export function logExtensionEnable( }; logger.emit(logRecord); } + +export function logExtensionDisable( + config: Config, + event: ExtensionDisableEvent, +): void { + ClearcutLogger.getInstance(config)?.logExtensionDisableEvent(event); + if (!isTelemetrySdkInitialized()) return; + + const attributes: LogAttributes = { + ...getCommonAttributes(config), + ...event, + 'event.name': EVENT_EXTENSION_DISABLE, + 'event.timestamp': new Date().toISOString(), + }; + + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: `Disabled extension ${event.extension_name}`, + attributes, + }; + logger.emit(logRecord); +} diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 6f7283dc1f..272df3d334 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -669,3 +669,17 @@ export class ExtensionEnableEvent implements BaseTelemetryEvent { this.setting_scope = settingScope; } } + +export class ExtensionDisableEvent implements BaseTelemetryEvent { + 'event.name': 'extension_disable'; + 'event.timestamp': string; + extension_name: string; + setting_scope: string; + + constructor(extension_name: string, settingScope: string) { + this['event.name'] = 'extension_disable'; + this['event.timestamp'] = new Date().toISOString(); + this.extension_name = extension_name; + this.setting_scope = settingScope; + } +}