Add extensions logging (#11261)

This commit is contained in:
christine betts
2025-10-21 16:55:16 -04:00
committed by GitHub
parent e9e80b054d
commit c6a59896f3
16 changed files with 230 additions and 65 deletions
+12
View File
@@ -603,6 +603,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
{ {
path: '/path/to/ext1', path: '/path/to/ext1',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
contextFiles: ['/path/to/ext1/GEMINI.md'], contextFiles: ['/path/to/ext1/GEMINI.md'],
isActive: true, isActive: true,
@@ -610,6 +611,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
{ {
path: '/path/to/ext2', path: '/path/to/ext2',
name: 'ext2', name: 'ext2',
id: 'ext2-id',
version: '1.0.0', version: '1.0.0',
contextFiles: [], contextFiles: [],
isActive: true, isActive: true,
@@ -617,6 +619,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
{ {
path: '/path/to/ext3', path: '/path/to/ext3',
name: 'ext3', name: 'ext3',
id: 'ext3-id',
version: '1.0.0', version: '1.0.0',
contextFiles: [ contextFiles: [
'/path/to/ext3/context1.md', '/path/to/ext3/context1.md',
@@ -690,6 +693,8 @@ describe('mergeMcpServers', () => {
{ {
path: '/path/to/ext1', path: '/path/to/ext1',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
mcpServers: { mcpServers: {
'ext1-server': { 'ext1-server': {
@@ -730,6 +735,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext1', path: '/path/to/ext1',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool3', 'tool4'], excludeTools: ['tool3', 'tool4'],
contextFiles: [], contextFiles: [],
@@ -738,6 +744,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext2', path: '/path/to/ext2',
name: 'ext2', name: 'ext2',
id: 'ext2-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool5'], excludeTools: ['tool5'],
contextFiles: [], contextFiles: [],
@@ -764,6 +771,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext1', path: '/path/to/ext1',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool2', 'tool3'], excludeTools: ['tool2', 'tool3'],
contextFiles: [], contextFiles: [],
@@ -790,6 +798,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext1', path: '/path/to/ext1',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool2', 'tool3'], excludeTools: ['tool2', 'tool3'],
contextFiles: [], contextFiles: [],
@@ -798,6 +807,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext2', path: '/path/to/ext2',
name: 'ext2', name: 'ext2',
id: 'ext2-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool3', 'tool4'], excludeTools: ['tool3', 'tool4'],
contextFiles: [], contextFiles: [],
@@ -871,6 +881,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext', path: '/path/to/ext',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool1', 'tool2'], excludeTools: ['tool1', 'tool2'],
contextFiles: [], contextFiles: [],
@@ -897,6 +908,7 @@ describe('mergeExcludeTools', () => {
{ {
path: '/path/to/ext', path: '/path/to/ext',
name: 'ext1', name: 'ext1',
id: 'ext1-id',
version: '1.0.0', version: '1.0.0',
excludeTools: ['tool2'], excludeTools: ['tool2'],
contextFiles: [], contextFiles: [],
+21 -4
View File
@@ -21,6 +21,7 @@ import {
loadExtensionConfig, loadExtensionConfig,
loadExtensions, loadExtensions,
uninstallExtension, uninstallExtension,
hashValue,
} from './extension.js'; } from './extension.js';
import { import {
GEMINI_DIR, GEMINI_DIR,
@@ -1259,6 +1260,10 @@ This extension will run the following MCP servers:
extensionsDir: userExtensionsDir, extensionsDir: userExtensionsDir,
name: 'my-local-extension', name: 'my-local-extension',
version: '1.0.0', version: '1.0.0',
installMetadata: {
source: userExtensionsDir,
type: 'local',
},
}); });
await uninstallExtension('my-local-extension', isUpdate); await uninstallExtension('my-local-extension', isUpdate);
@@ -1269,7 +1274,8 @@ This extension will run the following MCP servers:
} else { } else {
expect(mockLogExtensionUninstall).toHaveBeenCalled(); expect(mockLogExtensionUninstall).toHaveBeenCalled();
expect(ExtensionUninstallEvent).toHaveBeenCalledWith( expect(ExtensionUninstallEvent).toHaveBeenCalledWith(
'my-local-extension', hashValue('my-local-extension'),
hashValue(userExtensionsDir),
'success', 'success',
); );
} }
@@ -1313,7 +1319,8 @@ This extension will run the following MCP servers:
expect(fs.existsSync(sourceExtDir)).toBe(false); expect(fs.existsSync(sourceExtDir)).toBe(false);
expect(mockLogExtensionUninstall).toHaveBeenCalled(); expect(mockLogExtensionUninstall).toHaveBeenCalled();
expect(ExtensionUninstallEvent).toHaveBeenCalledWith( expect(ExtensionUninstallEvent).toHaveBeenCalledWith(
'gemini-sql-extension', hashValue('gemini-sql-extension'),
hashValue('https://github.com/google/gemini-sql-extension'),
'success', 'success',
); );
}); });
@@ -1423,6 +1430,10 @@ This extension will run the following MCP servers:
extensionsDir: userExtensionsDir, extensionsDir: userExtensionsDir,
name: 'ext1', name: 'ext1',
version: '1.0.0', version: '1.0.0',
installMetadata: {
source: userExtensionsDir,
type: 'local',
},
}); });
disableExtension( disableExtension(
@@ -1433,7 +1444,8 @@ This extension will run the following MCP servers:
expect(mockLogExtensionDisable).toHaveBeenCalled(); expect(mockLogExtensionDisable).toHaveBeenCalled();
expect(ExtensionDisableEvent).toHaveBeenCalledWith( expect(ExtensionDisableEvent).toHaveBeenCalledWith(
'ext1', hashValue('ext1'),
hashValue(userExtensionsDir),
SettingScope.Workspace, SettingScope.Workspace,
); );
}); });
@@ -1497,6 +1509,10 @@ This extension will run the following MCP servers:
extensionsDir: userExtensionsDir, extensionsDir: userExtensionsDir,
name: 'ext1', name: 'ext1',
version: '1.0.0', version: '1.0.0',
installMetadata: {
source: userExtensionsDir,
type: 'local',
},
}); });
const extensionEnablementManager = new ExtensionEnablementManager(); const extensionEnablementManager = new ExtensionEnablementManager();
disableExtension( disableExtension(
@@ -1512,7 +1528,8 @@ This extension will run the following MCP servers:
expect(mockLogExtensionEnable).toHaveBeenCalled(); expect(mockLogExtensionEnable).toHaveBeenCalled();
expect(ExtensionEnableEvent).toHaveBeenCalledWith( expect(ExtensionEnableEvent).toHaveBeenCalledWith(
'ext1', hashValue('ext1'),
hashValue(userExtensionsDir),
SettingScope.Workspace, SettingScope.Workspace,
); );
}); });
+65 -41
View File
@@ -199,28 +199,6 @@ export function loadExtension(
) )
.filter((contextFilePath) => fs.existsSync(contextFilePath)); .filter((contextFilePath) => fs.existsSync(contextFilePath));
// IDs are created by hashing details of the installation source in order to
// deduplicate extensions with conflicting names and also obfuscate any
// potentially sensitive information such as private git urls, system paths,
// or project names.
const hash = createHash('sha256');
const githubUrlParts =
installMetadata &&
(installMetadata.type === 'git' ||
installMetadata.type === 'github-release')
? tryParseGithubUrl(installMetadata.source)
: null;
if (githubUrlParts) {
// For github repos, we use the https URI to the repo as the ID.
hash.update(
`https://github.com/${githubUrlParts.owner}/${githubUrlParts.repo}`,
);
} else {
hash.update(installMetadata?.source ?? config.name);
}
const id = hash.digest('hex');
return { return {
name: config.name, name: config.name,
version: config.version, version: config.version,
@@ -230,7 +208,7 @@ export function loadExtension(
mcpServers: config.mcpServers, mcpServers: config.mcpServers,
excludeTools: config.excludeTools, excludeTools: config.excludeTools,
isActive: extensionEnablementManager.isEnabled(config.name, workspaceDir), isActive: extensionEnablementManager.isEnabled(config.name, workspaceDir),
id, id: getExtensionId(config, installMetadata),
}; };
} catch (e) { } catch (e) {
debugLogger.error( debugLogger.error(
@@ -384,6 +362,10 @@ async function promptForConsentInteractive(
}); });
} }
export function hashValue(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
export async function installOrUpdateExtension( export async function installOrUpdateExtension(
installMetadata: ExtensionInstallMetadata, installMetadata: ExtensionInstallMetadata,
requestConsent: (consent: string) => Promise<boolean>, requestConsent: (consent: string) => Promise<boolean>,
@@ -520,15 +502,15 @@ export async function installOrUpdateExtension(
await fs.promises.rm(tempDir, { recursive: true, force: true }); await fs.promises.rm(tempDir, { recursive: true, force: true });
} }
} }
if (isUpdate) { if (isUpdate) {
logExtensionUpdateEvent( logExtensionUpdateEvent(
telemetryConfig, telemetryConfig,
new ExtensionUpdateEvent( new ExtensionUpdateEvent(
newExtensionConfig.name, hashValue(newExtensionConfig.name),
getExtensionId(newExtensionConfig, installMetadata),
newExtensionConfig.version, newExtensionConfig.version,
previousExtensionConfig.version, previousExtensionConfig.version,
installMetadata.source, installMetadata.type,
'success', 'success',
), ),
); );
@@ -536,9 +518,10 @@ export async function installOrUpdateExtension(
logExtensionInstallEvent( logExtensionInstallEvent(
telemetryConfig, telemetryConfig,
new ExtensionInstallEvent( new ExtensionInstallEvent(
newExtensionConfig.name, hashValue(newExtensionConfig.name),
getExtensionId(newExtensionConfig, installMetadata),
newExtensionConfig.version, newExtensionConfig.version,
installMetadata.source, installMetadata.type,
'success', 'success',
), ),
); );
@@ -564,14 +547,19 @@ export async function installOrUpdateExtension(
// Ignore error, this is just for logging. // Ignore error, this is just for logging.
} }
} }
const config = newExtensionConfig ?? previousExtensionConfig;
const extensionId = config
? getExtensionId(config, installMetadata)
: undefined;
if (isUpdate) { if (isUpdate) {
logExtensionUpdateEvent( logExtensionUpdateEvent(
telemetryConfig, telemetryConfig,
new ExtensionUpdateEvent( new ExtensionUpdateEvent(
newExtensionConfig?.name ?? previousExtensionConfig.name, hashValue(config?.name ?? ''),
extensionId ?? '',
newExtensionConfig?.version ?? '', newExtensionConfig?.version ?? '',
previousExtensionConfig.version, previousExtensionConfig.version,
installMetadata.source, installMetadata.type,
'error', 'error',
), ),
); );
@@ -579,9 +567,10 @@ export async function installOrUpdateExtension(
logExtensionInstallEvent( logExtensionInstallEvent(
telemetryConfig, telemetryConfig,
new ExtensionInstallEvent( new ExtensionInstallEvent(
newExtensionConfig?.name ?? '', hashValue(newExtensionConfig?.name ?? ''),
extensionId ?? '',
newExtensionConfig?.version ?? '', newExtensionConfig?.version ?? '',
installMetadata.source, installMetadata.type,
'error', 'error',
), ),
); );
@@ -707,16 +696,16 @@ export async function uninstallExtension(
new ExtensionEnablementManager(), new ExtensionEnablementManager(),
cwd, cwd,
); );
const extensionName = installedExtensions.find( const extension = installedExtensions.find(
(installed) => (installed) =>
installed.name.toLowerCase() === extensionIdentifier.toLowerCase() || installed.name.toLowerCase() === extensionIdentifier.toLowerCase() ||
installed.installMetadata?.source.toLowerCase() === installed.installMetadata?.source.toLowerCase() ===
extensionIdentifier.toLowerCase(), extensionIdentifier.toLowerCase(),
)?.name; );
if (!extensionName) { if (!extension) {
throw new Error(`Extension not found.`); throw new Error(`Extension not found.`);
} }
const storage = new ExtensionStorage(extensionName); const storage = new ExtensionStorage(extension.name);
await fs.promises.rm(storage.getExtensionDir(), { await fs.promises.rm(storage.getExtensionDir(), {
recursive: true, recursive: true,
@@ -727,13 +716,17 @@ export async function uninstallExtension(
// uninstalls related to updates. // uninstalls related to updates.
if (isUpdate) return; if (isUpdate) return;
const manager = new ExtensionEnablementManager([extensionName]); const manager = new ExtensionEnablementManager([extension.name]);
manager.remove(extensionName); manager.remove(extension.name);
const telemetryConfig = getTelemetryConfig(cwd); const telemetryConfig = getTelemetryConfig(cwd);
logExtensionUninstall( logExtensionUninstall(
telemetryConfig, telemetryConfig,
new ExtensionUninstallEvent(extensionName, 'success'), new ExtensionUninstallEvent(
hashValue(extension.name),
extension.id,
'success',
),
); );
} }
@@ -747,6 +740,7 @@ export function toOutputString(
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗'); const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
let output = `${status} ${extension.name} (${extension.version})`; let output = `${status} ${extension.name} (${extension.version})`;
output += `\n ID: ${extension.id}`;
output += `\n Path: ${extension.path}`; output += `\n Path: ${extension.path}`;
if (extension.installMetadata) { if (extension.installMetadata) {
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`; output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;
@@ -797,7 +791,10 @@ export function disableExtension(
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
extensionEnablementManager.disable(name, true, scopePath); extensionEnablementManager.disable(name, true, scopePath);
logExtensionDisable(config, new ExtensionDisableEvent(name, scope)); logExtensionDisable(
config,
new ExtensionDisableEvent(hashValue(name), extension.id, scope),
);
} }
export function enableExtension( export function enableExtension(
@@ -816,5 +813,32 @@ export function enableExtension(
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
extensionEnablementManager.enable(name, true, scopePath); extensionEnablementManager.enable(name, true, scopePath);
const config = getTelemetryConfig(cwd); const config = getTelemetryConfig(cwd);
logExtensionEnable(config, new ExtensionEnableEvent(name, scope)); logExtensionEnable(
config,
new ExtensionEnableEvent(hashValue(name), extension.id, scope),
);
}
function getExtensionId(
config: ExtensionConfig,
installMetadata?: ExtensionInstallMetadata,
): string {
// IDs are created by hashing details of the installation source in order to
// deduplicate extensions with conflicting names and also obfuscate any
// potentially sensitive information such as private git urls, system paths,
// or project names.
let idValue = config.name;
const githubUrlParts =
installMetadata &&
(installMetadata.type === 'git' ||
installMetadata.type === 'github-release')
? tryParseGithubUrl(installMetadata.source)
: null;
if (githubUrlParts) {
// For github repos, we use the https URI to the repo as the ID.
idValue = `https://github.com/${githubUrlParts.owner}/${githubUrlParts.repo}`;
} else {
idValue = installMetadata?.source ?? config.name;
}
return hashValue(idValue);
} }
@@ -141,6 +141,7 @@ describe('git extension helpers', () => {
it('should return NOT_UPDATABLE for non-git extensions', async () => { it('should return NOT_UPDATABLE for non-git extensions', async () => {
const extension: GeminiCLIExtension = { const extension: GeminiCLIExtension = {
name: 'test', name: 'test',
id: 'test-id',
path: '/ext', path: '/ext',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
@@ -160,6 +161,7 @@ describe('git extension helpers', () => {
it('should return ERROR if no remotes found', async () => { it('should return ERROR if no remotes found', async () => {
const extension: GeminiCLIExtension = { const extension: GeminiCLIExtension = {
name: 'test', name: 'test',
id: 'test-id',
path: '/ext', path: '/ext',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
@@ -180,6 +182,7 @@ describe('git extension helpers', () => {
it('should return UPDATE_AVAILABLE when remote hash is different', async () => { it('should return UPDATE_AVAILABLE when remote hash is different', async () => {
const extension: GeminiCLIExtension = { const extension: GeminiCLIExtension = {
name: 'test', name: 'test',
id: 'test-id',
path: '/ext', path: '/ext',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
@@ -205,6 +208,7 @@ describe('git extension helpers', () => {
it('should return UP_TO_DATE when remote and local hashes are the same', async () => { it('should return UP_TO_DATE when remote and local hashes are the same', async () => {
const extension: GeminiCLIExtension = { const extension: GeminiCLIExtension = {
name: 'test', name: 'test',
id: 'test-id',
path: '/ext', path: '/ext',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
@@ -230,6 +234,7 @@ describe('git extension helpers', () => {
it('should return ERROR on git error', async () => { it('should return ERROR on git error', async () => {
const extension: GeminiCLIExtension = { const extension: GeminiCLIExtension = {
name: 'test', name: 'test',
id: 'test-id',
path: '/ext', path: '/ext',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
@@ -223,6 +223,7 @@ describe('extensionsCommand', () => {
const extensionOne: GeminiCLIExtension = { const extensionOne: GeminiCLIExtension = {
name: 'ext-one', name: 'ext-one',
id: 'ext-one-id',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
path: '/test/dir/ext-one', path: '/test/dir/ext-one',
@@ -235,6 +236,7 @@ describe('extensionsCommand', () => {
}; };
const extensionTwo: GeminiCLIExtension = { const extensionTwo: GeminiCLIExtension = {
name: 'another-ext', name: 'another-ext',
id: 'another-ext-id',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
path: '/test/dir/another-ext', path: '/test/dir/another-ext',
@@ -247,6 +249,7 @@ describe('extensionsCommand', () => {
}; };
const allExt: GeminiCLIExtension = { const allExt: GeminiCLIExtension = {
name: 'all-ext', name: 'all-ext',
id: 'all-ext-id',
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
path: '/test/dir/all-ext', path: '/test/dir/all-ext',
+1
View File
@@ -196,6 +196,7 @@ export interface SlashCommand {
// Optional metadata for extension commands // Optional metadata for extension commands
extensionName?: string; extensionName?: string;
extensionId?: string;
// The action to run. Optional for parent commands that only group sub-commands. // The action to run. Optional for parent commands that only group sub-commands.
action?: ( action?: (
@@ -518,6 +518,7 @@ export const useSlashCommandProcessor = (
command: resolvedCommandPath[0], command: resolvedCommandPath[0],
subcommand, subcommand,
status: SlashCommandStatus.ERROR, status: SlashCommandStatus.ERROR,
extension_id: commandToExecute?.extensionId,
}); });
logSlashCommand(config, event); logSlashCommand(config, event);
} }
@@ -535,6 +536,7 @@ export const useSlashCommandProcessor = (
command: resolvedCommandPath[0], command: resolvedCommandPath[0],
subcommand, subcommand,
status: SlashCommandStatus.SUCCESS, status: SlashCommandStatus.SUCCESS,
extension_id: commandToExecute?.extensionId,
}); });
logSlashCommand(config, event); logSlashCommand(config, event);
} }
@@ -57,6 +57,7 @@ describe('useExtensionUpdates', () => {
const extensions = [ const extensions = [
{ {
name: 'test-extension', name: 'test-extension',
id: 'test-extension-id',
type: 'git', type: 'git',
version: '1.0.0', version: '1.0.0',
path: '/some/path', path: '/some/path',
@@ -269,6 +270,7 @@ describe('useExtensionUpdates', () => {
const extensions = [ const extensions = [
{ {
name: 'test-extension-1', name: 'test-extension-1',
id: 'test-extension-1-id',
type: 'git', type: 'git',
version: '1.0.0', version: '1.0.0',
path: '/some/path1', path: '/some/path1',
@@ -282,6 +284,8 @@ describe('useExtensionUpdates', () => {
}, },
{ {
name: 'test-extension-2', name: 'test-extension-2',
id: 'test-extension-2-id',
type: 'git', type: 'git',
version: '2.0.0', version: '2.0.0',
path: '/some/path2', path: '/some/path2',
+1 -1
View File
@@ -140,7 +140,7 @@ export interface GeminiCLIExtension {
mcpServers?: Record<string, MCPServerConfig>; mcpServers?: Record<string, MCPServerConfig>;
contextFiles: string[]; contextFiles: string[];
excludeTools?: string[]; excludeTools?: string[];
id?: string; id: string;
} }
export interface ExtensionInstallMetadata { export interface ExtensionInstallMetadata {
@@ -446,6 +446,15 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS, gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS,
value: event.mcp_tools ? event.mcp_tools : '', value: event.mcp_tools ? event.mcp_tools : '',
}, },
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSIONS_COUNT,
value: event.extensions_count.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSION_IDS,
value: event.extension_ids.toString(),
},
]; ];
this.sessionData = data; this.sessionData = data;
@@ -893,6 +902,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name, value: event.extension_name,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
value: event.extension_id,
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION,
value: event.extension_version, value: event.extension_version,
@@ -921,6 +934,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name, value: event.extension_name,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
value: event.extension_id,
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UNINSTALL_STATUS, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UNINSTALL_STATUS,
value: event.status, value: event.status,
@@ -941,6 +958,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name, value: event.extension_name,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
value: event.extension_id,
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION,
value: event.extension_version, value: event.extension_version,
@@ -1037,6 +1058,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name, value: event.extension_name,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
value: event.extension_id,
},
{ {
gemini_cli_key: gemini_cli_key:
EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE, EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE,
@@ -1072,6 +1097,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name, value: event.extension_name,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
value: event.extension_id,
},
{ {
gemini_cli_key: gemini_cli_key:
EventMetadataKey.GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE, EventMetadataKey.GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE,
@@ -7,7 +7,7 @@
// Defines valid event metadata keys for Clearcut logging. // Defines valid event metadata keys for Clearcut logging.
export enum EventMetadataKey { export enum EventMetadataKey {
// Deleted enums: 24 // Deleted enums: 24
// Next ID: 117 // Next ID: 122
GEMINI_CLI_KEY_UNKNOWN = 0, GEMINI_CLI_KEY_UNKNOWN = 0,
@@ -373,6 +373,9 @@ export enum EventMetadataKey {
// Logs the name of the extension. // Logs the name of the extension.
GEMINI_CLI_EXTENSION_NAME = 85, GEMINI_CLI_EXTENSION_NAME = 85,
// Logs the name of the extension.
GEMINI_CLI_EXTENSION_ID = 121,
// Logs the version of the extension. // Logs the version of the extension.
GEMINI_CLI_EXTENSION_VERSION = 86, GEMINI_CLI_EXTENSION_VERSION = 86,
@@ -391,6 +394,12 @@ export enum EventMetadataKey {
// Logs the status of the extension uninstall // Logs the status of the extension uninstall
GEMINI_CLI_EXTENSION_UPDATE_STATUS = 118, GEMINI_CLI_EXTENSION_UPDATE_STATUS = 118,
// Logs the count of extensions in Start Session Event
GEMINI_CLI_START_SESSION_EXTENSIONS_COUNT = 119,
// Logs the name of extensions as a comma-separated string
GEMINI_CLI_START_SESSION_EXTENSION_IDS = 120,
// 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,
+30 -15
View File
@@ -201,6 +201,7 @@ describe('loggers', () => {
getTargetDir: () => 'target-dir', getTargetDir: () => 'target-dir',
getProxy: () => 'http://test.proxy.com:8080', getProxy: () => 'http://test.proxy.com:8080',
getOutputFormat: () => OutputFormat.JSON, getOutputFormat: () => OutputFormat.JSON,
getExtensions: () => [],
} as unknown as Config; } as unknown as Config;
const startSessionEvent = new StartSessionEvent(mockConfig); const startSessionEvent = new StartSessionEvent(mockConfig);
@@ -229,6 +230,8 @@ describe('loggers', () => {
mcp_tools: undefined, mcp_tools: undefined,
mcp_tools_count: undefined, mcp_tools_count: undefined,
output_format: 'json', output_format: 'json',
extension_ids: '',
extensions_count: 0,
}, },
}); });
}); });
@@ -1042,6 +1045,10 @@ describe('loggers', () => {
}, },
required: ['arg1', 'arg2'], required: ['arg1', 'arg2'],
}, },
false,
undefined,
undefined,
'test-extension',
); );
const call: CompletedToolCall = { const call: CompletedToolCall = {
@@ -1076,6 +1083,7 @@ describe('loggers', () => {
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_TOOL_CALL, 'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_id: 'test-extension',
function_name: 'mock_mcp_tool', function_name: 'mock_mcp_tool',
function_args: JSON.stringify( function_args: JSON.stringify(
{ {
@@ -1094,6 +1102,7 @@ describe('loggers', () => {
error: undefined, error: undefined,
error_type: undefined, error_type: undefined,
metadata: undefined, metadata: undefined,
content_length: undefined,
}, },
}); });
}); });
@@ -1310,7 +1319,8 @@ describe('loggers', () => {
it('should log extension install event', () => { it('should log extension install event', () => {
const event = new ExtensionInstallEvent( const event = new ExtensionInstallEvent(
'vscode', 'testing',
'testing-id',
'0.1.0', '0.1.0',
'git', 'git',
'success', 'success',
@@ -1323,14 +1333,14 @@ describe('loggers', () => {
).toHaveBeenCalledWith(event); ).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({ expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Installed extension vscode', body: 'Installed extension testing',
attributes: { attributes: {
'session.id': 'test-session-id', 'session.id': 'test-session-id',
'user.email': 'test-user@example.com', 'user.email': 'test-user@example.com',
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_EXTENSION_INSTALL, 'event.name': EVENT_EXTENSION_INSTALL,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode', extension_name: 'testing',
extension_version: '0.1.0', extension_version: '0.1.0',
extension_source: 'git', extension_source: 'git',
status: 'success', status: 'success',
@@ -1358,7 +1368,8 @@ describe('loggers', () => {
it('should log extension update event', () => { it('should log extension update event', () => {
const event = new ExtensionUpdateEvent( const event = new ExtensionUpdateEvent(
'vscode', 'testing',
'testing-id',
'0.1.0', '0.1.0',
'0.1.1', '0.1.1',
'git', 'git',
@@ -1372,14 +1383,14 @@ describe('loggers', () => {
).toHaveBeenCalledWith(event); ).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({ expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Updated extension vscode', body: 'Updated extension testing',
attributes: { attributes: {
'session.id': 'test-session-id', 'session.id': 'test-session-id',
'user.email': 'test-user@example.com', 'user.email': 'test-user@example.com',
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_EXTENSION_UPDATE, 'event.name': EVENT_EXTENSION_UPDATE,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode', extension_name: 'testing',
extension_version: '0.1.0', extension_version: '0.1.0',
extension_previous_version: '0.1.1', extension_previous_version: '0.1.1',
extension_source: 'git', extension_source: 'git',
@@ -1407,7 +1418,11 @@ describe('loggers', () => {
}); });
it('should log extension uninstall event', () => { it('should log extension uninstall event', () => {
const event = new ExtensionUninstallEvent('vscode', 'success'); const event = new ExtensionUninstallEvent(
'testing',
'testing-id',
'success',
);
logExtensionUninstall(mockConfig, event); logExtensionUninstall(mockConfig, event);
@@ -1416,14 +1431,14 @@ describe('loggers', () => {
).toHaveBeenCalledWith(event); ).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({ expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Uninstalled extension vscode', body: 'Uninstalled extension testing',
attributes: { attributes: {
'session.id': 'test-session-id', 'session.id': 'test-session-id',
'user.email': 'test-user@example.com', 'user.email': 'test-user@example.com',
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_EXTENSION_UNINSTALL, 'event.name': EVENT_EXTENSION_UNINSTALL,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode', extension_name: 'testing',
status: 'success', status: 'success',
}, },
}); });
@@ -1445,7 +1460,7 @@ describe('loggers', () => {
}); });
it('should log extension enable event', () => { it('should log extension enable event', () => {
const event = new ExtensionEnableEvent('vscode', 'user'); const event = new ExtensionEnableEvent('testing', 'testing-id', 'user');
logExtensionEnable(mockConfig, event); logExtensionEnable(mockConfig, event);
@@ -1454,14 +1469,14 @@ describe('loggers', () => {
).toHaveBeenCalledWith(event); ).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({ expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Enabled extension vscode', body: 'Enabled extension testing',
attributes: { attributes: {
'session.id': 'test-session-id', 'session.id': 'test-session-id',
'user.email': 'test-user@example.com', 'user.email': 'test-user@example.com',
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_EXTENSION_ENABLE, 'event.name': EVENT_EXTENSION_ENABLE,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode', extension_name: 'testing',
setting_scope: 'user', setting_scope: 'user',
}, },
}); });
@@ -1483,7 +1498,7 @@ describe('loggers', () => {
}); });
it('should log extension disable event', () => { it('should log extension disable event', () => {
const event = new ExtensionDisableEvent('vscode', 'user'); const event = new ExtensionDisableEvent('testing', 'testing-id', 'user');
logExtensionDisable(mockConfig, event); logExtensionDisable(mockConfig, event);
@@ -1492,14 +1507,14 @@ describe('loggers', () => {
).toHaveBeenCalledWith(event); ).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({ expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Disabled extension vscode', body: 'Disabled extension testing',
attributes: { attributes: {
'session.id': 'test-session-id', 'session.id': 'test-session-id',
'user.email': 'test-user@example.com', 'user.email': 'test-user@example.com',
'installation.id': 'test-installation-id', 'installation.id': 'test-installation-id',
'event.name': EVENT_EXTENSION_DISABLE, 'event.name': EVENT_EXTENSION_DISABLE,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode', extension_name: 'testing',
setting_scope: 'user', setting_scope: 'user',
}, },
}); });
+41 -3
View File
@@ -54,6 +54,8 @@ export class StartSessionEvent implements BaseTelemetryEvent {
mcp_tools_count?: number; mcp_tools_count?: number;
mcp_tools?: string; mcp_tools?: string;
output_format: OutputFormat; output_format: OutputFormat;
extensions_count: number;
extension_ids: string;
constructor(config: Config, toolRegistry?: ToolRegistry) { constructor(config: Config, toolRegistry?: ToolRegistry) {
const generatorConfig = config.getContentGeneratorConfig(); const generatorConfig = config.getContentGeneratorConfig();
@@ -85,6 +87,9 @@ export class StartSessionEvent implements BaseTelemetryEvent {
config.getFileFilteringRespectGitIgnore(); config.getFileFilteringRespectGitIgnore();
this.mcp_servers_count = mcpServers ? Object.keys(mcpServers).length : 0; this.mcp_servers_count = mcpServers ? Object.keys(mcpServers).length : 0;
this.output_format = config.getOutputFormat(); this.output_format = config.getOutputFormat();
const extensions = config.getExtensions();
this.extensions_count = extensions.length;
this.extension_ids = extensions.map((e) => e.id).join(',');
if (toolRegistry) { if (toolRegistry) {
const mcpTools = toolRegistry const mcpTools = toolRegistry
.getAllTools() .getAllTools()
@@ -116,6 +121,8 @@ export class StartSessionEvent implements BaseTelemetryEvent {
mcp_tools: this.mcp_tools, mcp_tools: this.mcp_tools,
mcp_tools_count: this.mcp_tools_count, mcp_tools_count: this.mcp_tools_count,
output_format: this.output_format, output_format: this.output_format,
extensions_count: this.extensions_count,
extension_ids: this.extension_ids,
}; };
} }
@@ -198,6 +205,7 @@ export class ToolCallEvent implements BaseTelemetryEvent {
tool_type: 'native' | 'mcp'; tool_type: 'native' | 'mcp';
content_length?: number; content_length?: number;
mcp_server_name?: string; mcp_server_name?: string;
extension_id?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: { [key: string]: any }; metadata?: { [key: string]: any };
@@ -243,6 +251,7 @@ export class ToolCallEvent implements BaseTelemetryEvent {
) { ) {
this.tool_type = 'mcp'; this.tool_type = 'mcp';
this.mcp_server_name = call.tool.serverName; this.mcp_server_name = call.tool.serverName;
this.extension_id = call.tool.extensionId;
} else { } else {
this.tool_type = 'native'; this.tool_type = 'native';
} }
@@ -292,6 +301,7 @@ export class ToolCallEvent implements BaseTelemetryEvent {
tool_type: this.tool_type, tool_type: this.tool_type,
content_length: this.content_length, content_length: this.content_length,
mcp_server_name: this.mcp_server_name, mcp_server_name: this.mcp_server_name,
extension_id: this.extension_id,
metadata: this.metadata, metadata: this.metadata,
}; };
@@ -627,6 +637,7 @@ export interface SlashCommandEvent extends BaseTelemetryEvent {
command: string; command: string;
subcommand?: string; subcommand?: string;
status?: SlashCommandStatus; status?: SlashCommandStatus;
extension_id?: string;
toOpenTelemetryAttributes(config: Config): LogAttributes; toOpenTelemetryAttributes(config: Config): LogAttributes;
toLogBody(): string; toLogBody(): string;
} }
@@ -635,6 +646,7 @@ export function makeSlashCommandEvent({
command, command,
subcommand, subcommand,
status, status,
extension_id,
}: Omit< }: Omit<
SlashCommandEvent, SlashCommandEvent,
CommonFields | 'toOpenTelemetryAttributes' | 'toLogBody' CommonFields | 'toOpenTelemetryAttributes' | 'toLogBody'
@@ -645,6 +657,7 @@ export function makeSlashCommandEvent({
command, command,
subcommand, subcommand,
status, status,
extension_id,
toOpenTelemetryAttributes(config: Config): LogAttributes { toOpenTelemetryAttributes(config: Config): LogAttributes {
return { return {
...getCommonAttributes(config), ...getCommonAttributes(config),
@@ -653,6 +666,7 @@ export function makeSlashCommandEvent({
command: this.command, command: this.command,
subcommand: this.subcommand, subcommand: this.subcommand,
status: this.status, status: this.status,
extension_id: this.extension_id,
}; };
}, },
toLogBody(): string { toLogBody(): string {
@@ -1041,12 +1055,14 @@ export class ExtensionInstallEvent implements BaseTelemetryEvent {
'event.name': 'extension_install'; 'event.name': 'extension_install';
'event.timestamp': string; 'event.timestamp': string;
extension_name: string; extension_name: string;
extension_id: string;
extension_version: string; extension_version: string;
extension_source: string; extension_source: string;
status: 'success' | 'error'; status: 'success' | 'error';
constructor( constructor(
extension_name: string, extension_name: string,
extension_id: string,
extension_version: string, extension_version: string,
extension_source: string, extension_source: string,
status: 'success' | 'error', status: 'success' | 'error',
@@ -1054,6 +1070,7 @@ export class ExtensionInstallEvent implements BaseTelemetryEvent {
this['event.name'] = 'extension_install'; this['event.name'] = 'extension_install';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name; this.extension_name = extension_name;
this.extension_id = extension_id;
this.extension_version = extension_version; this.extension_version = extension_version;
this.extension_source = extension_source; this.extension_source = extension_source;
this.status = status; this.status = status;
@@ -1132,12 +1149,18 @@ export class ExtensionUninstallEvent implements BaseTelemetryEvent {
'event.name': 'extension_uninstall'; 'event.name': 'extension_uninstall';
'event.timestamp': string; 'event.timestamp': string;
extension_name: string; extension_name: string;
extension_id: string;
status: 'success' | 'error'; status: 'success' | 'error';
constructor(extension_name: string, status: 'success' | 'error') { constructor(
extension_name: string,
extension_id: string,
status: 'success' | 'error',
) {
this['event.name'] = 'extension_uninstall'; this['event.name'] = 'extension_uninstall';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name; this.extension_name = extension_name;
this.extension_id = extension_id;
this.status = status; this.status = status;
} }
@@ -1161,6 +1184,7 @@ export class ExtensionUpdateEvent implements BaseTelemetryEvent {
'event.name': 'extension_update'; 'event.name': 'extension_update';
'event.timestamp': string; 'event.timestamp': string;
extension_name: string; extension_name: string;
extension_id: string;
extension_previous_version: string; extension_previous_version: string;
extension_version: string; extension_version: string;
extension_source: string; extension_source: string;
@@ -1168,6 +1192,7 @@ export class ExtensionUpdateEvent implements BaseTelemetryEvent {
constructor( constructor(
extension_name: string, extension_name: string,
extension_id: string,
extension_version: string, extension_version: string,
extension_previous_version: string, extension_previous_version: string,
extension_source: string, extension_source: string,
@@ -1176,6 +1201,7 @@ export class ExtensionUpdateEvent implements BaseTelemetryEvent {
this['event.name'] = 'extension_update'; this['event.name'] = 'extension_update';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name; this.extension_name = extension_name;
this.extension_id = extension_id;
this.extension_version = extension_version; this.extension_version = extension_version;
this.extension_previous_version = extension_previous_version; this.extension_previous_version = extension_previous_version;
this.extension_source = extension_source; this.extension_source = extension_source;
@@ -1205,12 +1231,18 @@ export class ExtensionEnableEvent implements BaseTelemetryEvent {
'event.name': 'extension_enable'; 'event.name': 'extension_enable';
'event.timestamp': string; 'event.timestamp': string;
extension_name: string; extension_name: string;
extension_id: string;
setting_scope: string; setting_scope: string;
constructor(extension_name: string, settingScope: string) { constructor(
extension_name: string,
extension_id: string,
settingScope: string,
) {
this['event.name'] = 'extension_enable'; this['event.name'] = 'extension_enable';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name; this.extension_name = extension_name;
this.extension_id = extension_id;
this.setting_scope = settingScope; this.setting_scope = settingScope;
} }
@@ -1291,12 +1323,18 @@ export class ExtensionDisableEvent implements BaseTelemetryEvent {
'event.name': 'extension_disable'; 'event.name': 'extension_disable';
'event.timestamp': string; 'event.timestamp': string;
extension_name: string; extension_name: string;
extension_id: string;
setting_scope: string; setting_scope: string;
constructor(extension_name: string, settingScope: string) { constructor(
extension_name: string,
extension_id: string,
settingScope: string,
) {
this['event.name'] = 'extension_disable'; this['event.name'] = 'extension_disable';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name; this.extension_name = extension_name;
this.extension_id = extension_id;
this.setting_scope = settingScope; this.setting_scope = settingScope;
} }
+1
View File
@@ -610,6 +610,7 @@ export async function discoverTools(
mcpServerConfig.trust, mcpServerConfig.trust,
undefined, undefined,
cliConfig, cliConfig,
mcpServerConfig.extension?.id,
), ),
); );
} catch (error) { } catch (error) {
+4
View File
@@ -213,6 +213,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
readonly trust?: boolean, readonly trust?: boolean,
nameOverride?: string, nameOverride?: string,
private readonly cliConfig?: Config, private readonly cliConfig?: Config,
override readonly extensionId?: string,
) { ) {
super( super(
nameOverride ?? generateValidName(serverToolName), nameOverride ?? generateValidName(serverToolName),
@@ -222,6 +223,8 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
parameterSchema, parameterSchema,
true, // isOutputMarkdown true, // isOutputMarkdown
false, // canUpdateOutput false, // canUpdateOutput
undefined, // messageBus
extensionId,
); );
} }
@@ -235,6 +238,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
this.trust, this.trust,
`${this.serverName}__${this.serverToolName}`, `${this.serverName}__${this.serverToolName}`,
this.cliConfig, this.cliConfig,
this.extensionId,
); );
} }
+1
View File
@@ -290,6 +290,7 @@ export abstract class DeclarativeTool<
readonly isOutputMarkdown: boolean = true, readonly isOutputMarkdown: boolean = true,
readonly canUpdateOutput: boolean = false, readonly canUpdateOutput: boolean = false,
readonly messageBus?: MessageBus, readonly messageBus?: MessageBus,
readonly extensionId?: string,
) {} ) {}
get schema(): FunctionDeclaration { get schema(): FunctionDeclaration {