mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-29 19:20:58 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a5c470faf | |||
| c024064f47 | |||
| ae509f1ba5 | |||
| 65c3d7cabb | |||
| c72ac00a53 | |||
| 2bc481a3b5 | |||
| 169f9e457d | |||
| 8481d977bb | |||
| dd4d7301ba | |||
| 5357770a3d | |||
| dc3ab31af0 |
@@ -58,7 +58,7 @@ describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
|
||||
);
|
||||
|
||||
await run.expectText('Chat history compressed', 5000);
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
// TODO: Context compression is broken and doesn't include the system
|
||||
// instructions or tool counts, so it thinks compression is beneficial when
|
||||
@@ -102,6 +102,9 @@ describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
|
||||
});
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
await run.expectText('tips for getting started:', 5000);
|
||||
// Wait for the async command loaders to finish so /compress is recognized
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await run.type('/compress');
|
||||
await run.type('\r');
|
||||
|
||||
@@ -116,5 +119,5 @@ describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
|
||||
foundEvent,
|
||||
'chat_compression telemetry event should not be found for NOOP',
|
||||
).toBe(false);
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
@@ -33,6 +33,8 @@ describe('Interactive file system', () => {
|
||||
rig.createFile(fileName, '1.0.0');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
await run.expectText('tips for getting started:', 5000);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
// Step 1: Read the file
|
||||
const readPrompt = `Read the version from ${fileName}`;
|
||||
@@ -56,5 +58,5 @@ describe('Interactive file system', () => {
|
||||
|
||||
// Wait for telemetry to flush and file system to sync, especially in sandboxed environments
|
||||
await rig.waitForTelemetryReady();
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
Generated
+3
-32
@@ -449,8 +449,7 @@
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bundled-es-modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
@@ -1474,7 +1473,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -2152,7 +2150,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",
|
||||
@@ -2333,7 +2330,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"
|
||||
}
|
||||
@@ -2383,7 +2379,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2758,7 +2753,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
|
||||
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2792,7 +2786,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
|
||||
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0"
|
||||
@@ -2847,7 +2840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
|
||||
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0",
|
||||
@@ -4054,7 +4046,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4328,7 +4319,6 @@
|
||||
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
@@ -5073,7 +5063,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"
|
||||
},
|
||||
@@ -7151,8 +7140,7 @@
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -7737,7 +7725,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8255,7 +8242,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -9522,7 +9508,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
|
||||
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -9782,7 +9767,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz",
|
||||
"integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.3",
|
||||
@@ -13496,7 +13480,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13507,7 +13490,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15627,7 +15609,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15850,8 +15831,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15859,7 +15839,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16025,7 +16004,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16093,7 +16071,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",
|
||||
@@ -16480,7 +16457,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17051,7 +17027,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17064,7 +17039,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -17703,7 +17677,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"
|
||||
}
|
||||
@@ -18139,7 +18112,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -18258,7 +18230,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -121,8 +121,10 @@ describe('Telemetry Metrics', () => {
|
||||
return actualApi;
|
||||
});
|
||||
|
||||
const { getCommonAttributes } = await import('./telemetryAttributes.js');
|
||||
(getCommonAttributes as Mock).mockReturnValue({
|
||||
const { getCommonMetricAttributes } = await import(
|
||||
'./telemetryAttributes.js'
|
||||
);
|
||||
(getCommonMetricAttributes as Mock).mockReturnValue({
|
||||
'session.id': 'test-session-id',
|
||||
'installation.id': 'test-installation-id',
|
||||
'user.email': 'test@example.com',
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
TokenStorageInitializationEvent,
|
||||
} from './types.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import { getCommonAttributes } from './telemetryAttributes.js';
|
||||
import { getCommonMetricAttributes } from './telemetryAttributes.js';
|
||||
import { sanitizeHookName } from './sanitize.js';
|
||||
|
||||
const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression';
|
||||
@@ -104,7 +104,7 @@ const EXIT_FAIL_COUNT = 'gemini_cli.exit.fail.count';
|
||||
const PLAN_EXECUTION_COUNT = 'gemini_cli.plan.execution.count';
|
||||
|
||||
const baseMetricDefinition = {
|
||||
getCommonAttributes,
|
||||
getCommonAttributes: getCommonMetricAttributes,
|
||||
};
|
||||
|
||||
const COUNTER_DEFINITIONS = {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import {
|
||||
getCommonAttributes,
|
||||
getCommonMetricAttributes,
|
||||
} from './telemetryAttributes.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
import { InstallationManager } from '../utils/installationManager.js';
|
||||
|
||||
vi.mock('../utils/userAccountManager.js');
|
||||
vi.mock('../utils/installationManager.js');
|
||||
|
||||
describe('telemetryAttributes', () => {
|
||||
let mockConfig: Partial<Config>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockConfig = {
|
||||
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue(undefined),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue(undefined),
|
||||
};
|
||||
|
||||
(
|
||||
UserAccountManager.prototype.getCachedGoogleAccount as Mock
|
||||
).mockReturnValue(undefined);
|
||||
(InstallationManager.prototype.getInstallationId as Mock).mockReturnValue(
|
||||
'mock-install-id',
|
||||
);
|
||||
});
|
||||
|
||||
describe('getCommonMetricAttributes', () => {
|
||||
it('should return interactive and auth_type when defined', () => {
|
||||
mockConfig.getContentGeneratorConfig = vi
|
||||
.fn()
|
||||
.mockReturnValue({ authType: 'oauth-personal' });
|
||||
|
||||
const attributes = getCommonMetricAttributes(mockConfig as Config);
|
||||
|
||||
expect(attributes).toEqual({
|
||||
interactive: true,
|
||||
auth_type: 'oauth-personal',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return only interactive when auth_type is not defined', () => {
|
||||
const attributes = getCommonMetricAttributes(mockConfig as Config);
|
||||
|
||||
expect(attributes).toEqual({
|
||||
interactive: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommonAttributes', () => {
|
||||
it('should include all common attributes', () => {
|
||||
(
|
||||
UserAccountManager.prototype.getCachedGoogleAccount as Mock
|
||||
).mockReturnValue('test@google.com');
|
||||
mockConfig.getExperiments = vi
|
||||
.fn()
|
||||
.mockReturnValue({ experimentIds: [123, 456] });
|
||||
mockConfig.getContentGeneratorConfig = vi
|
||||
.fn()
|
||||
.mockReturnValue({ authType: 'adc' });
|
||||
|
||||
const attributes = getCommonAttributes(mockConfig as Config);
|
||||
|
||||
expect(attributes).toEqual({
|
||||
'session.id': 'mock-session-id',
|
||||
'installation.id': 'mock-install-id',
|
||||
interactive: true,
|
||||
'user.email': 'test@google.com',
|
||||
auth_type: 'adc',
|
||||
'experiments.ids': '123,456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should safely truncate experiments string to not exceed 1000 characters and not cut mid-ID', () => {
|
||||
// Generate a list of experiment IDs that will produce a string > 1000 chars
|
||||
const expIds = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
// e.g., 100000000 -> 9 chars + 1 comma = 10 chars per ID
|
||||
expIds.push(100000000 + i);
|
||||
}
|
||||
mockConfig.getExperiments = vi
|
||||
.fn()
|
||||
.mockReturnValue({ experimentIds: expIds });
|
||||
|
||||
const attributes = getCommonAttributes(mockConfig as Config);
|
||||
const expString = attributes['experiments.ids'] as string;
|
||||
|
||||
expect(expString.length).toBeLessThanOrEqual(1000);
|
||||
|
||||
// Verify the last ID is complete (not cut off) by checking if it's one of our expected IDs
|
||||
const ids = expString.split(',');
|
||||
const lastIdStr = ids[ids.length - 1];
|
||||
const lastIdNumber = parseInt(lastIdStr, 10);
|
||||
|
||||
expect(lastIdNumber).toBeGreaterThanOrEqual(100000000);
|
||||
expect(lastIdNumber).toBeLessThan(100000200);
|
||||
|
||||
// Also ensure no trailing comma
|
||||
expect(expString.endsWith(',')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,19 +12,36 @@ import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const installationManager = new InstallationManager();
|
||||
|
||||
export function getCommonMetricAttributes(config: Config): Attributes {
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
|
||||
return {
|
||||
interactive: config.isInteractive(),
|
||||
...(authType && { auth_type: authType }),
|
||||
};
|
||||
}
|
||||
|
||||
export function getCommonAttributes(config: Config): Attributes {
|
||||
const email = userAccountManager.getCachedGoogleAccount();
|
||||
const experiments = config.getExperiments();
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
|
||||
let experimentsIdsStr = '';
|
||||
if (experiments && experiments.experimentIds.length > 0) {
|
||||
experimentsIdsStr = experiments.experimentIds.join(',');
|
||||
if (experimentsIdsStr.length > 1000) {
|
||||
experimentsIdsStr = experimentsIdsStr.substring(0, 1000);
|
||||
const lastCommaIndex = experimentsIdsStr.lastIndexOf(',');
|
||||
if (lastCommaIndex > 0) {
|
||||
experimentsIdsStr = experimentsIdsStr.substring(0, lastCommaIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...getCommonMetricAttributes(config),
|
||||
'session.id': config.getSessionId(),
|
||||
'installation.id': installationManager.getInstallationId(),
|
||||
interactive: config.isInteractive(),
|
||||
...(email && { 'user.email': email }),
|
||||
...(authType && { auth_type: authType }),
|
||||
...(experiments &&
|
||||
experiments.experimentIds.length > 0 && {
|
||||
'experiments.ids': experiments.experimentIds,
|
||||
}),
|
||||
...(experimentsIdsStr && { 'experiments.ids': experimentsIdsStr }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,26 +52,95 @@ describe('truncateForTelemetry', () => {
|
||||
});
|
||||
|
||||
it('should correctly truncate strings with multi-byte unicode characters (emojis)', () => {
|
||||
// 5 emojis, each is multiple bytes in UTF-16
|
||||
// 5 emojis, each is a surrogate pair (2 code units)
|
||||
const emojis = '👋🌍🚀🔥🎉';
|
||||
|
||||
// Truncating to length 5 (which is 2.5 emojis in UTF-16 length terms)
|
||||
// truncateString will stop after the full grapheme clusters that fit within 5
|
||||
const result = truncateForTelemetry(emojis, 5);
|
||||
// Truncating to 2 code units
|
||||
const result = truncateForTelemetry(emojis, 2);
|
||||
|
||||
expect(result).toBe('👋🌍...[TRUNCATED: original length 10]');
|
||||
expect(result).toBe('👋...[TRUNCATED: original length 10]');
|
||||
});
|
||||
|
||||
it('should stringify and truncate objects if exceeding maxLength', () => {
|
||||
it('should stringify and structurally truncate objects if exceeding limits', () => {
|
||||
const obj = { message: 'hello world', nested: { a: 1 } };
|
||||
const stringified = JSON.stringify(obj);
|
||||
const result = truncateForTelemetry(obj, 10);
|
||||
expect(result).toBe(
|
||||
stringified.substring(0, 10) +
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
JSON.stringify({
|
||||
message: 'hello worl...[TRUNCATED: original length 11]',
|
||||
nested: { a: 1 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should structurally truncate arrays and depth', () => {
|
||||
const obj = {
|
||||
arr: [1, 2, 3],
|
||||
deep: { level1: { level2: { level3: { a: 1 } } } },
|
||||
};
|
||||
const result = truncateForTelemetry(obj, 100, 2, 3);
|
||||
expect(result).toBe(
|
||||
JSON.stringify({
|
||||
arr: [1, 2, '[TRUNCATED: Array of length 3]'],
|
||||
deep: { level1: { level2: '[TRUNCATED: Max Depth Reached]' } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle objects with a toJSON method', () => {
|
||||
const date = new Date('2026-04-13T00:00:00.000Z');
|
||||
const result = truncateForTelemetry(date);
|
||||
expect(result).toBe('2026-04-13T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle getters via direct property access', () => {
|
||||
const obj = {
|
||||
get myGetter() {
|
||||
return 'getter value';
|
||||
},
|
||||
get errorGetter() {
|
||||
throw new Error('getter error');
|
||||
},
|
||||
};
|
||||
const result = truncateForTelemetry(obj);
|
||||
expect(result).toBe(
|
||||
JSON.stringify({
|
||||
myGetter: 'getter value',
|
||||
errorGetter: '[ERROR: Failed to read property]',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should truncate extremely long keys', () => {
|
||||
const longKey = 'a'.repeat(150);
|
||||
const obj = {
|
||||
[longKey]: 'value',
|
||||
};
|
||||
const result = truncateForTelemetry(obj);
|
||||
const expectedKey = 'a'.repeat(100) + '...[TRUNCATED_KEY]';
|
||||
expect(result).toBe(
|
||||
JSON.stringify({
|
||||
[expectedKey]: 'value',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should enforce a global payload string limit without breaking JSON', () => {
|
||||
const obj = {
|
||||
a: 'x'.repeat(100),
|
||||
b: 'y'.repeat(100),
|
||||
};
|
||||
// Capping global string length to 50
|
||||
const result = truncateForTelemetry(obj, 100, 100, 4, 100, 50) as string;
|
||||
|
||||
// It should replace the entire object with a valid JSON string indicating truncation
|
||||
expect(result).toBe(
|
||||
'"[TRUNCATED: Payload exceeded global limit of 50 characters. Original length: 215]"',
|
||||
);
|
||||
|
||||
// Prove it remains perfectly parseable JSON
|
||||
expect(() => JSON.parse(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should stringify objects unchanged if within maxLength', () => {
|
||||
const obj = { a: 1 };
|
||||
expect(truncateForTelemetry(obj, 100)).toBe(JSON.stringify(obj));
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { truncateString } from '../utils/textUtils.js';
|
||||
import {
|
||||
GEN_AI_AGENT_DESCRIPTION,
|
||||
GEN_AI_AGENT_NAME,
|
||||
@@ -45,6 +44,17 @@ export const spanRegistry = new FinalizationRegistry((endSpan: () => void) => {
|
||||
}
|
||||
});
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function hasToJSON(value: unknown): value is { toJSON: () => unknown } {
|
||||
if (!isRecord(value)) return false;
|
||||
if (!('toJSON' in value)) return false;
|
||||
const toJSONFn = value['toJSON'];
|
||||
return typeof toJSONFn === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a value for inclusion in telemetry attributes.
|
||||
*
|
||||
@@ -54,27 +64,98 @@ export const spanRegistry = new FinalizationRegistry((endSpan: () => void) => {
|
||||
*/
|
||||
export function truncateForTelemetry(
|
||||
value: unknown,
|
||||
maxLength = 10000,
|
||||
maxStringLength = 10000,
|
||||
maxArrayLength = 100,
|
||||
maxDepth = 4,
|
||||
maxObjectKeys = 100,
|
||||
maxGlobalStringLength = 50000,
|
||||
): AttributeValue | undefined {
|
||||
if (typeof value === 'string') {
|
||||
return truncateString(
|
||||
value,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${value.length}]`,
|
||||
) as AttributeValue;
|
||||
const truncateObj = (v: unknown, depth: number): unknown => {
|
||||
if (typeof v === 'string') {
|
||||
if (v.length > maxStringLength) {
|
||||
let truncatedStr = v.slice(0, maxStringLength);
|
||||
if (truncatedStr.length > 0 && /[\uD800-\uDBFF]$/.test(truncatedStr)) {
|
||||
truncatedStr = truncatedStr.slice(0, -1);
|
||||
}
|
||||
return truncatedStr + `...[TRUNCATED: original length ${v.length}]`;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if (
|
||||
typeof v === 'number' ||
|
||||
typeof v === 'boolean' ||
|
||||
v === null ||
|
||||
v === undefined
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
if (hasToJSON(v)) {
|
||||
try {
|
||||
return truncateObj(v.toJSON(), depth);
|
||||
} catch {
|
||||
// Ignore and fall back to manual structural iteration
|
||||
}
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
if (depth >= maxDepth) {
|
||||
return `[TRUNCATED: Max Depth Reached]`;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
if (v.length > maxArrayLength) {
|
||||
const truncatedArray = v
|
||||
.slice(0, maxArrayLength)
|
||||
.map((item) => truncateObj(item, depth + 1));
|
||||
truncatedArray.push(`[TRUNCATED: Array of length ${v.length}]`);
|
||||
return truncatedArray;
|
||||
}
|
||||
return v.map((item) => truncateObj(item, depth + 1));
|
||||
}
|
||||
|
||||
const newObj: Record<string, unknown> = {};
|
||||
let numKeys = 0;
|
||||
const recordV = isRecord(v) ? v : {};
|
||||
for (const key in recordV) {
|
||||
if (!Object.prototype.hasOwnProperty.call(recordV, key)) continue;
|
||||
if (numKeys >= maxObjectKeys) {
|
||||
newObj['__truncated'] =
|
||||
`[TRUNCATED: Object with >${maxObjectKeys} keys]`;
|
||||
break;
|
||||
}
|
||||
const truncatedKey =
|
||||
key.length > 100 ? key.slice(0, 100) + '...[TRUNCATED_KEY]' : key;
|
||||
try {
|
||||
const val = recordV[key];
|
||||
newObj[truncatedKey] = truncateObj(val, depth + 1);
|
||||
} catch {
|
||||
newObj[truncatedKey] = '[ERROR: Failed to read property]';
|
||||
}
|
||||
numKeys++;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const truncated = truncateObj(value, 0);
|
||||
|
||||
if (
|
||||
typeof truncated === 'string' ||
|
||||
typeof truncated === 'number' ||
|
||||
typeof truncated === 'boolean'
|
||||
) {
|
||||
return truncated as AttributeValue;
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const stringified = safeJsonStringify(value);
|
||||
return truncateString(
|
||||
stringified,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
) as AttributeValue;
|
||||
if (truncated === null || truncated === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value as AttributeValue;
|
||||
|
||||
const stringified = safeJsonStringify(truncated);
|
||||
|
||||
if (stringified.length > maxGlobalStringLength) {
|
||||
return `"[TRUNCATED: Payload exceeded global limit of ${maxGlobalStringLength} characters. Original length: ${stringified.length}]"`;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return stringified as AttributeValue;
|
||||
}
|
||||
|
||||
function isAsyncIterable<T>(value: T): value is T & AsyncIterable<unknown> {
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { AgentTerminateMode } from '../agents/types.js';
|
||||
import { getCommonAttributes } from './telemetryAttributes.js';
|
||||
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { truncateForTelemetry } from './trace.js';
|
||||
import {
|
||||
toInputMessages,
|
||||
toOutputMessages,
|
||||
@@ -451,7 +452,7 @@ export class ApiRequestEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
attributes['gen_ai.input.messages'] = truncateForTelemetry(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
}
|
||||
@@ -548,7 +549,7 @@ export class ApiErrorEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
attributes['gen_ai.input.messages'] = truncateForTelemetry(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
}
|
||||
@@ -618,7 +619,7 @@ function toGenerateContentConfigAttributes(
|
||||
'gen_ai.request.max_tokens': config.maxOutputTokens,
|
||||
'gen_ai.output.type': toOutputType(config.responseMimeType),
|
||||
'gen_ai.request.stop_sequences': config.stopSequences,
|
||||
'gen_ai.system_instructions': JSON.stringify(
|
||||
'gen_ai.system_instructions': truncateForTelemetry(
|
||||
toSystemInstruction(config.systemInstruction),
|
||||
),
|
||||
};
|
||||
@@ -716,7 +717,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
'gen_ai.response.finish_reasons': this.finish_reasons,
|
||||
...(shouldIncludePayloads(config)
|
||||
? {
|
||||
'gen_ai.output.messages': JSON.stringify(
|
||||
'gen_ai.output.messages': truncateForTelemetry(
|
||||
toOutputMessages(this.response.candidates),
|
||||
),
|
||||
}
|
||||
@@ -731,7 +732,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
attributes['gen_ai.input.messages'] = truncateForTelemetry(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -457,6 +457,7 @@ export class TestRig {
|
||||
// Nightly releases sometimes becomes out of sync with local code and
|
||||
// triggers auto-update, which causes tests to fail.
|
||||
enableAutoUpdate: false,
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
telemetry: {
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user