Metric(extension) - Add logging for uninstalling extension (#8293)

Co-authored-by: Shi Shu <shii@google.com>
This commit is contained in:
shishu314
2025-09-12 13:38:54 -04:00
committed by GitHub
parent c99539b991
commit 8a5e692373
6 changed files with 78 additions and 9 deletions
+19
View File
@@ -31,6 +31,7 @@ import {
type MCPServerConfig, type MCPServerConfig,
ClearcutLogger, ClearcutLogger,
type Config, type Config,
ExtensionUninstallEvent,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { SettingScope, loadSettings } from './settings.js'; import { SettingScope, loadSettings } from './settings.js';
@@ -76,15 +77,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>(); await importOriginal<typeof import('@google/gemini-cli-core')>();
const mockLogExtensionInstallEvent = vi.fn(); const mockLogExtensionInstallEvent = vi.fn();
const mockLogExtensionUninstallEvent = vi.fn();
return { return {
...actual, ...actual,
ClearcutLogger: { ClearcutLogger: {
getInstance: vi.fn(() => ({ getInstance: vi.fn(() => ({
logExtensionInstallEvent: mockLogExtensionInstallEvent, logExtensionInstallEvent: mockLogExtensionInstallEvent,
logExtensionUninstallEvent: mockLogExtensionUninstallEvent,
})), })),
}, },
Config: vi.fn(), Config: vi.fn(),
ExtensionInstallEvent: vi.fn(), ExtensionInstallEvent: vi.fn(),
ExtensionUninstallEvent: vi.fn(),
}; };
}); });
@@ -689,6 +693,21 @@ describe('uninstallExtension', () => {
'Extension "nonexistent-extension" not found.', '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', () => { describe('performWorkspaceExtensionMigration', () => {
+20 -9
View File
@@ -14,6 +14,7 @@ import {
ClearcutLogger, ClearcutLogger,
Config, Config,
ExtensionInstallEvent, ExtensionInstallEvent,
ExtensionUninstallEvent,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
@@ -123,6 +124,18 @@ export async function performWorkspaceExtensionMigration(
return failedInstallNames; 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( export function loadExtensions(
workspaceDir: string = process.cwd(), workspaceDir: string = process.cwd(),
): Extension[] { ): Extension[] {
@@ -403,14 +416,7 @@ export async function installExtension(
installMetadata: ExtensionInstallMetadata, installMetadata: ExtensionInstallMetadata,
cwd: string = process.cwd(), cwd: string = process.cwd(),
): Promise<string> { ): Promise<string> {
const config = new Config({ const logger = getClearcutLogger(cwd);
sessionId: randomUUID(),
targetDir: process.cwd(),
cwd: process.cwd(),
model: '',
debugMode: false,
});
const logger = ClearcutLogger.getInstance(config);
let newExtensionConfig: ExtensionConfig | null = null; let newExtensionConfig: ExtensionConfig | null = null;
let localSourcePath: string | undefined; let localSourcePath: string | undefined;
@@ -563,6 +569,7 @@ export async function uninstallExtension(
extensionName: string, extensionName: string,
cwd: string = process.cwd(), cwd: string = process.cwd(),
): Promise<void> { ): Promise<void> {
const logger = getClearcutLogger(cwd);
const installedExtensions = loadUserExtensions(); const installedExtensions = loadUserExtensions();
if ( if (
!installedExtensions.some( !installedExtensions.some(
@@ -577,10 +584,14 @@ export async function uninstallExtension(
cwd, cwd,
); );
const storage = new ExtensionStorage(extensionName); const storage = new ExtensionStorage(extensionName);
return await fs.promises.rm(storage.getExtensionDir(), {
await fs.promises.rm(storage.getExtensionDir(), {
recursive: true, recursive: true,
force: true, force: true,
}); });
logger?.logExtensionUninstallEvent(
new ExtensionUninstallEvent(extensionName, 'success'),
);
} }
export function toOutputString(extension: Extension): string { export function toOutputString(extension: Extension): string {
+1
View File
@@ -29,6 +29,7 @@ export {
IdeConnectionEvent, IdeConnectionEvent,
IdeConnectionType, IdeConnectionType,
ExtensionInstallEvent, ExtensionInstallEvent,
ExtensionUninstallEvent,
} from './src/telemetry/types.js'; } from './src/telemetry/types.js';
export { makeFakeConfig } from './src/test-utils/config.js'; export { makeFakeConfig } from './src/test-utils/config.js';
export * from './src/utils/pathReader.js'; export * from './src/utils/pathReader.js';
@@ -26,6 +26,7 @@ import type {
ContentRetryFailureEvent, ContentRetryFailureEvent,
ExtensionInstallEvent, ExtensionInstallEvent,
ToolOutputTruncatedEvent, ToolOutputTruncatedEvent,
ExtensionUninstallEvent,
} from '../types.js'; } from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js'; import { EventMetadataKey } from './event-metadata-key.js';
import type { Config } from '../../config/config.js'; import type { Config } from '../../config/config.js';
@@ -59,6 +60,7 @@ export enum EventNames {
CONTENT_RETRY = 'content_retry', CONTENT_RETRY = 'content_retry',
CONTENT_RETRY_FAILURE = 'content_retry_failure', CONTENT_RETRY_FAILURE = 'content_retry_failure',
EXTENSION_INSTALL = 'extension_install', EXTENSION_INSTALL = 'extension_install',
EXTENSION_UNINSTALL = 'extension_uninstall',
TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated',
} }
@@ -863,6 +865,24 @@ export class ClearcutLogger {
this.flushIfNeeded(); 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 { logToolOutputTruncatedEvent(event: ToolOutputTruncatedEvent): void {
const data: EventValue[] = [ const data: EventValue[] = [
{ {
@@ -350,6 +350,9 @@ export enum EventMetadataKey {
// Logs the status of the extension install. // Logs the status of the extension install.
GEMINI_CLI_EXTENSION_INSTALL_STATUS = 88, GEMINI_CLI_EXTENSION_INSTALL_STATUS = 88,
// Logs the status of the extension uninstall
GEMINI_CLI_EXTENSION_UNINSTALL_STATUS = 96,
// ========================================================================== // ==========================================================================
// Tool Output Truncated Event Keys // Tool Output Truncated Event Keys
// =========================================================================== // ===========================================================================
+15
View File
@@ -535,6 +535,7 @@ export type TelemetryEvent =
| ContentRetryEvent | ContentRetryEvent
| ContentRetryFailureEvent | ContentRetryFailureEvent
| ExtensionInstallEvent | ExtensionInstallEvent
| ExtensionUninstallEvent
| ToolOutputTruncatedEvent; | ToolOutputTruncatedEvent;
export class ExtensionInstallEvent implements BaseTelemetryEvent { export class ExtensionInstallEvent implements BaseTelemetryEvent {
@@ -590,3 +591,17 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
this.lines = details.lines; 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;
}
}