mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
feat(telemetry): add keychain availability and token storage metrics (#18971)
This commit is contained in:
@@ -79,6 +79,14 @@ vi.mock('./oauth-credential-storage.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({
|
||||||
|
HybridTokenStorage: vi.fn(() => ({
|
||||||
|
getCredentials: vi.fn(),
|
||||||
|
setCredentials: vi.fn(),
|
||||||
|
deleteCredentials: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
getNoBrowser: () => false,
|
getNoBrowser: () => false,
|
||||||
getProxy: () => 'http://test.proxy.com:8080',
|
getProxy: () => 'http://test.proxy.com:8080',
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ vi.mock('./keychain-token-storage.js', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../code_assist/oauth-credential-storage.js', () => ({
|
||||||
|
OAuthCredentialStorage: {
|
||||||
|
saveCredentials: vi.fn(),
|
||||||
|
loadCredentials: vi.fn(),
|
||||||
|
clearCredentials: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../core/apiKeyCredentialStorage.js', () => ({
|
||||||
|
loadApiKey: vi.fn(),
|
||||||
|
saveApiKey: vi.fn(),
|
||||||
|
clearApiKey: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('./file-token-storage.js', () => ({
|
vi.mock('./file-token-storage.js', () => ({
|
||||||
FileTokenStorage: vi.fn().mockImplementation(() => ({
|
FileTokenStorage: vi.fn().mockImplementation(() => ({
|
||||||
getCredentials: vi.fn(),
|
getCredentials: vi.fn(),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { BaseTokenStorage } from './base-token-storage.js';
|
|||||||
import { FileTokenStorage } from './file-token-storage.js';
|
import { FileTokenStorage } from './file-token-storage.js';
|
||||||
import type { TokenStorage, OAuthCredentials } from './types.js';
|
import type { TokenStorage, OAuthCredentials } from './types.js';
|
||||||
import { TokenStorageType } from './types.js';
|
import { TokenStorageType } from './types.js';
|
||||||
|
import { coreEvents } from '../../utils/events.js';
|
||||||
|
import { TokenStorageInitializationEvent } from '../../telemetry/types.js';
|
||||||
|
|
||||||
const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_FORCE_FILE_STORAGE';
|
const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_FORCE_FILE_STORAGE';
|
||||||
|
|
||||||
@@ -34,6 +36,11 @@ export class HybridTokenStorage extends BaseTokenStorage {
|
|||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
this.storage = keychainStorage;
|
this.storage = keychainStorage;
|
||||||
this.storageType = TokenStorageType.KEYCHAIN;
|
this.storageType = TokenStorageType.KEYCHAIN;
|
||||||
|
|
||||||
|
coreEvents.emitTelemetryTokenStorageType(
|
||||||
|
new TokenStorageInitializationEvent('keychain', forceFileStorage),
|
||||||
|
);
|
||||||
|
|
||||||
return this.storage;
|
return this.storage;
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
@@ -43,6 +50,11 @@ export class HybridTokenStorage extends BaseTokenStorage {
|
|||||||
|
|
||||||
this.storage = new FileTokenStorage(this.serviceName);
|
this.storage = new FileTokenStorage(this.serviceName);
|
||||||
this.storageType = TokenStorageType.ENCRYPTED_FILE;
|
this.storageType = TokenStorageType.ENCRYPTED_FILE;
|
||||||
|
|
||||||
|
coreEvents.emitTelemetryTokenStorageType(
|
||||||
|
new TokenStorageInitializationEvent('encrypted_file', forceFileStorage),
|
||||||
|
);
|
||||||
|
|
||||||
return this.storage;
|
return this.storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,20 @@ vi.mock('keytar', () => ({
|
|||||||
default: mockKeytar,
|
default: mockKeytar,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('node:crypto', () => ({
|
vi.mock('node:crypto', async (importOriginal) => {
|
||||||
randomBytes: vi.fn(() => ({
|
const actual = await importOriginal<typeof import('node:crypto')>();
|
||||||
toString: vi.fn(() => mockCryptoRandomBytesString),
|
return {
|
||||||
})),
|
...actual,
|
||||||
}));
|
randomBytes: vi.fn(() => ({
|
||||||
|
toString: vi.fn(() => mockCryptoRandomBytesString),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('../../utils/events.js', () => ({
|
vi.mock('../../utils/events.js', () => ({
|
||||||
coreEvents: {
|
coreEvents: {
|
||||||
emitFeedback: vi.fn(),
|
emitFeedback: vi.fn(),
|
||||||
|
emitTelemetryKeychainAvailability: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as crypto from 'node:crypto';
|
|||||||
import { BaseTokenStorage } from './base-token-storage.js';
|
import { BaseTokenStorage } from './base-token-storage.js';
|
||||||
import type { OAuthCredentials, SecretStorage } from './types.js';
|
import type { OAuthCredentials, SecretStorage } from './types.js';
|
||||||
import { coreEvents } from '../../utils/events.js';
|
import { coreEvents } from '../../utils/events.js';
|
||||||
|
import { KeychainAvailabilityEvent } from '../../telemetry/types.js';
|
||||||
|
|
||||||
interface Keytar {
|
interface Keytar {
|
||||||
getPassword(service: string, account: string): Promise<string | null>;
|
getPassword(service: string, account: string): Promise<string | null>;
|
||||||
@@ -263,9 +264,21 @@ export class KeychainTokenStorage
|
|||||||
|
|
||||||
const success = deleted && retrieved === testPassword;
|
const success = deleted && retrieved === testPassword;
|
||||||
this.keychainAvailable = success;
|
this.keychainAvailable = success;
|
||||||
|
|
||||||
|
coreEvents.emitTelemetryKeychainAvailability(
|
||||||
|
new KeychainAvailabilityEvent(success),
|
||||||
|
);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
this.keychainAvailable = false;
|
this.keychainAvailable = false;
|
||||||
|
|
||||||
|
// Do not log the raw error message to avoid potential PII leaks
|
||||||
|
// (e.g. from OS-level error messages containing file paths)
|
||||||
|
coreEvents.emitTelemetryKeychainAvailability(
|
||||||
|
new KeychainAvailabilityEvent(false),
|
||||||
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import type {
|
|||||||
ApprovalModeDurationEvent,
|
ApprovalModeDurationEvent,
|
||||||
PlanExecutionEvent,
|
PlanExecutionEvent,
|
||||||
ToolOutputMaskingEvent,
|
ToolOutputMaskingEvent,
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { EventMetadataKey } from './event-metadata-key.js';
|
import { EventMetadataKey } from './event-metadata-key.js';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
@@ -111,6 +113,8 @@ export enum EventNames {
|
|||||||
APPROVAL_MODE_DURATION = 'approval_mode_duration',
|
APPROVAL_MODE_DURATION = 'approval_mode_duration',
|
||||||
PLAN_EXECUTION = 'plan_execution',
|
PLAN_EXECUTION = 'plan_execution',
|
||||||
TOOL_OUTPUT_MASKING = 'tool_output_masking',
|
TOOL_OUTPUT_MASKING = 'tool_output_masking',
|
||||||
|
KEYCHAIN_AVAILABILITY = 'keychain_availability',
|
||||||
|
TOKEN_STORAGE_INITIALIZATION = 'token_storage_initialization',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogResponse {
|
export interface LogResponse {
|
||||||
@@ -1613,6 +1617,40 @@ export class ClearcutLogger {
|
|||||||
this.flushIfNeeded();
|
this.flushIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logKeychainAvailabilityEvent(event: KeychainAvailabilityEvent): void {
|
||||||
|
const data: EventValue[] = [
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_KEYCHAIN_AVAILABLE,
|
||||||
|
value: JSON.stringify(event.available),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.enqueueLogEvent(
|
||||||
|
this.createLogEvent(EventNames.KEYCHAIN_AVAILABILITY, data),
|
||||||
|
);
|
||||||
|
this.flushIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
logTokenStorageInitializationEvent(
|
||||||
|
event: TokenStorageInitializationEvent,
|
||||||
|
): void {
|
||||||
|
const data: EventValue[] = [
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_TYPE,
|
||||||
|
value: event.type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_FORCED,
|
||||||
|
value: JSON.stringify(event.forced),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.enqueueLogEvent(
|
||||||
|
this.createLogEvent(EventNames.TOKEN_STORAGE_INITIALIZATION, data),
|
||||||
|
);
|
||||||
|
this.flushIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds default fields to data, and returns a new data array. This fields
|
* Adds default fields to data, and returns a new data array. This fields
|
||||||
* should exist on all log events.
|
* should exist on all log events.
|
||||||
|
|||||||
@@ -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: 156
|
// Next ID: 159
|
||||||
|
|
||||||
GEMINI_CLI_KEY_UNKNOWN = 0,
|
GEMINI_CLI_KEY_UNKNOWN = 0,
|
||||||
|
|
||||||
@@ -578,7 +578,6 @@ export enum EventMetadataKey {
|
|||||||
// Logs the total prunable tokens identified at the trigger point.
|
// Logs the total prunable tokens identified at the trigger point.
|
||||||
GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS = 151,
|
GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS = 151,
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Ask User Stats Event Keys
|
// Ask User Stats Event Keys
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
@@ -593,4 +592,17 @@ export enum EventMetadataKey {
|
|||||||
|
|
||||||
// Logs the number of questions answered in the ask_user tool.
|
// Logs the number of questions answered in the ask_user tool.
|
||||||
GEMINI_CLI_ASK_USER_ANSWER_COUNT = 155,
|
GEMINI_CLI_ASK_USER_ANSWER_COUNT = 155,
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Keychain & Token Storage Event Keys
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
// Logs whether the keychain is available.
|
||||||
|
GEMINI_CLI_KEYCHAIN_AVAILABLE = 156,
|
||||||
|
|
||||||
|
// Logs the type of token storage initialized.
|
||||||
|
GEMINI_CLI_TOKEN_STORAGE_TYPE = 157,
|
||||||
|
|
||||||
|
// Logs whether the token storage type was forced by an environment variable.
|
||||||
|
GEMINI_CLI_TOKEN_STORAGE_FORCED = 158,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import type {
|
|||||||
LlmLoopCheckEvent,
|
LlmLoopCheckEvent,
|
||||||
PlanExecutionEvent,
|
PlanExecutionEvent,
|
||||||
ToolOutputMaskingEvent,
|
ToolOutputMaskingEvent,
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
recordApiErrorMetrics,
|
recordApiErrorMetrics,
|
||||||
@@ -76,6 +78,8 @@ import {
|
|||||||
recordLinesChanged,
|
recordLinesChanged,
|
||||||
recordHookCallMetrics,
|
recordHookCallMetrics,
|
||||||
recordPlanExecution,
|
recordPlanExecution,
|
||||||
|
recordKeychainAvailability,
|
||||||
|
recordTokenStorageInitialization,
|
||||||
} from './metrics.js';
|
} from './metrics.js';
|
||||||
import { bufferTelemetryEvent } from './sdk.js';
|
import { bufferTelemetryEvent } from './sdk.js';
|
||||||
import type { UiEvent } from './uiTelemetry.js';
|
import type { UiEvent } from './uiTelemetry.js';
|
||||||
@@ -805,3 +809,37 @@ export function logStartupStats(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logKeychainAvailability(
|
||||||
|
config: Config,
|
||||||
|
event: KeychainAvailabilityEvent,
|
||||||
|
): void {
|
||||||
|
ClearcutLogger.getInstance(config)?.logKeychainAvailabilityEvent(event);
|
||||||
|
bufferTelemetryEvent(() => {
|
||||||
|
const logger = logs.getLogger(SERVICE_NAME);
|
||||||
|
const logRecord: LogRecord = {
|
||||||
|
body: event.toLogBody(),
|
||||||
|
attributes: event.toOpenTelemetryAttributes(config),
|
||||||
|
};
|
||||||
|
logger.emit(logRecord);
|
||||||
|
|
||||||
|
recordKeychainAvailability(config, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logTokenStorageInitialization(
|
||||||
|
config: Config,
|
||||||
|
event: TokenStorageInitializationEvent,
|
||||||
|
): void {
|
||||||
|
ClearcutLogger.getInstance(config)?.logTokenStorageInitializationEvent(event);
|
||||||
|
bufferTelemetryEvent(() => {
|
||||||
|
const logger = logs.getLogger(SERVICE_NAME);
|
||||||
|
const logRecord: LogRecord = {
|
||||||
|
body: event.toLogBody(),
|
||||||
|
attributes: event.toOpenTelemetryAttributes(config),
|
||||||
|
};
|
||||||
|
logger.emit(logRecord);
|
||||||
|
|
||||||
|
recordTokenStorageInitialization(config, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ import {
|
|||||||
ApiRequestPhase,
|
ApiRequestPhase,
|
||||||
} from './metrics.js';
|
} from './metrics.js';
|
||||||
import { makeFakeConfig } from '../test-utils/config.js';
|
import { makeFakeConfig } from '../test-utils/config.js';
|
||||||
import { ModelRoutingEvent, AgentFinishEvent } from './types.js';
|
import {
|
||||||
|
ModelRoutingEvent,
|
||||||
|
AgentFinishEvent,
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
|
} from './types.js';
|
||||||
import { AgentTerminateMode } from '../agents/types.js';
|
import { AgentTerminateMode } from '../agents/types.js';
|
||||||
|
|
||||||
const mockCounterAddFn: Mock<
|
const mockCounterAddFn: Mock<
|
||||||
@@ -97,6 +102,8 @@ describe('Telemetry Metrics', () => {
|
|||||||
let recordLinesChangedModule: typeof import('./metrics.js').recordLinesChanged;
|
let recordLinesChangedModule: typeof import('./metrics.js').recordLinesChanged;
|
||||||
let recordSlowRenderModule: typeof import('./metrics.js').recordSlowRender;
|
let recordSlowRenderModule: typeof import('./metrics.js').recordSlowRender;
|
||||||
let recordPlanExecutionModule: typeof import('./metrics.js').recordPlanExecution;
|
let recordPlanExecutionModule: typeof import('./metrics.js').recordPlanExecution;
|
||||||
|
let recordKeychainAvailabilityModule: typeof import('./metrics.js').recordKeychainAvailability;
|
||||||
|
let recordTokenStorageInitializationModule: typeof import('./metrics.js').recordTokenStorageInitialization;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -142,6 +149,10 @@ describe('Telemetry Metrics', () => {
|
|||||||
recordLinesChangedModule = metricsJsModule.recordLinesChanged;
|
recordLinesChangedModule = metricsJsModule.recordLinesChanged;
|
||||||
recordSlowRenderModule = metricsJsModule.recordSlowRender;
|
recordSlowRenderModule = metricsJsModule.recordSlowRender;
|
||||||
recordPlanExecutionModule = metricsJsModule.recordPlanExecution;
|
recordPlanExecutionModule = metricsJsModule.recordPlanExecution;
|
||||||
|
recordKeychainAvailabilityModule =
|
||||||
|
metricsJsModule.recordKeychainAvailability;
|
||||||
|
recordTokenStorageInitializationModule =
|
||||||
|
metricsJsModule.recordTokenStorageInitialization;
|
||||||
|
|
||||||
const otelApiModule = await import('@opentelemetry/api');
|
const otelApiModule = await import('@opentelemetry/api');
|
||||||
|
|
||||||
@@ -1485,4 +1496,57 @@ describe('Telemetry Metrics', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Keychain and Token Storage Metrics', () => {
|
||||||
|
describe('recordKeychainAvailability', () => {
|
||||||
|
it('should not record metrics if not initialized', () => {
|
||||||
|
const config = makeFakeConfig({});
|
||||||
|
const event = new KeychainAvailabilityEvent(true);
|
||||||
|
recordKeychainAvailabilityModule(config, event);
|
||||||
|
expect(mockCounterAddFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record keychain availability when initialized', () => {
|
||||||
|
const config = makeFakeConfig({});
|
||||||
|
initializeMetricsModule(config);
|
||||||
|
mockCounterAddFn.mockClear();
|
||||||
|
|
||||||
|
const event = new KeychainAvailabilityEvent(true);
|
||||||
|
recordKeychainAvailabilityModule(config, event);
|
||||||
|
|
||||||
|
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
|
||||||
|
'session.id': 'test-session-id',
|
||||||
|
'installation.id': 'test-installation-id',
|
||||||
|
'user.email': 'test@example.com',
|
||||||
|
available: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordTokenStorageInitialization', () => {
|
||||||
|
it('should not record metrics if not initialized', () => {
|
||||||
|
const config = makeFakeConfig({});
|
||||||
|
const event = new TokenStorageInitializationEvent('hybrid', false);
|
||||||
|
recordTokenStorageInitializationModule(config, event);
|
||||||
|
expect(mockCounterAddFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record token storage initialization when initialized', () => {
|
||||||
|
const config = makeFakeConfig({});
|
||||||
|
initializeMetricsModule(config);
|
||||||
|
mockCounterAddFn.mockClear();
|
||||||
|
|
||||||
|
const event = new TokenStorageInitializationEvent('keychain', true);
|
||||||
|
recordTokenStorageInitializationModule(config, event);
|
||||||
|
|
||||||
|
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
|
||||||
|
'session.id': 'test-session-id',
|
||||||
|
'installation.id': 'test-installation-id',
|
||||||
|
'user.email': 'test@example.com',
|
||||||
|
type: 'keychain',
|
||||||
|
forced: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import type {
|
|||||||
ModelSlashCommandEvent,
|
ModelSlashCommandEvent,
|
||||||
AgentFinishEvent,
|
AgentFinishEvent,
|
||||||
RecoveryAttemptEvent,
|
RecoveryAttemptEvent,
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { AuthType } from '../core/contentGenerator.js';
|
import { AuthType } from '../core/contentGenerator.js';
|
||||||
import { getCommonAttributes } from './telemetryAttributes.js';
|
import { getCommonAttributes } from './telemetryAttributes.js';
|
||||||
@@ -37,6 +39,8 @@ const MODEL_SLASH_COMMAND_CALL_COUNT =
|
|||||||
'gemini_cli.slash_command.model.call_count';
|
'gemini_cli.slash_command.model.call_count';
|
||||||
const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count';
|
const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count';
|
||||||
const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency';
|
const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency';
|
||||||
|
const KEYCHAIN_AVAILABILITY_COUNT = 'gemini_cli.keychain.availability.count';
|
||||||
|
const TOKEN_STORAGE_TYPE_COUNT = 'gemini_cli.token_storage.type.count';
|
||||||
|
|
||||||
// Agent Metrics
|
// Agent Metrics
|
||||||
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
|
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
|
||||||
@@ -236,6 +240,25 @@ const COUNTER_DEFINITIONS = {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[KEYCHAIN_AVAILABILITY_COUNT]: {
|
||||||
|
description: 'Counts keychain availability checks.',
|
||||||
|
valueType: ValueType.INT,
|
||||||
|
assign: (c: Counter) => (keychainAvailabilityCounter = c),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
attributes: {} as {
|
||||||
|
available: boolean;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[TOKEN_STORAGE_TYPE_COUNT]: {
|
||||||
|
description: 'Counts token storage type initializations.',
|
||||||
|
valueType: ValueType.INT,
|
||||||
|
assign: (c: Counter) => (tokenStorageTypeCounter = c),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
attributes: {} as {
|
||||||
|
type: string;
|
||||||
|
forced: boolean;
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const HISTOGRAM_DEFINITIONS = {
|
const HISTOGRAM_DEFINITIONS = {
|
||||||
@@ -572,6 +595,8 @@ let planExecutionCounter: Counter | undefined;
|
|||||||
let slowRenderHistogram: Histogram | undefined;
|
let slowRenderHistogram: Histogram | undefined;
|
||||||
let hookCallCounter: Counter | undefined;
|
let hookCallCounter: Counter | undefined;
|
||||||
let hookCallLatencyHistogram: Histogram | undefined;
|
let hookCallLatencyHistogram: Histogram | undefined;
|
||||||
|
let keychainAvailabilityCounter: Counter | undefined;
|
||||||
|
let tokenStorageTypeCounter: Counter | undefined;
|
||||||
|
|
||||||
// OpenTelemetry GenAI Semantic Convention Metrics
|
// OpenTelemetry GenAI Semantic Convention Metrics
|
||||||
let genAiClientTokenUsageHistogram: Histogram | undefined;
|
let genAiClientTokenUsageHistogram: Histogram | undefined;
|
||||||
@@ -1279,3 +1304,32 @@ export function recordHookCallMetrics(
|
|||||||
hookCallCounter.add(1, metricAttributes);
|
hookCallCounter.add(1, metricAttributes);
|
||||||
hookCallLatencyHistogram.record(durationMs, metricAttributes);
|
hookCallLatencyHistogram.record(durationMs, metricAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a metric for keychain availability.
|
||||||
|
*/
|
||||||
|
export function recordKeychainAvailability(
|
||||||
|
config: Config,
|
||||||
|
event: KeychainAvailabilityEvent,
|
||||||
|
): void {
|
||||||
|
if (!keychainAvailabilityCounter || !isMetricsInitialized) return;
|
||||||
|
keychainAvailabilityCounter.add(1, {
|
||||||
|
...baseMetricDefinition.getCommonAttributes(config),
|
||||||
|
available: event.available,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a metric for token storage type initialization.
|
||||||
|
*/
|
||||||
|
export function recordTokenStorageInitialization(
|
||||||
|
config: Config,
|
||||||
|
event: TokenStorageInitializationEvent,
|
||||||
|
): void {
|
||||||
|
if (!tokenStorageTypeCounter || !isMetricsInitialized) return;
|
||||||
|
tokenStorageTypeCounter.add(1, {
|
||||||
|
...baseMetricDefinition.getCommonAttributes(config),
|
||||||
|
type: event.type,
|
||||||
|
forced: event.forced,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ import {
|
|||||||
import { TelemetryTarget } from './index.js';
|
import { TelemetryTarget } from './index.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import { authEvents } from '../code_assist/oauth2.js';
|
import { authEvents } from '../code_assist/oauth2.js';
|
||||||
|
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||||
|
import {
|
||||||
|
logKeychainAvailability,
|
||||||
|
logTokenStorageInitialization,
|
||||||
|
} from './loggers.js';
|
||||||
|
import type {
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
||||||
class DiagLoggerAdapter {
|
class DiagLoggerAdapter {
|
||||||
@@ -86,6 +95,12 @@ let telemetryInitialized = false;
|
|||||||
let callbackRegistered = false;
|
let callbackRegistered = false;
|
||||||
let authListener: ((newCredentials: JWTInput) => Promise<void>) | undefined =
|
let authListener: ((newCredentials: JWTInput) => Promise<void>) | undefined =
|
||||||
undefined;
|
undefined;
|
||||||
|
let keychainAvailabilityListener:
|
||||||
|
| ((event: KeychainAvailabilityEvent) => void)
|
||||||
|
| undefined = undefined;
|
||||||
|
let tokenStorageTypeListener:
|
||||||
|
| ((event: TokenStorageInitializationEvent) => void)
|
||||||
|
| undefined = undefined;
|
||||||
const telemetryBuffer: Array<() => void | Promise<void>> = [];
|
const telemetryBuffer: Array<() => void | Promise<void>> = [];
|
||||||
let activeTelemetryEmail: string | undefined;
|
let activeTelemetryEmail: string | undefined;
|
||||||
|
|
||||||
@@ -196,6 +211,26 @@ export async function initializeTelemetry(
|
|||||||
'session.id': config.getSessionId(),
|
'session.id': config.getSessionId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!keychainAvailabilityListener) {
|
||||||
|
keychainAvailabilityListener = (event: KeychainAvailabilityEvent) => {
|
||||||
|
logKeychainAvailability(config, event);
|
||||||
|
};
|
||||||
|
coreEvents.on(
|
||||||
|
CoreEvent.TelemetryKeychainAvailability,
|
||||||
|
keychainAvailabilityListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenStorageTypeListener) {
|
||||||
|
tokenStorageTypeListener = (event: TokenStorageInitializationEvent) => {
|
||||||
|
logTokenStorageInitialization(config, event);
|
||||||
|
};
|
||||||
|
coreEvents.on(
|
||||||
|
CoreEvent.TelemetryTokenStorageType,
|
||||||
|
tokenStorageTypeListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
|
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
|
||||||
const otlpProtocol = config.getTelemetryOtlpProtocol();
|
const otlpProtocol = config.getTelemetryOtlpProtocol();
|
||||||
const telemetryTarget = config.getTelemetryTarget();
|
const telemetryTarget = config.getTelemetryTarget();
|
||||||
@@ -376,6 +411,20 @@ export async function shutdownTelemetry(
|
|||||||
authEvents.off('post_auth', authListener);
|
authEvents.off('post_auth', authListener);
|
||||||
authListener = undefined;
|
authListener = undefined;
|
||||||
}
|
}
|
||||||
|
if (keychainAvailabilityListener) {
|
||||||
|
coreEvents.off(
|
||||||
|
CoreEvent.TelemetryKeychainAvailability,
|
||||||
|
keychainAvailabilityListener,
|
||||||
|
);
|
||||||
|
keychainAvailabilityListener = undefined;
|
||||||
|
}
|
||||||
|
if (tokenStorageTypeListener) {
|
||||||
|
coreEvents.off(
|
||||||
|
CoreEvent.TelemetryTokenStorageType,
|
||||||
|
tokenStorageTypeListener,
|
||||||
|
);
|
||||||
|
tokenStorageTypeListener = undefined;
|
||||||
|
}
|
||||||
callbackRegistered = false;
|
callbackRegistered = false;
|
||||||
activeTelemetryEmail = undefined;
|
activeTelemetryEmail = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2111,3 +2111,60 @@ export class HookCallEvent implements BaseTelemetryEvent {
|
|||||||
return `Hook call ${hookId} ${status} in ${this.duration_ms}ms`;
|
return `Hook call ${hookId} ${status} in ${this.duration_ms}ms`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EVENT_KEYCHAIN_AVAILABILITY = 'gemini_cli.keychain.availability';
|
||||||
|
export class KeychainAvailabilityEvent implements BaseTelemetryEvent {
|
||||||
|
'event.name': 'keychain_availability';
|
||||||
|
'event.timestamp': string;
|
||||||
|
available: boolean;
|
||||||
|
|
||||||
|
constructor(available: boolean) {
|
||||||
|
this['event.name'] = 'keychain_availability';
|
||||||
|
this['event.timestamp'] = new Date().toISOString();
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||||
|
const attributes: LogAttributes = {
|
||||||
|
...getCommonAttributes(config),
|
||||||
|
'event.name': EVENT_KEYCHAIN_AVAILABILITY,
|
||||||
|
'event.timestamp': this['event.timestamp'],
|
||||||
|
available: this.available,
|
||||||
|
};
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
toLogBody(): string {
|
||||||
|
return `Keychain availability: ${this.available}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EVENT_TOKEN_STORAGE_INITIALIZATION =
|
||||||
|
'gemini_cli.token_storage.initialization';
|
||||||
|
export class TokenStorageInitializationEvent implements BaseTelemetryEvent {
|
||||||
|
'event.name': 'token_storage_initialization';
|
||||||
|
'event.timestamp': string;
|
||||||
|
type: string;
|
||||||
|
forced: boolean;
|
||||||
|
|
||||||
|
constructor(type: string, forced: boolean) {
|
||||||
|
this['event.name'] = 'token_storage_initialization';
|
||||||
|
this['event.timestamp'] = new Date().toISOString();
|
||||||
|
this.type = type;
|
||||||
|
this.forced = forced;
|
||||||
|
}
|
||||||
|
|
||||||
|
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||||
|
return {
|
||||||
|
...getCommonAttributes(config),
|
||||||
|
'event.name': EVENT_TOKEN_STORAGE_INITIALIZATION,
|
||||||
|
'event.timestamp': this['event.timestamp'],
|
||||||
|
type: this.type,
|
||||||
|
forced: this.forced,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toLogBody(): string {
|
||||||
|
return `Token storage initialized. Type: ${this.type}. Forced: ${this.forced}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import type { AgentDefinition } from '../agents/types.js';
|
|||||||
import type { McpClient } from '../tools/mcp-client.js';
|
import type { McpClient } from '../tools/mcp-client.js';
|
||||||
import type { ExtensionEvents } from './extensionLoader.js';
|
import type { ExtensionEvents } from './extensionLoader.js';
|
||||||
import type { EditorType } from './editor.js';
|
import type { EditorType } from './editor.js';
|
||||||
|
import type {
|
||||||
|
TokenStorageInitializationEvent,
|
||||||
|
KeychainAvailabilityEvent,
|
||||||
|
} from '../telemetry/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the severity level for user-facing feedback.
|
* Defines the severity level for user-facing feedback.
|
||||||
@@ -168,6 +172,8 @@ export enum CoreEvent {
|
|||||||
EditorSelected = 'editor-selected',
|
EditorSelected = 'editor-selected',
|
||||||
SlashCommandConflicts = 'slash-command-conflicts',
|
SlashCommandConflicts = 'slash-command-conflicts',
|
||||||
QuotaChanged = 'quota-changed',
|
QuotaChanged = 'quota-changed',
|
||||||
|
TelemetryKeychainAvailability = 'telemetry-keychain-availability',
|
||||||
|
TelemetryTokenStorageType = 'telemetry-token-storage-type',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,6 +204,8 @@ export interface CoreEvents extends ExtensionEvents {
|
|||||||
[CoreEvent.RequestEditorSelection]: never[];
|
[CoreEvent.RequestEditorSelection]: never[];
|
||||||
[CoreEvent.EditorSelected]: [EditorSelectedPayload];
|
[CoreEvent.EditorSelected]: [EditorSelectedPayload];
|
||||||
[CoreEvent.SlashCommandConflicts]: [SlashCommandConflictsPayload];
|
[CoreEvent.SlashCommandConflicts]: [SlashCommandConflictsPayload];
|
||||||
|
[CoreEvent.TelemetryKeychainAvailability]: [KeychainAvailabilityEvent];
|
||||||
|
[CoreEvent.TelemetryTokenStorageType]: [TokenStorageInitializationEvent];
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventBacklogItem = {
|
type EventBacklogItem = {
|
||||||
@@ -367,6 +375,14 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitTelemetryKeychainAvailability(event: KeychainAvailabilityEvent): void {
|
||||||
|
this._emitOrQueue(CoreEvent.TelemetryKeychainAvailability, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitTelemetryTokenStorageType(event: TokenStorageInitializationEvent): void {
|
||||||
|
this._emitOrQueue(CoreEvent.TelemetryTokenStorageType, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const coreEvents = new CoreEventEmitter();
|
export const coreEvents = new CoreEventEmitter();
|
||||||
|
|||||||
Reference in New Issue
Block a user