diff --git a/package-lock.json b/package-lock.json index 1830ea72950..87058e63c1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2501,7 +2501,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2682,7 +2681,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2716,7 +2714,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -3085,7 +3082,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3119,7 +3115,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -3172,7 +3167,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -4410,7 +4404,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4688,7 +4681,6 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5700,7 +5692,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6145,7 +6136,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/array-includes": { "version": "3.1.9", @@ -7433,6 +7425,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -8755,7 +8748,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9360,6 +9352,7 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9369,6 +9362,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9378,6 +9372,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -9631,6 +9626,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -9649,6 +9645,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9657,13 +9654,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -10947,7 +10946,6 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.6.tgz", "integrity": "sha512-QHl6l1cl3zPCaRMzt9TUbTX6Q5SzvkGEZDDad0DmSf5SPmT1/90k6pGPejEvDCJprkitwObXpPaTWGHItqsy4g==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -14136,7 +14134,8 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/path-type": { "version": "3.0.0", @@ -14716,7 +14715,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14727,7 +14725,6 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16547,6 +16544,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/systeminformation": { + "version": "5.30.2", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.2.tgz", + "integrity": "sha512-Rrt5oFTWluUVuPlbtn3o9ja+nvjdF3Um4DG0KxqfYvpzcx7Q9plZBTjJiJy9mAouua4+OI7IUGBaG9Zyt9NgxA==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -16972,7 +16995,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17199,8 +17221,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -17208,7 +17229,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -17392,7 +17412,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17555,6 +17574,7 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -17610,7 +17630,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17727,7 +17746,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17741,7 +17759,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18448,7 +18465,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18923,6 +18939,7 @@ "shell-quote": "^1.8.3", "simple-git": "^3.28.0", "strip-ansi": "^7.1.0", + "systeminformation": "^5.25.11", "tree-sitter-bash": "^0.25.0", "undici": "^7.10.0", "uuid": "^13.0.0", @@ -19013,7 +19030,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/core/package.json b/packages/core/package.json index 7bbeeed2fa7..428066517b9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -64,6 +64,7 @@ "shell-quote": "^1.8.3", "simple-git": "^3.28.0", "strip-ansi": "^7.1.0", + "systeminformation": "^5.25.11", "tree-sitter-bash": "^0.25.0", "undici": "^7.10.0", "uuid": "^13.0.0", diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 349fa182eb6..8af85e88d40 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -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 { 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(); + 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; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index f3fc7e13470..46e5828f706 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -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 { + 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 { + 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 { 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; + }, }; diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index e53ae71ae98..5f12b8442e6 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -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, } diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index c0023a16802..d584dc8ae7e 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -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(), diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 7ab974213fc..eef2fe6db7f 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -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 = {