metrics(extensions) - Add logging methods for extensions operations (#8702)

Co-authored-by: Shi Shu <shii@google.com>
This commit is contained in:
shishu314
2025-09-18 14:01:36 -04:00
committed by GitHub
parent 853ae56e7e
commit bcfd50b45b
6 changed files with 246 additions and 1 deletions

View File

@@ -24,6 +24,7 @@ import type {
InvalidChunkEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
ExtensionEnableEvent,
ExtensionInstallEvent,
ToolOutputTruncatedEvent,
ExtensionUninstallEvent,
@@ -61,6 +62,7 @@ export enum EventNames {
INVALID_CHUNK = 'invalid_chunk',
CONTENT_RETRY = 'content_retry',
CONTENT_RETRY_FAILURE = 'content_retry_failure',
EXTENSION_ENABLE = 'extension_enable',
EXTENSION_INSTALL = 'extension_install',
EXTENSION_UNINSTALL = 'extension_uninstall',
TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated',
@@ -959,6 +961,25 @@ export class ClearcutLogger {
this.flushIfNeeded();
}
logExtensionEnableEvent(event: ExtensionEnableEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
value: event.extension_name,
},
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE,
value: event.setting_scope,
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.EXTENSION_ENABLE, data),
);
this.flushIfNeeded();
}
/**
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.

View File

@@ -353,6 +353,9 @@ export enum EventMetadataKey {
// Logs the status of the extension uninstall
GEMINI_CLI_EXTENSION_UNINSTALL_STATUS = 96,
// Logs the setting scope for an extension enablement.
GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE = 102,
// ==========================================================================
// Tool Output Truncated Event Keys
// ===========================================================================

View File

@@ -12,6 +12,9 @@ export const EVENT_API_REQUEST = 'gemini_cli.api_request';
export const EVENT_API_ERROR = 'gemini_cli.api_error';
export const EVENT_API_RESPONSE = 'gemini_cli.api_response';
export const EVENT_CLI_CONFIG = 'gemini_cli.config';
export const EVENT_EXTENSION_ENABLE = 'gemini_cli.extension_enable';
export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install';
export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall';
export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback';
export const EVENT_RIPGREP_FALLBACK = 'gemini_cli.ripgrep_fallback';
export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check';

View File

@@ -33,6 +33,9 @@ import {
EVENT_FILE_OPERATION,
EVENT_RIPGREP_FALLBACK,
EVENT_MODEL_ROUTING,
EVENT_EXTENSION_ENABLE,
EVENT_EXTENSION_INSTALL,
EVENT_EXTENSION_UNINSTALL,
} from './constants.js';
import {
logApiRequest,
@@ -47,6 +50,9 @@ import {
logRipgrepFallback,
logToolOutputTruncated,
logModelRouting,
logExtensionEnable,
logExtensionInstallEvent,
logExtensionUninstall,
} from './loggers.js';
import { ToolCallDecision } from './tool-call-decision.js';
import {
@@ -62,11 +68,14 @@ import {
FileOperationEvent,
ToolOutputTruncatedEvent,
ModelRoutingEvent,
ExtensionEnableEvent,
ExtensionInstallEvent,
ExtensionUninstallEvent,
} from './types.js';
import * as metrics from './metrics.js';
import { FileOperation } from './metrics.js';
import * as sdk from './sdk.js';
import { vi, describe, beforeEach, it, expect } from 'vitest';
import { vi, describe, beforeEach, it, expect, afterEach } from 'vitest';
import type { GenerateContentResponseUsageMetadata } from '@google/genai';
import * as uiTelemetry from './uiTelemetry.js';
import { makeFakeConfig } from '../test-utils/config.js';
@@ -1108,4 +1117,122 @@ describe('loggers', () => {
expect(metrics.recordModelRoutingMetrics).not.toHaveBeenCalled();
});
});
describe('logExtensionInstall', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
} as unknown as Config;
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logExtensionInstallEvent');
});
afterEach(() => {
vi.resetAllMocks();
});
it('should log extension install event', () => {
const event = new ExtensionInstallEvent(
'vscode',
'0.1.0',
'git',
'success',
);
logExtensionInstallEvent(mockConfig, event);
expect(
ClearcutLogger.prototype.logExtensionInstallEvent,
).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Installed extension vscode',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_EXTENSION_INSTALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode',
extension_version: '0.1.0',
extension_source: 'git',
status: 'success',
},
});
});
});
describe('logExtensionUninstall', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
} as unknown as Config;
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logExtensionUninstallEvent');
});
afterEach(() => {
vi.resetAllMocks();
});
it('should log extension uninstall event', () => {
const event = new ExtensionUninstallEvent('vscode', 'success');
logExtensionUninstall(mockConfig, event);
expect(
ClearcutLogger.prototype.logExtensionUninstallEvent,
).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Uninstalled extension vscode',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_EXTENSION_UNINSTALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode',
status: 'success',
},
});
});
});
describe('logExtensionEnable', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
} as unknown as Config;
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logExtensionEnableEvent');
});
afterEach(() => {
vi.resetAllMocks();
});
it('should log extension enable event', () => {
const event = new ExtensionEnableEvent('vscode', 'user');
logExtensionEnable(mockConfig, event);
expect(
ClearcutLogger.prototype.logExtensionEnableEvent,
).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Enabled extension vscode',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_EXTENSION_ENABLE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
extension_name: 'vscode',
setting_scope: 'user',
},
});
});
});
});

View File

@@ -13,6 +13,8 @@ import {
EVENT_API_REQUEST,
EVENT_API_RESPONSE,
EVENT_CLI_CONFIG,
EVENT_EXTENSION_UNINSTALL,
EVENT_EXTENSION_ENABLE,
EVENT_IDE_CONNECTION,
EVENT_TOOL_CALL,
EVENT_USER_PROMPT,
@@ -29,6 +31,7 @@ import {
EVENT_FILE_OPERATION,
EVENT_RIPGREP_FALLBACK,
EVENT_MODEL_ROUTING,
EVENT_EXTENSION_INSTALL,
} from './constants.js';
import type {
ApiErrorEvent,
@@ -54,6 +57,9 @@ import type {
RipgrepFallbackEvent,
ToolOutputTruncatedEvent,
ModelRoutingEvent,
ExtensionEnableEvent,
ExtensionUninstallEvent,
ExtensionInstallEvent,
} from './types.js';
import {
recordApiErrorMetrics,
@@ -691,3 +697,73 @@ export function logModelRouting(
logger.emit(logRecord);
recordModelRoutingMetrics(config, event);
}
export function logExtensionInstallEvent(
config: Config,
event: ExtensionInstallEvent,
): void {
ClearcutLogger.getInstance(config)?.logExtensionInstallEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_EXTENSION_INSTALL,
'event.timestamp': new Date().toISOString(),
extension_name: event.extension_name,
extension_version: event.extension_version,
extension_source: event.extension_source,
status: event.status,
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Installed extension ${event.extension_name}`,
attributes,
};
logger.emit(logRecord);
}
export function logExtensionUninstall(
config: Config,
event: ExtensionUninstallEvent,
): void {
ClearcutLogger.getInstance(config)?.logExtensionUninstallEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_EXTENSION_UNINSTALL,
'event.timestamp': new Date().toISOString(),
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Uninstalled extension ${event.extension_name}`,
attributes,
};
logger.emit(logRecord);
}
export function logExtensionEnable(
config: Config,
event: ExtensionEnableEvent,
): void {
ClearcutLogger.getInstance(config)?.logExtensionEnableEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_EXTENSION_ENABLE,
'event.timestamp': new Date().toISOString(),
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Enabled extension ${event.extension_name}`,
attributes,
};
logger.emit(logRecord);
}

View File

@@ -576,6 +576,7 @@ export type TelemetryEvent =
| InvalidChunkEvent
| ContentRetryEvent
| ContentRetryFailureEvent
| ExtensionEnableEvent
| ExtensionInstallEvent
| ExtensionUninstallEvent
| ModelRoutingEvent
@@ -648,3 +649,17 @@ export class ExtensionUninstallEvent implements BaseTelemetryEvent {
this.status = status;
}
}
export class ExtensionEnableEvent implements BaseTelemetryEvent {
'event.name': 'extension_enable';
'event.timestamp': string;
extension_name: string;
setting_scope: string;
constructor(extension_name: string, settingScope: string) {
this['event.name'] = 'extension_enable';
this['event.timestamp'] = new Date().toISOString();
this.extension_name = extension_name;
this.setting_scope = settingScope;
}
}