feat(config): Support telemetry configuration via environment variables (#9113)

This commit is contained in:
Jerop Kipruto
2025-09-23 03:23:06 +09:00
committed by GitHub
parent 10392ad344
commit c0c7ad10ca
7 changed files with 479 additions and 29 deletions

View File

@@ -2311,3 +2311,146 @@ describe('parseArguments with positional prompt', () => {
expect(argv.prompt).toBe('test prompt');
});
});
describe('Telemetry configuration via environment variables', () => {
it('should prioritize GEMINI_TELEMETRY_ENABLED over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_ENABLED', 'true');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { enabled: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryEnabled()).toBe(true);
});
it('should prioritize GEMINI_TELEMETRY_TARGET over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_TARGET', 'gcp');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { target: 'local' } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('gcp');
});
it('should throw when GEMINI_TELEMETRY_TARGET is invalid', async () => {
vi.stubEnv('GEMINI_TELEMETRY_TARGET', 'bogus');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { target: 'gcp' } };
await expect(
loadCliConfig(settings, [], 'test-session', argv),
).rejects.toThrow(
/Invalid telemetry configuration: .*Invalid telemetry target/i,
);
vi.unstubAllEnvs();
});
it('should prioritize GEMINI_TELEMETRY_OTLP_ENDPOINT over settings and default env var', async () => {
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://default.env.com');
vi.stubEnv('GEMINI_TELEMETRY_OTLP_ENDPOINT', 'http://gemini.env.com');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
telemetry: { otlpEndpoint: 'http://settings.com' },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryOtlpEndpoint()).toBe('http://gemini.env.com');
});
it('should prioritize GEMINI_TELEMETRY_OTLP_PROTOCOL over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_OTLP_PROTOCOL', 'http');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { otlpProtocol: 'grpc' } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryOtlpProtocol()).toBe('http');
});
it('should prioritize GEMINI_TELEMETRY_LOG_PROMPTS over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', 'false');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { logPrompts: true } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
it('should prioritize GEMINI_TELEMETRY_OUTFILE over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_OUTFILE', '/gemini/env/telemetry.log');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
telemetry: { outfile: '/settings/telemetry.log' },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryOutfile()).toBe('/gemini/env/telemetry.log');
});
it('should prioritize GEMINI_TELEMETRY_USE_COLLECTOR over settings', async () => {
vi.stubEnv('GEMINI_TELEMETRY_USE_COLLECTOR', 'true');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { useCollector: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryUseCollector()).toBe(true);
});
it('should use settings value when GEMINI_TELEMETRY_ENABLED is not set', async () => {
vi.stubEnv('GEMINI_TELEMETRY_ENABLED', undefined);
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { enabled: true } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryEnabled()).toBe(true);
});
it('should use settings value when GEMINI_TELEMETRY_TARGET is not set', async () => {
vi.stubEnv('GEMINI_TELEMETRY_TARGET', undefined);
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { telemetry: { target: 'local' } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('local');
});
it("should treat GEMINI_TELEMETRY_ENABLED='1' as true", async () => {
vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '1');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, [], 'test-session', argv);
expect(config.getTelemetryEnabled()).toBe(true);
});
it("should treat GEMINI_TELEMETRY_ENABLED='0' as false", async () => {
vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '0');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{ telemetry: { enabled: true } },
[],
'test-session',
argv,
);
expect(config.getTelemetryEnabled()).toBe(false);
});
it("should treat GEMINI_TELEMETRY_LOG_PROMPTS='1' as true", async () => {
vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', '1');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, [], 'test-session', argv);
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
});
it("should treat GEMINI_TELEMETRY_LOG_PROMPTS='false' as false", async () => {
vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', 'false');
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{ telemetry: { logPrompts: true } },
[],
'test-session',
argv,
);
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
});

View File

@@ -12,7 +12,6 @@ import { hideBin } from 'yargs/helpers';
import process from 'node:process';
import { mcpCommand } from '../commands/mcp.js';
import type {
TelemetryTarget,
FileFilteringOptions,
MCPServerConfig,
OutputFormat,
@@ -32,6 +31,8 @@ import {
ShellTool,
EditTool,
WriteFileTool,
resolveTelemetrySettings,
FatalConfigError,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
@@ -503,6 +504,22 @@ export async function loadCliConfig(
approvalMode = ApprovalMode.DEFAULT;
}
let telemetrySettings;
try {
telemetrySettings = await resolveTelemetrySettings({
argv,
env: process.env as unknown as Record<string, string | undefined>,
settings: settings.telemetry,
});
} catch (err) {
if (err instanceof FatalConfigError) {
throw new FatalConfigError(
`Invalid telemetry configuration: ${err.message}.`,
);
}
throw err;
}
const policyEngineConfig = createPolicyEngineConfig(settings, approvalMode);
// Fix: If promptWords are provided, always use non-interactive mode
@@ -608,23 +625,7 @@ export async function loadCliConfig(
...settings.ui?.accessibility,
screenReader,
},
telemetry: {
enabled: argv.telemetry ?? settings.telemetry?.enabled,
target: (argv.telemetryTarget ??
settings.telemetry?.target) as TelemetryTarget,
otlpEndpoint:
argv.telemetryOtlpEndpoint ??
process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] ??
settings.telemetry?.otlpEndpoint,
otlpProtocol: (['grpc', 'http'] as const).find(
(p) =>
p ===
(argv.telemetryOtlpProtocol ?? settings.telemetry?.otlpProtocol),
),
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
useCollector: settings.telemetry?.useCollector,
},
telemetry: telemetrySettings,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
fileFiltering: settings.context?.fileFiltering,
checkpointing: