From 8a5e692373af4c81c7e68315639580e46b15eb63 Mon Sep 17 00:00:00 2001 From: shishu314 Date: Fri, 12 Sep 2025 13:38:54 -0400 Subject: [PATCH] Metric(extension) - Add logging for uninstalling extension (#8293) Co-authored-by: Shi Shu --- packages/cli/src/config/extension.test.ts | 19 ++++++++++++ packages/cli/src/config/extension.ts | 29 +++++++++++++------ packages/core/index.ts | 1 + .../clearcut-logger/clearcut-logger.ts | 20 +++++++++++++ .../clearcut-logger/event-metadata-key.ts | 3 ++ packages/core/src/telemetry/types.ts | 15 ++++++++++ 6 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 2835c9fff8..c070979b35 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -31,6 +31,7 @@ import { type MCPServerConfig, ClearcutLogger, type Config, + ExtensionUninstallEvent, } from '@google/gemini-cli-core'; import { execSync } from 'node:child_process'; import { SettingScope, loadSettings } from './settings.js'; @@ -76,15 +77,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = await importOriginal(); const mockLogExtensionInstallEvent = vi.fn(); + const mockLogExtensionUninstallEvent = vi.fn(); return { ...actual, ClearcutLogger: { getInstance: vi.fn(() => ({ logExtensionInstallEvent: mockLogExtensionInstallEvent, + logExtensionUninstallEvent: mockLogExtensionUninstallEvent, })), }, Config: vi.fn(), ExtensionInstallEvent: vi.fn(), + ExtensionUninstallEvent: vi.fn(), }; }); @@ -689,6 +693,21 @@ describe('uninstallExtension', () => { 'Extension "nonexistent-extension" not found.', ); }); + + it('should log uninstall event', async () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'my-local-extension', + version: '1.0.0', + }); + + await uninstallExtension('my-local-extension'); + + const logger = ClearcutLogger.getInstance({} as Config); + expect(logger?.logExtensionUninstallEvent).toHaveBeenCalledWith( + new ExtensionUninstallEvent('my-local-extension', 'success'), + ); + }); }); describe('performWorkspaceExtensionMigration', () => { diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 459c5f24b3..c262c9a12e 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -14,6 +14,7 @@ import { ClearcutLogger, Config, ExtensionInstallEvent, + ExtensionUninstallEvent, } from '@google/gemini-cli-core'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -123,6 +124,18 @@ export async function performWorkspaceExtensionMigration( return failedInstallNames; } +function getClearcutLogger(cwd: string) { + const config = new Config({ + sessionId: randomUUID(), + targetDir: cwd, + cwd, + model: '', + debugMode: false, + }); + const logger = ClearcutLogger.getInstance(config); + return logger; +} + export function loadExtensions( workspaceDir: string = process.cwd(), ): Extension[] { @@ -403,14 +416,7 @@ export async function installExtension( installMetadata: ExtensionInstallMetadata, cwd: string = process.cwd(), ): Promise { - const config = new Config({ - sessionId: randomUUID(), - targetDir: process.cwd(), - cwd: process.cwd(), - model: '', - debugMode: false, - }); - const logger = ClearcutLogger.getInstance(config); + const logger = getClearcutLogger(cwd); let newExtensionConfig: ExtensionConfig | null = null; let localSourcePath: string | undefined; @@ -563,6 +569,7 @@ export async function uninstallExtension( extensionName: string, cwd: string = process.cwd(), ): Promise { + const logger = getClearcutLogger(cwd); const installedExtensions = loadUserExtensions(); if ( !installedExtensions.some( @@ -577,10 +584,14 @@ export async function uninstallExtension( cwd, ); const storage = new ExtensionStorage(extensionName); - return await fs.promises.rm(storage.getExtensionDir(), { + + await fs.promises.rm(storage.getExtensionDir(), { recursive: true, force: true, }); + logger?.logExtensionUninstallEvent( + new ExtensionUninstallEvent(extensionName, 'success'), + ); } export function toOutputString(extension: Extension): string { diff --git a/packages/core/index.ts b/packages/core/index.ts index 81f09969f4..d746c9a082 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -29,6 +29,7 @@ export { IdeConnectionEvent, IdeConnectionType, ExtensionInstallEvent, + ExtensionUninstallEvent, } from './src/telemetry/types.js'; export { makeFakeConfig } from './src/test-utils/config.js'; export * from './src/utils/pathReader.js'; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 4cc19e4f7b..7a9273853f 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -26,6 +26,7 @@ import type { ContentRetryFailureEvent, ExtensionInstallEvent, ToolOutputTruncatedEvent, + ExtensionUninstallEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; @@ -59,6 +60,7 @@ export enum EventNames { CONTENT_RETRY = 'content_retry', CONTENT_RETRY_FAILURE = 'content_retry_failure', EXTENSION_INSTALL = 'extension_install', + EXTENSION_UNINSTALL = 'extension_uninstall', TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', } @@ -863,6 +865,24 @@ export class ClearcutLogger { this.flushIfNeeded(); } + logExtensionUninstallEvent(event: ExtensionUninstallEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, + value: event.extension_name, + }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UNINSTALL_STATUS, + value: event.status, + }, + ]; + + this.enqueueLogEvent( + this.createLogEvent(EventNames.EXTENSION_UNINSTALL, data), + ); + this.flushIfNeeded(); + } + logToolOutputTruncatedEvent(event: ToolOutputTruncatedEvent): void { const data: EventValue[] = [ { 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 25ea046283..cea62dc3b9 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -350,6 +350,9 @@ export enum EventMetadataKey { // Logs the status of the extension install. GEMINI_CLI_EXTENSION_INSTALL_STATUS = 88, + // Logs the status of the extension uninstall + GEMINI_CLI_EXTENSION_UNINSTALL_STATUS = 96, + // ========================================================================== // Tool Output Truncated Event Keys // =========================================================================== diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index e22cc37288..ee87558e1c 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -535,6 +535,7 @@ export type TelemetryEvent = | ContentRetryEvent | ContentRetryFailureEvent | ExtensionInstallEvent + | ExtensionUninstallEvent | ToolOutputTruncatedEvent; export class ExtensionInstallEvent implements BaseTelemetryEvent { @@ -590,3 +591,17 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { this.lines = details.lines; } } + +export class ExtensionUninstallEvent implements BaseTelemetryEvent { + 'event.name': 'extension_uninstall'; + 'event.timestamp': string; + extension_name: string; + status: 'success' | 'error'; + + constructor(extension_name: string, status: 'success' | 'error') { + this['event.name'] = 'extension_uninstall'; + this['event.timestamp'] = new Date().toISOString(); + this.extension_name = extension_name; + this.status = status; + } +}