mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -07:00
Collect hardware details telemetry. (#16119)
This commit is contained in:
committed by
GitHub
parent
e9c9dd1d67
commit
6ef2a92233
@@ -26,6 +26,7 @@ import { makeFakeConfig } from '../../test-utils/config.js';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../mocks/msw.js';
|
||||
import {
|
||||
StartSessionEvent,
|
||||
UserPromptEvent,
|
||||
makeChatCompressionEvent,
|
||||
ModelRoutingEvent,
|
||||
@@ -40,6 +41,9 @@ import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
|
||||
import { UserAccountManager } from '../../utils/userAccountManager.js';
|
||||
import { InstallationManager } from '../../utils/installationManager.js';
|
||||
|
||||
import si from 'systeminformation';
|
||||
import type { Systeminformation } from 'systeminformation';
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
toHaveMetadataValue: ([key, value]: [EventMetadataKey, string]) => R;
|
||||
toHaveEventName: (name: EventNames) => R;
|
||||
@@ -111,8 +115,24 @@ expect.extend({
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...actual,
|
||||
cpus: vi.fn(() => [{ model: 'Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz' }]),
|
||||
totalmem: vi.fn(() => 32 * 1024 * 1024 * 1024),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../utils/userAccountManager.js');
|
||||
vi.mock('../../utils/installationManager.js');
|
||||
vi.mock('systeminformation', () => ({
|
||||
default: {
|
||||
graphics: vi.fn().mockResolvedValue({
|
||||
controllers: [{ model: 'Mock GPU' }],
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUserAccount = vi.mocked(UserAccountManager.prototype);
|
||||
const mockInstallMgr = vi.mocked(InstallationManager.prototype);
|
||||
@@ -204,6 +224,7 @@ describe('ClearcutLogger', () => {
|
||||
|
||||
afterEach(() => {
|
||||
ClearcutLogger.clearInstance();
|
||||
TEST_ONLY.resetCachedGpuInfoForTesting();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -238,7 +259,7 @@ describe('ClearcutLogger', () => {
|
||||
});
|
||||
|
||||
describe('createLogEvent', () => {
|
||||
it('logs the total number of google accounts', () => {
|
||||
it('logs the total number of google accounts', async () => {
|
||||
const { logger } = setup({
|
||||
lifetimeGoogleAccounts: 9001,
|
||||
});
|
||||
@@ -346,6 +367,73 @@ describe('ClearcutLogger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('logs the GPU information (single GPU)', async () => {
|
||||
vi.mocked(si.graphics).mockResolvedValueOnce({
|
||||
controllers: [{ model: 'Single GPU' }],
|
||||
} as unknown as Systeminformation.GraphicsData);
|
||||
const { logger, loggerConfig } = setup({});
|
||||
|
||||
await logger?.logStartSessionEvent(new StartSessionEvent(loggerConfig));
|
||||
|
||||
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
|
||||
|
||||
const gpuInfoEntry = event?.event_metadata[0].find(
|
||||
(item) => item.gemini_cli_key === EventMetadataKey.GEMINI_CLI_GPU_INFO,
|
||||
);
|
||||
expect(gpuInfoEntry).toBeDefined();
|
||||
expect(gpuInfoEntry?.value).toBe('Single GPU');
|
||||
});
|
||||
|
||||
it('logs multiple GPUs', async () => {
|
||||
vi.mocked(si.graphics).mockResolvedValueOnce({
|
||||
controllers: [{ model: 'GPU 1' }, { model: 'GPU 2' }],
|
||||
} as unknown as Systeminformation.GraphicsData);
|
||||
const { logger, loggerConfig } = setup({});
|
||||
|
||||
await logger?.logStartSessionEvent(new StartSessionEvent(loggerConfig));
|
||||
|
||||
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
|
||||
const metadata = event?.event_metadata[0];
|
||||
|
||||
const gpuInfoEntry = metadata?.find(
|
||||
(m) => m.gemini_cli_key === EventMetadataKey.GEMINI_CLI_GPU_INFO,
|
||||
);
|
||||
expect(gpuInfoEntry?.value).toBe('GPU 1, GPU 2');
|
||||
});
|
||||
|
||||
it('logs NA when no GPUs are found', async () => {
|
||||
vi.mocked(si.graphics).mockResolvedValueOnce({
|
||||
controllers: [],
|
||||
} as unknown as Systeminformation.GraphicsData);
|
||||
const { logger, loggerConfig } = setup({});
|
||||
|
||||
await logger?.logStartSessionEvent(new StartSessionEvent(loggerConfig));
|
||||
|
||||
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
|
||||
const metadata = event?.event_metadata[0];
|
||||
|
||||
const gpuInfoEntry = metadata?.find(
|
||||
(m) => m.gemini_cli_key === EventMetadataKey.GEMINI_CLI_GPU_INFO,
|
||||
);
|
||||
expect(gpuInfoEntry?.value).toBe('NA');
|
||||
});
|
||||
|
||||
it('logs FAILED when GPU detection fails', async () => {
|
||||
vi.mocked(si.graphics).mockRejectedValueOnce(
|
||||
new Error('Detection failed'),
|
||||
);
|
||||
const { logger, loggerConfig } = setup({});
|
||||
|
||||
await logger?.logStartSessionEvent(new StartSessionEvent(loggerConfig));
|
||||
|
||||
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
|
||||
|
||||
expect(event?.event_metadata[0]).toContainEqual({
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GPU_INFO,
|
||||
value: 'FAILED',
|
||||
});
|
||||
});
|
||||
|
||||
type SurfaceDetectionTestCase = {
|
||||
name: string;
|
||||
env: Record<string, string | undefined>;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as os from 'node:os';
|
||||
import si from 'systeminformation';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type {
|
||||
StartSessionEvent,
|
||||
@@ -57,6 +59,7 @@ import {
|
||||
isCloudShell,
|
||||
} from '../../ide/detect-ide.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
export enum EventNames {
|
||||
START_SESSION = 'start_session',
|
||||
@@ -190,6 +193,35 @@ const MAX_EVENTS = 1000;
|
||||
*/
|
||||
const MAX_RETRY_EVENTS = 100;
|
||||
|
||||
const NO_GPU = 'NA';
|
||||
|
||||
let cachedGpuInfo: string | undefined;
|
||||
|
||||
async function refreshGpuInfo(): Promise<void> {
|
||||
try {
|
||||
const graphics = await si.graphics();
|
||||
if (graphics.controllers && graphics.controllers.length > 0) {
|
||||
cachedGpuInfo = graphics.controllers.map((c) => c.model).join(', ');
|
||||
} else {
|
||||
cachedGpuInfo = NO_GPU;
|
||||
}
|
||||
} catch (error) {
|
||||
cachedGpuInfo = 'FAILED';
|
||||
debugLogger.error(
|
||||
'Failed to get GPU information for telemetry',
|
||||
getErrorMessage(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getGpuInfo(): Promise<string> {
|
||||
if (!cachedGpuInfo) {
|
||||
await refreshGpuInfo();
|
||||
}
|
||||
|
||||
return cachedGpuInfo ?? NO_GPU;
|
||||
}
|
||||
|
||||
// Singleton class for batch posting log events to Clearcut. When a new event comes in, the elapsed time
|
||||
// is checked and events are flushed to Clearcut if at least a minute has passed since the last flush.
|
||||
export class ClearcutLogger {
|
||||
@@ -321,7 +353,6 @@ export class ClearcutLogger {
|
||||
const email = this.userAccountManager.getCachedGoogleAccount();
|
||||
const surface = determineSurface();
|
||||
const ghWorkflowName = determineGHWorkflowName();
|
||||
|
||||
const baseMetadata: EventValue[] = [
|
||||
...data,
|
||||
{
|
||||
@@ -475,7 +506,7 @@ export class ClearcutLogger {
|
||||
return result;
|
||||
}
|
||||
|
||||
logStartSessionEvent(event: StartSessionEvent): void {
|
||||
async logStartSessionEvent(event: StartSessionEvent): Promise<void> {
|
||||
const data: EventValue[] = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL,
|
||||
@@ -564,6 +595,29 @@ export class ClearcutLogger {
|
||||
value: event.extension_ids.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
// Add hardware information only to the start session event
|
||||
const cpus = os.cpus();
|
||||
data.push(
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_INFO,
|
||||
value: cpus[0].model,
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_CORES,
|
||||
value: cpus.length.toString(),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_RAM_TOTAL_GB,
|
||||
value: (os.totalmem() / 1024 ** 3).toFixed(2).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
const gpuInfo = await getGpuInfo();
|
||||
data.push({
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GPU_INFO,
|
||||
value: gpuInfo,
|
||||
});
|
||||
this.sessionData = data;
|
||||
|
||||
// Flush after experiments finish loading from CCPA server
|
||||
@@ -1533,4 +1587,8 @@ export class ClearcutLogger {
|
||||
export const TEST_ONLY = {
|
||||
MAX_RETRY_EVENTS,
|
||||
MAX_EVENTS,
|
||||
refreshGpuInfo,
|
||||
resetCachedGpuInfoForTesting: () => {
|
||||
cachedGpuInfo = undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -517,4 +517,16 @@ export enum EventMetadataKey {
|
||||
|
||||
// Logs the exit code of the hook script (if applicable).
|
||||
GEMINI_CLI_HOOK_EXIT_CODE = 136,
|
||||
|
||||
// Logs CPU information of user machine.
|
||||
GEMINI_CLI_CPU_INFO = 137,
|
||||
|
||||
// Logs number of CPU cores of user machine.
|
||||
GEMINI_CLI_CPU_CORES = 138,
|
||||
|
||||
// Logs GPU information of user machine.
|
||||
GEMINI_CLI_GPU_INFO = 139,
|
||||
|
||||
// Logs total RAM in GB of user machine.
|
||||
GEMINI_CLI_RAM_TOTAL_GB = 140,
|
||||
}
|
||||
|
||||
@@ -111,6 +111,14 @@ import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
import { InstallationManager } from '../utils/installationManager.js';
|
||||
import { AgentTerminateMode } from '../agents/types.js';
|
||||
|
||||
vi.mock('systeminformation', () => ({
|
||||
default: {
|
||||
graphics: vi.fn().mockResolvedValue({
|
||||
controllers: [{ model: 'Mock GPU' }],
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('loggers', () => {
|
||||
const mockLogger = {
|
||||
emit: vi.fn(),
|
||||
|
||||
@@ -79,7 +79,7 @@ export function logCliConfiguration(
|
||||
config: Config,
|
||||
event: StartSessionEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
void ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
|
||||
Reference in New Issue
Block a user