feat(telemetry): add keychain availability and token storage metrics (#18971)

This commit is contained in:
Abhi
2026-02-18 00:11:38 +09:00
committed by GitHub
parent bbf6800778
commit bf9ca33c18
13 changed files with 388 additions and 8 deletions
@@ -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', () => ({
FileTokenStorage: vi.fn().mockImplementation(() => ({
getCredentials: vi.fn(),
@@ -8,6 +8,8 @@ import { BaseTokenStorage } from './base-token-storage.js';
import { FileTokenStorage } from './file-token-storage.js';
import type { TokenStorage, OAuthCredentials } 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';
@@ -34,6 +36,11 @@ export class HybridTokenStorage extends BaseTokenStorage {
if (isAvailable) {
this.storage = keychainStorage;
this.storageType = TokenStorageType.KEYCHAIN;
coreEvents.emitTelemetryTokenStorageType(
new TokenStorageInitializationEvent('keychain', forceFileStorage),
);
return this.storage;
}
} catch (_e) {
@@ -43,6 +50,11 @@ export class HybridTokenStorage extends BaseTokenStorage {
this.storage = new FileTokenStorage(this.serviceName);
this.storageType = TokenStorageType.ENCRYPTED_FILE;
coreEvents.emitTelemetryTokenStorageType(
new TokenStorageInitializationEvent('encrypted_file', forceFileStorage),
);
return this.storage;
}
@@ -25,15 +25,20 @@ vi.mock('keytar', () => ({
default: mockKeytar,
}));
vi.mock('node:crypto', () => ({
randomBytes: vi.fn(() => ({
toString: vi.fn(() => mockCryptoRandomBytesString),
})),
}));
vi.mock('node:crypto', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:crypto')>();
return {
...actual,
randomBytes: vi.fn(() => ({
toString: vi.fn(() => mockCryptoRandomBytesString),
})),
};
});
vi.mock('../../utils/events.js', () => ({
coreEvents: {
emitFeedback: vi.fn(),
emitTelemetryKeychainAvailability: vi.fn(),
},
}));
@@ -8,6 +8,7 @@ import * as crypto from 'node:crypto';
import { BaseTokenStorage } from './base-token-storage.js';
import type { OAuthCredentials, SecretStorage } from './types.js';
import { coreEvents } from '../../utils/events.js';
import { KeychainAvailabilityEvent } from '../../telemetry/types.js';
interface Keytar {
getPassword(service: string, account: string): Promise<string | null>;
@@ -263,9 +264,21 @@ export class KeychainTokenStorage
const success = deleted && retrieved === testPassword;
this.keychainAvailable = success;
coreEvents.emitTelemetryKeychainAvailability(
new KeychainAvailabilityEvent(success),
);
return success;
} catch (_error) {
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;
}
}