Skip to content

Commit b8363e3

Browse files
shishu314Shi Shu
authored andcommitted
metrics(extensions) - Add logging methods for extensions operations (#8702)
Co-authored-by: Shi Shu <shii@google.com>
1 parent e936f0d commit b8363e3

File tree

6 files changed

+246
-1
lines changed

6 files changed

+246
-1
lines changed

packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
InvalidChunkEvent,
2525
ContentRetryEvent,
2626
ContentRetryFailureEvent,
27+
ExtensionEnableEvent,
2728
ExtensionInstallEvent,
2829
ToolOutputTruncatedEvent,
2930
ExtensionUninstallEvent,
@@ -61,6 +62,7 @@ export enum EventNames {
6162
INVALID_CHUNK = 'invalid_chunk',
6263
CONTENT_RETRY = 'content_retry',
6364
CONTENT_RETRY_FAILURE = 'content_retry_failure',
65+
EXTENSION_ENABLE = 'extension_enable',
6466
EXTENSION_INSTALL = 'extension_install',
6567
EXTENSION_UNINSTALL = 'extension_uninstall',
6668
TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated',
@@ -959,6 +961,25 @@ export class ClearcutLogger {
959961
this.flushIfNeeded();
960962
}
961963

964+
logExtensionEnableEvent(event: ExtensionEnableEvent): void {
965+
const data: EventValue[] = [
966+
{
967+
gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
968+
value: event.extension_name,
969+
},
970+
{
971+
gemini_cli_key:
972+
EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE,
973+
value: event.setting_scope,
974+
},
975+
];
976+
977+
this.enqueueLogEvent(
978+
this.createLogEvent(EventNames.EXTENSION_ENABLE, data),
979+
);
980+
this.flushIfNeeded();
981+
}
982+
962983
/**
963984
* Adds default fields to data, and returns a new data array. This fields
964985
* should exist on all log events.

packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ export enum EventMetadataKey {
353353
// Logs the status of the extension uninstall
354354
GEMINI_CLI_EXTENSION_UNINSTALL_STATUS = 96,
355355

356+
// Logs the setting scope for an extension enablement.
357+
GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE = 102,
358+
356359
// ==========================================================================
357360
// Tool Output Truncated Event Keys
358361
// ===========================================================================

packages/core/src/telemetry/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const EVENT_API_REQUEST = 'gemini_cli.api_request';
1212
export const EVENT_API_ERROR = 'gemini_cli.api_error';
1313
export const EVENT_API_RESPONSE = 'gemini_cli.api_response';
1414
export const EVENT_CLI_CONFIG = 'gemini_cli.config';
15+
export const EVENT_EXTENSION_ENABLE = 'gemini_cli.extension_enable';
16+
export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install';
17+
export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall';
1518
export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback';
1619
export const EVENT_RIPGREP_FALLBACK = 'gemini_cli.ripgrep_fallback';
1720
export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check';

packages/core/src/telemetry/loggers.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import {
3333
EVENT_FILE_OPERATION,
3434
EVENT_RIPGREP_FALLBACK,
3535
EVENT_MODEL_ROUTING,
36+
EVENT_EXTENSION_ENABLE,
37+
EVENT_EXTENSION_INSTALL,
38+
EVENT_EXTENSION_UNINSTALL,
3639
} from './constants.js';
3740
import {
3841
logApiRequest,
@@ -47,6 +50,9 @@ import {
4750
logRipgrepFallback,
4851
logToolOutputTruncated,
4952
logModelRouting,
53+
logExtensionEnable,
54+
logExtensionInstallEvent,
55+
logExtensionUninstall,
5056
} from './loggers.js';
5157
import { ToolCallDecision } from './tool-call-decision.js';
5258
import {
@@ -62,11 +68,14 @@ import {
6268
FileOperationEvent,
6369
ToolOutputTruncatedEvent,
6470
ModelRoutingEvent,
71+
ExtensionEnableEvent,
72+
ExtensionInstallEvent,
73+
ExtensionUninstallEvent,
6574
} from './types.js';
6675
import * as metrics from './metrics.js';
6776
import { FileOperation } from './metrics.js';
6877
import * as sdk from './sdk.js';
69-
import { vi, describe, beforeEach, it, expect } from 'vitest';
78+
import { vi, describe, beforeEach, it, expect, afterEach } from 'vitest';
7079
import type { GenerateContentResponseUsageMetadata } from '@google/genai';
7180
import * as uiTelemetry from './uiTelemetry.js';
7281
import { makeFakeConfig } from '../test-utils/config.js';
@@ -1108,4 +1117,122 @@ describe('loggers', () => {
11081117
expect(metrics.recordModelRoutingMetrics).not.toHaveBeenCalled();
11091118
});
11101119
});
1120+
1121+
describe('logExtensionInstall', () => {
1122+
const mockConfig = {
1123+
getSessionId: () => 'test-session-id',
1124+
getUsageStatisticsEnabled: () => true,
1125+
} as unknown as Config;
1126+
1127+
beforeEach(() => {
1128+
vi.spyOn(ClearcutLogger.prototype, 'logExtensionInstallEvent');
1129+
});
1130+
1131+
afterEach(() => {
1132+
vi.resetAllMocks();
1133+
});
1134+
1135+
it('should log extension install event', () => {
1136+
const event = new ExtensionInstallEvent(
1137+
'vscode',
1138+
'0.1.0',
1139+
'git',
1140+
'success',
1141+
);
1142+
1143+
logExtensionInstallEvent(mockConfig, event);
1144+
1145+
expect(
1146+
ClearcutLogger.prototype.logExtensionInstallEvent,
1147+
).toHaveBeenCalledWith(event);
1148+
1149+
expect(mockLogger.emit).toHaveBeenCalledWith({
1150+
body: 'Installed extension vscode',
1151+
attributes: {
1152+
'session.id': 'test-session-id',
1153+
'user.email': 'test-user@example.com',
1154+
'event.name': EVENT_EXTENSION_INSTALL,
1155+
'event.timestamp': '2025-01-01T00:00:00.000Z',
1156+
extension_name: 'vscode',
1157+
extension_version: '0.1.0',
1158+
extension_source: 'git',
1159+
status: 'success',
1160+
},
1161+
});
1162+
});
1163+
});
1164+
1165+
describe('logExtensionUninstall', () => {
1166+
const mockConfig = {
1167+
getSessionId: () => 'test-session-id',
1168+
getUsageStatisticsEnabled: () => true,
1169+
} as unknown as Config;
1170+
1171+
beforeEach(() => {
1172+
vi.spyOn(ClearcutLogger.prototype, 'logExtensionUninstallEvent');
1173+
});
1174+
1175+
afterEach(() => {
1176+
vi.resetAllMocks();
1177+
});
1178+
1179+
it('should log extension uninstall event', () => {
1180+
const event = new ExtensionUninstallEvent('vscode', 'success');
1181+
1182+
logExtensionUninstall(mockConfig, event);
1183+
1184+
expect(
1185+
ClearcutLogger.prototype.logExtensionUninstallEvent,
1186+
).toHaveBeenCalledWith(event);
1187+
1188+
expect(mockLogger.emit).toHaveBeenCalledWith({
1189+
body: 'Uninstalled extension vscode',
1190+
attributes: {
1191+
'session.id': 'test-session-id',
1192+
'user.email': 'test-user@example.com',
1193+
'event.name': EVENT_EXTENSION_UNINSTALL,
1194+
'event.timestamp': '2025-01-01T00:00:00.000Z',
1195+
extension_name: 'vscode',
1196+
status: 'success',
1197+
},
1198+
});
1199+
});
1200+
});
1201+
1202+
describe('logExtensionEnable', () => {
1203+
const mockConfig = {
1204+
getSessionId: () => 'test-session-id',
1205+
getUsageStatisticsEnabled: () => true,
1206+
} as unknown as Config;
1207+
1208+
beforeEach(() => {
1209+
vi.spyOn(ClearcutLogger.prototype, 'logExtensionEnableEvent');
1210+
});
1211+
1212+
afterEach(() => {
1213+
vi.resetAllMocks();
1214+
});
1215+
1216+
it('should log extension enable event', () => {
1217+
const event = new ExtensionEnableEvent('vscode', 'user');
1218+
1219+
logExtensionEnable(mockConfig, event);
1220+
1221+
expect(
1222+
ClearcutLogger.prototype.logExtensionEnableEvent,
1223+
).toHaveBeenCalledWith(event);
1224+
1225+
expect(mockLogger.emit).toHaveBeenCalledWith({
1226+
body: 'Enabled extension vscode',
1227+
attributes: {
1228+
'session.id': 'test-session-id',
1229+
'user.email': 'test-user@example.com',
1230+
'event.name': EVENT_EXTENSION_ENABLE,
1231+
'event.timestamp': '2025-01-01T00:00:00.000Z',
1232+
extension_name: 'vscode',
1233+
setting_scope: 'user',
1234+
},
1235+
});
1236+
});
1237+
});
11111238
});

packages/core/src/telemetry/loggers.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
EVENT_API_REQUEST,
1414
EVENT_API_RESPONSE,
1515
EVENT_CLI_CONFIG,
16+
EVENT_EXTENSION_UNINSTALL,
17+
EVENT_EXTENSION_ENABLE,
1618
EVENT_IDE_CONNECTION,
1719
EVENT_TOOL_CALL,
1820
EVENT_USER_PROMPT,
@@ -29,6 +31,7 @@ import {
2931
EVENT_FILE_OPERATION,
3032
EVENT_RIPGREP_FALLBACK,
3133
EVENT_MODEL_ROUTING,
34+
EVENT_EXTENSION_INSTALL,
3235
} from './constants.js';
3336
import type {
3437
ApiErrorEvent,
@@ -54,6 +57,9 @@ import type {
5457
RipgrepFallbackEvent,
5558
ToolOutputTruncatedEvent,
5659
ModelRoutingEvent,
60+
ExtensionEnableEvent,
61+
ExtensionUninstallEvent,
62+
ExtensionInstallEvent,
5763
} from './types.js';
5864
import {
5965
recordApiErrorMetrics,
@@ -691,3 +697,73 @@ export function logModelRouting(
691697
logger.emit(logRecord);
692698
recordModelRoutingMetrics(config, event);
693699
}
700+
701+
export function logExtensionInstallEvent(
702+
config: Config,
703+
event: ExtensionInstallEvent,
704+
): void {
705+
ClearcutLogger.getInstance(config)?.logExtensionInstallEvent(event);
706+
if (!isTelemetrySdkInitialized()) return;
707+
708+
const attributes: LogAttributes = {
709+
...getCommonAttributes(config),
710+
...event,
711+
'event.name': EVENT_EXTENSION_INSTALL,
712+
'event.timestamp': new Date().toISOString(),
713+
extension_name: event.extension_name,
714+
extension_version: event.extension_version,
715+
extension_source: event.extension_source,
716+
status: event.status,
717+
};
718+
719+
const logger = logs.getLogger(SERVICE_NAME);
720+
const logRecord: LogRecord = {
721+
body: `Installed extension ${event.extension_name}`,
722+
attributes,
723+
};
724+
logger.emit(logRecord);
725+
}
726+
727+
export function logExtensionUninstall(
728+
config: Config,
729+
event: ExtensionUninstallEvent,
730+
): void {
731+
ClearcutLogger.getInstance(config)?.logExtensionUninstallEvent(event);
732+
if (!isTelemetrySdkInitialized()) return;
733+
734+
const attributes: LogAttributes = {
735+
...getCommonAttributes(config),
736+
...event,
737+
'event.name': EVENT_EXTENSION_UNINSTALL,
738+
'event.timestamp': new Date().toISOString(),
739+
};
740+
741+
const logger = logs.getLogger(SERVICE_NAME);
742+
const logRecord: LogRecord = {
743+
body: `Uninstalled extension ${event.extension_name}`,
744+
attributes,
745+
};
746+
logger.emit(logRecord);
747+
}
748+
749+
export function logExtensionEnable(
750+
config: Config,
751+
event: ExtensionEnableEvent,
752+
): void {
753+
ClearcutLogger.getInstance(config)?.logExtensionEnableEvent(event);
754+
if (!isTelemetrySdkInitialized()) return;
755+
756+
const attributes: LogAttributes = {
757+
...getCommonAttributes(config),
758+
...event,
759+
'event.name': EVENT_EXTENSION_ENABLE,
760+
'event.timestamp': new Date().toISOString(),
761+
};
762+
763+
const logger = logs.getLogger(SERVICE_NAME);
764+
const logRecord: LogRecord = {
765+
body: `Enabled extension ${event.extension_name}`,
766+
attributes,
767+
};
768+
logger.emit(logRecord);
769+
}

packages/core/src/telemetry/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ export type TelemetryEvent =
576576
| InvalidChunkEvent
577577
| ContentRetryEvent
578578
| ContentRetryFailureEvent
579+
| ExtensionEnableEvent
579580
| ExtensionInstallEvent
580581
| ExtensionUninstallEvent
581582
| ModelRoutingEvent
@@ -648,3 +649,17 @@ export class ExtensionUninstallEvent implements BaseTelemetryEvent {
648649
this.status = status;
649650
}
650651
}
652+
653+
export class ExtensionEnableEvent implements BaseTelemetryEvent {
654+
'event.name': 'extension_enable';
655+
'event.timestamp': string;
656+
extension_name: string;
657+
setting_scope: string;
658+
659+
constructor(extension_name: string, settingScope: string) {
660+
this['event.name'] = 'extension_enable';
661+
this['event.timestamp'] = new Date().toISOString();
662+
this.extension_name = extension_name;
663+
this.setting_scope = settingScope;
664+
}
665+
}

0 commit comments

Comments
 (0)