diff --git a/docs/telemetry.md b/docs/telemetry.md index 6baa4a8894..d6e2434a27 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -307,6 +307,15 @@ for Gemini CLI: - `command` (string) - `subcommand` (string, if applicable) +- `gemini_cli.extension_enable`: This event occurs when an extension is enabled +- `gemini_cli.extension_install`: This event occurs when an extension is installed + - **Attributes**: + - `extension_name` (string) + - `extension_version` (string) + - `extension_source` (string) + - `status` (string) +- `gemini_cli.extension_uninstall`: This event occurs when an extension is uninstalled + ### Metrics Metrics are numerical measurements of behavior over time. The following metrics are collected for Gemini CLI: diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 915fd55f4a..f2b7aa7f93 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -24,9 +24,8 @@ import { import { GEMINI_DIR, type GeminiCLIExtension, - ClearcutLogger, - type Config, ExtensionUninstallEvent, + ExtensionEnableEvent, } from '@google/gemini-cli-core'; import { execSync } from 'node:child_process'; import { SettingScope } from './settings.js'; @@ -69,20 +68,18 @@ vi.mock('./trustedFolders.js', async (importOriginal) => { }; }); +const mockLogExtensionEnable = vi.hoisted(() => vi.fn()); +const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn()); +const mockLogExtensionUninstall = vi.hoisted(() => vi.fn()); 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(), + logExtensionEnable: mockLogExtensionEnable, + logExtensionInstallEvent: mockLogExtensionInstallEvent, + logExtensionUninstall: mockLogExtensionUninstall, + ExtensionEnableEvent: vi.fn(), ExtensionInstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(), }; @@ -763,8 +760,7 @@ describe('extension tests', () => { await installExtension({ source: sourceExtDir, type: 'local' }); - const logger = ClearcutLogger.getInstance({} as Config); - expect(logger?.logExtensionInstallEvent).toHaveBeenCalled(); + expect(mockLogExtensionInstallEvent).toHaveBeenCalled(); }); it('should show users information on their mcp server when installing', async () => { @@ -948,9 +944,10 @@ describe('extension tests', () => { await uninstallExtension('my-local-extension'); - const logger = ClearcutLogger.getInstance({} as Config); - expect(logger?.logExtensionUninstallEvent).toHaveBeenCalledWith( - new ExtensionUninstallEvent('my-local-extension', 'success'), + expect(mockLogExtensionUninstall).toHaveBeenCalled(); + expect(ExtensionUninstallEvent).toHaveBeenCalledWith( + 'my-local-extension', + 'success', ); }); @@ -969,9 +966,10 @@ describe('extension tests', () => { await uninstallExtension(gitUrl); expect(fs.existsSync(sourceExtDir)).toBe(false); - const logger = ClearcutLogger.getInstance({} as Config); - expect(logger?.logExtensionUninstallEvent).toHaveBeenCalledWith( - new ExtensionUninstallEvent('gemini-sql-extension', 'success'), + expect(mockLogExtensionUninstall).toHaveBeenCalled(); + expect(ExtensionUninstallEvent).toHaveBeenCalledWith( + 'gemini-sql-extension', + 'success', ); }); @@ -1233,6 +1231,22 @@ describe('extension tests', () => { expect(activeExtensions).toHaveLength(1); expect(activeExtensions[0].name).toBe('ext1'); }); + + it('should log an enable event', () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'ext1', + version: '1.0.0', + }); + disableExtension('ext1', SettingScope.Workspace); + enableExtension('ext1', SettingScope.Workspace); + + expect(mockLogExtensionEnable).toHaveBeenCalled(); + expect(ExtensionEnableEvent).toHaveBeenCalledWith( + 'ext1', + SettingScope.Workspace, + ); + }); }); }); diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 6475325ccf..346d9343a9 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -12,10 +12,13 @@ import type { import { GEMINI_DIR, Storage, - ClearcutLogger, Config, ExtensionInstallEvent, ExtensionUninstallEvent, + ExtensionEnableEvent, + logExtensionEnable, + logExtensionInstallEvent, + logExtensionUninstall, } from '@google/gemini-cli-core'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -123,16 +126,18 @@ export async function performWorkspaceExtensionMigration( return failedInstallNames; } -function getClearcutLogger(cwd: string) { +function getTelemetryConfig(cwd: string) { + const settings = loadSettings(cwd); const config = new Config({ + telemetry: settings.merged.telemetry, + interactive: false, sessionId: randomUUID(), targetDir: cwd, cwd, model: '', debugMode: false, }); - const logger = ClearcutLogger.getInstance(config); - return logger; + return config; } export function loadExtensions( @@ -372,7 +377,7 @@ export async function installExtension( askConsent: boolean = false, cwd: string = process.cwd(), ): Promise { - const logger = getClearcutLogger(cwd); + const telemetryConfig = getTelemetryConfig(cwd); let newExtensionConfig: ExtensionConfig | null = null; let localSourcePath: string | undefined; @@ -467,7 +472,8 @@ export async function installExtension( } } - logger?.logExtensionInstallEvent( + logExtensionInstallEvent( + telemetryConfig, new ExtensionInstallEvent( newExtensionConfig!.name, newExtensionConfig!.version, @@ -491,7 +497,8 @@ export async function installExtension( // Ignore error, this is just for logging. } } - logger?.logExtensionInstallEvent( + logExtensionInstallEvent( + telemetryConfig, new ExtensionInstallEvent( newExtensionConfig?.name ?? '', newExtensionConfig?.version ?? '', @@ -559,7 +566,7 @@ export async function uninstallExtension( extensionIdentifier: string, cwd: string = process.cwd(), ): Promise { - const logger = getClearcutLogger(cwd); + const telemetryConfig = getTelemetryConfig(cwd); const installedExtensions = loadUserExtensions(); const extensionName = installedExtensions.find( (installed) => @@ -581,7 +588,8 @@ export async function uninstallExtension( recursive: true, force: true, }); - logger?.logExtensionUninstallEvent( + logExtensionUninstall( + telemetryConfig, new ExtensionUninstallEvent(extensionName, 'success'), ); } @@ -648,4 +656,6 @@ export function enableExtension( ); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); manager.enable(name, true, scopePath); + const config = getTelemetryConfig(cwd); + logExtensionEnable(config, new ExtensionEnableEvent(name, scope)); } diff --git a/packages/cli/src/config/extensions/update.test.ts b/packages/cli/src/config/extensions/update.test.ts index 63af39603c..ac69b56500 100644 --- a/packages/cli/src/config/extensions/update.test.ts +++ b/packages/cli/src/config/extensions/update.test.ts @@ -55,20 +55,16 @@ vi.mock('../trustedFolders.js', async (importOriginal) => { }; }); +const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn()); +const mockLogExtensionUninstall = vi.hoisted(() => vi.fn()); + 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(), + logExtensionInstallEvent: mockLogExtensionInstallEvent, + logExtensionUninstall: mockLogExtensionUninstall, ExtensionInstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(), }; diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts index d06f9038d0..c9e04cab86 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts @@ -56,20 +56,16 @@ vi.mock('../../config/trustedFolders.js', async (importOriginal) => { }; }); +const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn()); +const mockLogExtensionUninstall = vi.hoisted(() => vi.fn()); + 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(), + logExtensionInstallEvent: mockLogExtensionInstallEvent, + logExtensionUninstall: mockLogExtensionUninstall, ExtensionInstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(), }; diff --git a/packages/core/index.ts b/packages/core/index.ts index 83334558e7..e451ac10ba 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -24,12 +24,16 @@ export { DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, } from './src/config/config.js'; export { detectIdeFromEnv } from './src/ide/detect-ide.js'; -export { logIdeConnection } from './src/telemetry/loggers.js'; +export { + logExtensionEnable, + logIdeConnection, +} from './src/telemetry/loggers.js'; export { IdeConnectionEvent, IdeConnectionType, ExtensionInstallEvent, + ExtensionEnableEvent, ExtensionUninstallEvent, } from './src/telemetry/types.js'; export { makeFakeConfig } from './src/test-utils/config.js'; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index dd91da0dd6..acd5c28a06 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -24,11 +24,11 @@ import type { InvalidChunkEvent, ContentRetryEvent, ContentRetryFailureEvent, - ExtensionEnableEvent, ExtensionInstallEvent, ToolOutputTruncatedEvent, ExtensionUninstallEvent, ModelRoutingEvent, + ExtensionEnableEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts index 9560269b7a..8b79af4255 100644 --- a/packages/core/src/telemetry/index.ts +++ b/packages/core/src/telemetry/index.ts @@ -36,6 +36,9 @@ export { logKittySequenceOverflow, logChatCompression, logToolOutputTruncated, + logExtensionEnable, + logExtensionInstallEvent, + logExtensionUninstall, } from './loggers.js'; export type { SlashCommandEvent, ChatCompressionEvent } from './types.js'; export {