Collect hardware details telemetry. (#16119)

This commit is contained in:
Christian Gunderman
2026-01-12 23:59:22 +00:00
committed by GitHub
parent e9c9dd1d67
commit 6ef2a92233
7 changed files with 213 additions and 30 deletions
@@ -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(),
+1 -1
View File
@@ -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 = {