metrics(extension) - Add logging for disable extension (#9238)

Co-authored-by: Shi Shu <shii@google.com>
This commit is contained in:
shishu314
2025-09-23 14:37:35 -04:00
committed by GitHub
parent 31c609daec
commit 98461ff667
9 changed files with 123 additions and 0 deletions
+14
View File
@@ -25,6 +25,7 @@ import {
GEMINI_DIR, GEMINI_DIR,
type GeminiCLIExtension, type GeminiCLIExtension,
ExtensionUninstallEvent, ExtensionUninstallEvent,
ExtensionDisableEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
@@ -71,6 +72,7 @@ vi.mock('./trustedFolders.js', async (importOriginal) => {
const mockLogExtensionEnable = vi.hoisted(() => vi.fn()); const mockLogExtensionEnable = vi.hoisted(() => vi.fn());
const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn()); const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn());
const mockLogExtensionUninstall = vi.hoisted(() => vi.fn()); const mockLogExtensionUninstall = vi.hoisted(() => vi.fn());
const mockLogExtensionDisable = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => { 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')>();
@@ -79,9 +81,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
logExtensionEnable: mockLogExtensionEnable, logExtensionEnable: mockLogExtensionEnable,
logExtensionInstallEvent: mockLogExtensionInstallEvent, logExtensionInstallEvent: mockLogExtensionInstallEvent,
logExtensionUninstall: mockLogExtensionUninstall, logExtensionUninstall: mockLogExtensionUninstall,
logExtensionDisable: mockLogExtensionDisable,
ExtensionEnableEvent: vi.fn(), ExtensionEnableEvent: vi.fn(),
ExtensionInstallEvent: vi.fn(), ExtensionInstallEvent: vi.fn(),
ExtensionUninstallEvent: 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), disableExtension('my-extension', SettingScope.System),
).toThrow('System and SystemDefaults scopes are not supported.'); ).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', () => { describe('enableExtension', () => {
+4
View File
@@ -15,10 +15,12 @@ import {
Config, Config,
ExtensionInstallEvent, ExtensionInstallEvent,
ExtensionUninstallEvent, ExtensionUninstallEvent,
ExtensionDisableEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
logExtensionEnable, logExtensionEnable,
logExtensionInstallEvent, logExtensionInstallEvent,
logExtensionUninstall, logExtensionUninstall,
logExtensionDisable,
} 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';
@@ -648,6 +650,7 @@ export function disableExtension(
scope: SettingScope, scope: SettingScope,
cwd: string = process.cwd(), cwd: string = process.cwd(),
) { ) {
const config = getTelemetryConfig(cwd);
if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) { if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) {
throw new Error('System and SystemDefaults scopes are not supported.'); 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(); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
manager.disable(name, true, scopePath); manager.disable(name, true, scopePath);
logExtensionDisable(config, new ExtensionDisableEvent(name, scope));
} }
export function enableExtension( export function enableExtension(
+2
View File
@@ -27,12 +27,14 @@ export { detectIdeFromEnv } from './src/ide/detect-ide.js';
export { export {
logExtensionEnable, logExtensionEnable,
logIdeConnection, logIdeConnection,
logExtensionDisable,
} from './src/telemetry/loggers.js'; } from './src/telemetry/loggers.js';
export { export {
IdeConnectionEvent, IdeConnectionEvent,
IdeConnectionType, IdeConnectionType,
ExtensionInstallEvent, ExtensionInstallEvent,
ExtensionDisableEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
ExtensionUninstallEvent, ExtensionUninstallEvent,
} from './src/telemetry/types.js'; } from './src/telemetry/types.js';
@@ -29,6 +29,7 @@ import type {
ExtensionUninstallEvent, ExtensionUninstallEvent,
ModelRoutingEvent, ModelRoutingEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
ExtensionDisableEvent,
} 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';
@@ -63,6 +64,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_ENABLE = 'extension_enable', EXTENSION_ENABLE = 'extension_enable',
EXTENSION_DISABLE = 'extension_disable',
EXTENSION_INSTALL = 'extension_install', EXTENSION_INSTALL = 'extension_install',
EXTENSION_UNINSTALL = 'extension_uninstall', EXTENSION_UNINSTALL = 'extension_uninstall',
TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated',
@@ -984,6 +986,25 @@ export class ClearcutLogger {
this.flushIfNeeded(); 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 * Adds default fields to data, and returns a new data array. This fields
* should exist on all log events. * should exist on all log events.
@@ -362,6 +362,9 @@ export enum EventMetadataKey {
// Logs the setting scope for an extension enablement. // Logs the setting scope for an extension enablement.
GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE = 102, 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 // Tool Output Truncated Event Keys
// =========================================================================== // ===========================================================================
+1
View File
@@ -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_ERROR = 'gemini_cli.api_error';
export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; export const EVENT_API_RESPONSE = 'gemini_cli.api_response';
export const EVENT_CLI_CONFIG = 'gemini_cli.config'; 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_ENABLE = 'gemini_cli.extension_enable';
export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install'; export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install';
export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall'; export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall';
@@ -34,6 +34,7 @@ import {
EVENT_RIPGREP_FALLBACK, EVENT_RIPGREP_FALLBACK,
EVENT_MODEL_ROUTING, EVENT_MODEL_ROUTING,
EVENT_EXTENSION_ENABLE, EVENT_EXTENSION_ENABLE,
EVENT_EXTENSION_DISABLE,
EVENT_EXTENSION_INSTALL, EVENT_EXTENSION_INSTALL,
EVENT_EXTENSION_UNINSTALL, EVENT_EXTENSION_UNINSTALL,
} from './constants.js'; } from './constants.js';
@@ -51,6 +52,7 @@ import {
logToolOutputTruncated, logToolOutputTruncated,
logModelRouting, logModelRouting,
logExtensionEnable, logExtensionEnable,
logExtensionDisable,
logExtensionInstallEvent, logExtensionInstallEvent,
logExtensionUninstall, logExtensionUninstall,
} from './loggers.js'; } from './loggers.js';
@@ -69,6 +71,7 @@ import {
ToolOutputTruncatedEvent, ToolOutputTruncatedEvent,
ModelRoutingEvent, ModelRoutingEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
ExtensionDisableEvent,
ExtensionInstallEvent, ExtensionInstallEvent,
ExtensionUninstallEvent, ExtensionUninstallEvent,
} from './types.js'; } 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',
},
});
});
});
}); });
+24
View File
@@ -32,6 +32,7 @@ import {
EVENT_RIPGREP_FALLBACK, EVENT_RIPGREP_FALLBACK,
EVENT_MODEL_ROUTING, EVENT_MODEL_ROUTING,
EVENT_EXTENSION_INSTALL, EVENT_EXTENSION_INSTALL,
EVENT_EXTENSION_DISABLE,
} from './constants.js'; } from './constants.js';
import type { import type {
ApiErrorEvent, ApiErrorEvent,
@@ -57,6 +58,7 @@ import type {
RipgrepFallbackEvent, RipgrepFallbackEvent,
ToolOutputTruncatedEvent, ToolOutputTruncatedEvent,
ModelRoutingEvent, ModelRoutingEvent,
ExtensionDisableEvent,
ExtensionEnableEvent, ExtensionEnableEvent,
ExtensionUninstallEvent, ExtensionUninstallEvent,
ExtensionInstallEvent, ExtensionInstallEvent,
@@ -767,3 +769,25 @@ export function logExtensionEnable(
}; };
logger.emit(logRecord); 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);
}
+14
View File
@@ -669,3 +669,17 @@ export class ExtensionEnableEvent implements BaseTelemetryEvent {
this.setting_scope = settingScope; 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;
}
}