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