From a889c15e389fc747299ecc2784862ea888562ada Mon Sep 17 00:00:00 2001 From: Riddhi Dutta Date: Fri, 24 Oct 2025 19:15:36 +0530 Subject: [PATCH 1/4] Adding Parameterised tests (#11930) --- .../core/src/utils/ignorePatterns.test.ts | 63 ++++----- packages/core/src/utils/paths.test.ts | 123 +++++------------- 2 files changed, 59 insertions(+), 127 deletions(-) diff --git a/packages/core/src/utils/ignorePatterns.test.ts b/packages/core/src/utils/ignorePatterns.test.ts index 646c4b6bb7..58f504f982 100644 --- a/packages/core/src/utils/ignorePatterns.test.ts +++ b/packages/core/src/utils/ignorePatterns.test.ts @@ -201,23 +201,14 @@ describe('FileExclusions', () => { }); describe('BINARY_EXTENSIONS', () => { - it('should include common binary file extensions', () => { - expect(BINARY_EXTENSIONS).toContain('.exe'); - expect(BINARY_EXTENSIONS).toContain('.dll'); - expect(BINARY_EXTENSIONS).toContain('.jar'); - expect(BINARY_EXTENSIONS).toContain('.zip'); - }); - - it('should include additional binary extensions', () => { - expect(BINARY_EXTENSIONS).toContain('.dat'); - expect(BINARY_EXTENSIONS).toContain('.obj'); - expect(BINARY_EXTENSIONS).toContain('.wasm'); - }); - - it('should include media file extensions', () => { - expect(BINARY_EXTENSIONS).toContain('.pdf'); - expect(BINARY_EXTENSIONS).toContain('.png'); - expect(BINARY_EXTENSIONS).toContain('.jpg'); + it.each([ + ['common binary file extensions', ['.exe', '.dll', '.jar', '.zip']], + ['additional binary extensions', ['.dat', '.obj', '.wasm']], + ['media file extensions', ['.pdf', '.png', '.jpg']], + ])('should include %s', (_, extensions) => { + extensions.forEach((ext) => { + expect(BINARY_EXTENSIONS).toContain(ext); + }); }); it('should be sorted', () => { @@ -235,11 +226,25 @@ describe('BINARY_EXTENSIONS', () => { }); describe('extractExtensionsFromPatterns', () => { - it('should extract simple extensions', () => { - const patterns = ['**/*.exe', '**/*.jar', '**/*.zip']; + it.each([ + [ + 'simple extensions', + ['**/*.exe', '**/*.jar', '**/*.zip'], + ['.exe', '.jar', '.zip'], + ], + [ + 'compound extensions', + ['**/*.tar.gz', '**/*.min.js', '**/*.d.ts'], + ['.gz', '.js', '.ts'], + ], + [ + 'dotfiles', + ['**/*.gitignore', '**/*.profile', '**/*.bashrc'], + ['.bashrc', '.gitignore', '.profile'], + ], + ])('should extract %s', (_, patterns, expected) => { const result = extractExtensionsFromPatterns(patterns); - - expect(result).toEqual(['.exe', '.jar', '.zip']); + expect(result).toEqual(expected); }); it('should handle brace expansion patterns', () => { @@ -293,22 +298,6 @@ describe('extractExtensionsFromPatterns', () => { expect(result).toEqual(['.css', '.html', '.js', '.jsx', '.ts', '.tsx']); }); - it('should handle compound extensions correctly using path.extname', () => { - const patterns = ['**/*.tar.gz', '**/*.min.js', '**/*.d.ts']; - const result = extractExtensionsFromPatterns(patterns); - - // Should extract the final extension part only - expect(result).toEqual(['.gz', '.js', '.ts']); - }); - - it('should handle dotfiles correctly', () => { - const patterns = ['**/*.gitignore', '**/*.profile', '**/*.bashrc']; - const result = extractExtensionsFromPatterns(patterns); - - // Dotfiles should be extracted properly - expect(result).toEqual(['.bashrc', '.gitignore', '.profile']); - }); - it('should handle edge cases with path.extname', () => { const patterns = ['**/*.hidden.', '**/*.config.json']; const result = extractExtensionsFromPatterns(patterns); diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 0e96467269..602f977a0c 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -8,76 +8,31 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { escapePath, unescapePath, isSubpath } from './paths.js'; describe('escapePath', () => { - it('should escape spaces', () => { - expect(escapePath('my file.txt')).toBe('my\\ file.txt'); - }); - - it('should escape tabs', () => { - expect(escapePath('file\twith\ttabs.txt')).toBe('file\\\twith\\\ttabs.txt'); - }); - - it('should escape parentheses', () => { - expect(escapePath('file(1).txt')).toBe('file\\(1\\).txt'); - }); - - it('should escape square brackets', () => { - expect(escapePath('file[backup].txt')).toBe('file\\[backup\\].txt'); - }); - - it('should escape curly braces', () => { - expect(escapePath('file{temp}.txt')).toBe('file\\{temp\\}.txt'); - }); - - it('should escape semicolons', () => { - expect(escapePath('file;name.txt')).toBe('file\\;name.txt'); - }); - - it('should escape ampersands', () => { - expect(escapePath('file&name.txt')).toBe('file\\&name.txt'); - }); - - it('should escape pipes', () => { - expect(escapePath('file|name.txt')).toBe('file\\|name.txt'); - }); - - it('should escape asterisks', () => { - expect(escapePath('file*.txt')).toBe('file\\*.txt'); - }); - - it('should escape question marks', () => { - expect(escapePath('file?.txt')).toBe('file\\?.txt'); - }); - - it('should escape dollar signs', () => { - expect(escapePath('file$name.txt')).toBe('file\\$name.txt'); - }); - - it('should escape backticks', () => { - expect(escapePath('file`name.txt')).toBe('file\\`name.txt'); - }); - - it('should escape single quotes', () => { - expect(escapePath("file'name.txt")).toBe("file\\'name.txt"); - }); - - it('should escape double quotes', () => { - expect(escapePath('file"name.txt')).toBe('file\\"name.txt'); - }); - - it('should escape hash symbols', () => { - expect(escapePath('file#name.txt')).toBe('file\\#name.txt'); - }); - - it('should escape exclamation marks', () => { - expect(escapePath('file!name.txt')).toBe('file\\!name.txt'); - }); - - it('should escape tildes', () => { - expect(escapePath('file~name.txt')).toBe('file\\~name.txt'); - }); - - it('should escape less than and greater than signs', () => { - expect(escapePath('file.txt')).toBe('file\\.txt'); + it.each([ + ['spaces', 'my file.txt', 'my\\ file.txt'], + ['tabs', 'file\twith\ttabs.txt', 'file\\\twith\\\ttabs.txt'], + ['parentheses', 'file(1).txt', 'file\\(1\\).txt'], + ['square brackets', 'file[backup].txt', 'file\\[backup\\].txt'], + ['curly braces', 'file{temp}.txt', 'file\\{temp\\}.txt'], + ['semicolons', 'file;name.txt', 'file\\;name.txt'], + ['ampersands', 'file&name.txt', 'file\\&name.txt'], + ['pipes', 'file|name.txt', 'file\\|name.txt'], + ['asterisks', 'file*.txt', 'file\\*.txt'], + ['question marks', 'file?.txt', 'file\\?.txt'], + ['dollar signs', 'file$name.txt', 'file\\$name.txt'], + ['backticks', 'file`name.txt', 'file\\`name.txt'], + ['single quotes', "file'name.txt", "file\\'name.txt"], + ['double quotes', 'file"name.txt', 'file\\"name.txt'], + ['hash symbols', 'file#name.txt', 'file\\#name.txt'], + ['exclamation marks', 'file!name.txt', 'file\\!name.txt'], + ['tildes', 'file~name.txt', 'file\\~name.txt'], + [ + 'less than and greater than signs', + 'file.txt', + 'file\\.txt', + ], + ])('should escape %s', (_, input, expected) => { + expect(escapePath(input)).toBe(expected); }); it('should handle multiple special characters', () => { @@ -135,26 +90,14 @@ describe('escapePath', () => { }); describe('unescapePath', () => { - it('should unescape spaces', () => { - expect(unescapePath('my\\ file.txt')).toBe('my file.txt'); - }); - - it('should unescape tabs', () => { - expect(unescapePath('file\\\twith\\\ttabs.txt')).toBe( - 'file\twith\ttabs.txt', - ); - }); - - it('should unescape parentheses', () => { - expect(unescapePath('file\\(1\\).txt')).toBe('file(1).txt'); - }); - - it('should unescape square brackets', () => { - expect(unescapePath('file\\[backup\\].txt')).toBe('file[backup].txt'); - }); - - it('should unescape curly braces', () => { - expect(unescapePath('file\\{temp\\}.txt')).toBe('file{temp}.txt'); + it.each([ + ['spaces', 'my\\ file.txt', 'my file.txt'], + ['tabs', 'file\\\twith\\\ttabs.txt', 'file\twith\ttabs.txt'], + ['parentheses', 'file\\(1\\).txt', 'file(1).txt'], + ['square brackets', 'file\\[backup\\].txt', 'file[backup].txt'], + ['curly braces', 'file\\{temp\\}.txt', 'file{temp}.txt'], + ])('should unescape %s', (_, input, expected) => { + expect(unescapePath(input)).toBe(expected); }); it('should unescape multiple special characters', () => { From c079084ca454ef3e83261cfeba1b8719d6163931 Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:26:42 +0200 Subject: [PATCH 2/4] chore(core): add token caching in google auth provider (#11946) --- .../core/src/mcp/google-auth-provider.test.ts | 65 ++++++++++++++++--- packages/core/src/mcp/google-auth-provider.ts | 27 +++++++- packages/core/src/mcp/oauth-utils.test.ts | 37 +++++++++++ packages/core/src/mcp/oauth-utils.ts | 24 +++++++ .../core/src/mcp/sa-impersonation-provider.ts | 29 +-------- 5 files changed, 144 insertions(+), 38 deletions(-) diff --git a/packages/core/src/mcp/google-auth-provider.test.ts b/packages/core/src/mcp/google-auth-provider.test.ts index b568fa2ca7..efe959ff3c 100644 --- a/packages/core/src/mcp/google-auth-provider.test.ts +++ b/packages/core/src/mcp/google-auth-provider.test.ts @@ -82,31 +82,76 @@ describe('GoogleCredentialProvider', () => { describe('with provider instance', () => { let provider: GoogleCredentialProvider; + let mockGetAccessToken: Mock; + let mockClient: { + getAccessToken: Mock; + credentials?: { expiry_date: number | null }; + }; beforeEach(() => { + // clear and reset mock client before each test + mockGetAccessToken = vi.fn(); + mockClient = { + getAccessToken: mockGetAccessToken, + }; + (GoogleAuth.prototype.getClient as Mock).mockResolvedValue(mockClient); provider = new GoogleCredentialProvider(validConfig); - vi.clearAllMocks(); }); it('should return credentials', async () => { - const mockClient = { - getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }), - }; - (GoogleAuth.prototype.getClient as Mock).mockResolvedValue(mockClient); + mockGetAccessToken.mockResolvedValue({ token: 'test-token' }); const credentials = await provider.tokens(); - expect(credentials?.access_token).toBe('test-token'); }); it('should return undefined if access token is not available', async () => { - const mockClient = { - getAccessToken: vi.fn().mockResolvedValue({ token: null }), - }; - (GoogleAuth.prototype.getClient as Mock).mockResolvedValue(mockClient); + mockGetAccessToken.mockResolvedValue({ token: null }); const credentials = await provider.tokens(); expect(credentials).toBeUndefined(); }); + + it('should return a cached token if it is not expired', async () => { + vi.useFakeTimers(); + mockClient.credentials = { expiry_date: Date.now() + 3600 * 1000 }; // 1 hour + mockGetAccessToken.mockResolvedValue({ token: 'test-token' }); + + // first call + const firstTokens = await provider.tokens(); + expect(firstTokens?.access_token).toBe('test-token'); + expect(mockGetAccessToken).toHaveBeenCalledTimes(1); + + // second call + vi.advanceTimersByTime(1800 * 1000); // Advance time by 30 minutes + const secondTokens = await provider.tokens(); + expect(secondTokens).toBe(firstTokens); + expect(mockGetAccessToken).toHaveBeenCalledTimes(1); // Should not be called again + + vi.useRealTimers(); + }); + + it('should fetch a new token if the cached token is expired', async () => { + vi.useFakeTimers(); + + // first call + mockClient.credentials = { expiry_date: Date.now() + 1000 }; // Expires in 1 second + mockGetAccessToken.mockResolvedValue({ token: 'expired-token' }); + + const firstTokens = await provider.tokens(); + expect(firstTokens?.access_token).toBe('expired-token'); + expect(mockGetAccessToken).toHaveBeenCalledTimes(1); + + // second call + vi.advanceTimersByTime(1001); // Advance time past expiry + mockClient.credentials = { expiry_date: Date.now() + 3600 * 1000 }; // New expiry + mockGetAccessToken.mockResolvedValue({ token: 'new-token' }); + + const newTokens = await provider.tokens(); + expect(newTokens?.access_token).toBe('new-token'); + expect(mockGetAccessToken).toHaveBeenCalledTimes(2); // new fetch + + vi.useRealTimers(); + }); }); }); diff --git a/packages/core/src/mcp/google-auth-provider.ts b/packages/core/src/mcp/google-auth-provider.ts index d761156225..d152b4d256 100644 --- a/packages/core/src/mcp/google-auth-provider.ts +++ b/packages/core/src/mcp/google-auth-provider.ts @@ -13,11 +13,14 @@ import type { } from '@modelcontextprotocol/sdk/shared/auth.js'; import { GoogleAuth } from 'google-auth-library'; import type { MCPServerConfig } from '../config/config.js'; +import { FIVE_MIN_BUFFER_MS } from './oauth-utils.js'; const ALLOWED_HOSTS = [/^.+\.googleapis\.com$/, /^(.*\.)?luci\.app$/]; export class GoogleCredentialProvider implements OAuthClientProvider { private readonly auth: GoogleAuth; + private cachedToken?: OAuthTokens; + private tokenExpiryTime?: number; // Properties required by OAuthClientProvider, with no-op values readonly redirectUrl = ''; @@ -65,6 +68,19 @@ export class GoogleCredentialProvider implements OAuthClientProvider { } async tokens(): Promise { + // check for a valid, non-expired cached token. + if ( + this.cachedToken && + this.tokenExpiryTime && + Date.now() < this.tokenExpiryTime - FIVE_MIN_BUFFER_MS + ) { + return this.cachedToken; + } + + // Clear invalid/expired cache. + this.cachedToken = undefined; + this.tokenExpiryTime = undefined; + const client = await this.auth.getClient(); const accessTokenResponse = await client.getAccessToken(); @@ -73,11 +89,18 @@ export class GoogleCredentialProvider implements OAuthClientProvider { return undefined; } - const tokens: OAuthTokens = { + const newToken: OAuthTokens = { access_token: accessTokenResponse.token, token_type: 'Bearer', }; - return tokens; + + const expiryTime = client.credentials?.expiry_date; + if (expiryTime) { + this.tokenExpiryTime = expiryTime; + this.cachedToken = newToken; + } + + return newToken; } saveTokens(_tokens: OAuthTokens): void { diff --git a/packages/core/src/mcp/oauth-utils.test.ts b/packages/core/src/mcp/oauth-utils.test.ts index 93aa507e21..bec8ef9f4b 100644 --- a/packages/core/src/mcp/oauth-utils.test.ts +++ b/packages/core/src/mcp/oauth-utils.test.ts @@ -325,4 +325,41 @@ describe('OAuthUtils', () => { expect(() => OAuthUtils.buildResourceParameter('not-a-url')).toThrow(); }); }); + + describe('parseTokenExpiry', () => { + it('should return the expiry time in milliseconds for a valid token', () => { + // Corresponds to a date of 2100-01-01T00:00:00Z + const expiry = 4102444800; + const payload = { exp: expiry }; + const token = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`; + const result = OAuthUtils.parseTokenExpiry(token); + expect(result).toBe(expiry * 1000); + }); + + it('should return undefined for a token without an expiry time', () => { + const payload = { iat: 1678886400 }; + const token = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`; + const result = OAuthUtils.parseTokenExpiry(token); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a token with an invalid expiry time', () => { + const payload = { exp: 'not-a-number' }; + const token = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`; + const result = OAuthUtils.parseTokenExpiry(token); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a malformed token', () => { + const token = 'not-a-valid-token'; + const result = OAuthUtils.parseTokenExpiry(token); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a token with invalid JSON in payload', () => { + const token = `header.${Buffer.from('{ not valid json').toString('base64')}.signature`; + const result = OAuthUtils.parseTokenExpiry(token); + expect(result).toBeUndefined(); + }); + }); }); diff --git a/packages/core/src/mcp/oauth-utils.ts b/packages/core/src/mcp/oauth-utils.ts index cf6bfc289d..0f4bd0b24a 100644 --- a/packages/core/src/mcp/oauth-utils.ts +++ b/packages/core/src/mcp/oauth-utils.ts @@ -38,6 +38,8 @@ export interface OAuthProtectedResourceMetadata { resource_encryption_enc_values_supported?: string[]; } +export const FIVE_MIN_BUFFER_MS = 5 * 60 * 1000; + /** * Utility class for common OAuth operations. */ @@ -362,4 +364,26 @@ export class OAuthUtils { const url = new URL(endpointUrl); return `${url.protocol}//${url.host}${url.pathname}`; } + + /** + * Parses a JWT string to extract its expiry time. + * @param idToken The JWT ID token. + * @returns The expiry time in **milliseconds**, or undefined if parsing fails. + */ + static parseTokenExpiry(idToken: string): number | undefined { + try { + const payload = JSON.parse( + Buffer.from(idToken.split('.')[1], 'base64').toString(), + ); + + if (payload && typeof payload.exp === 'number') { + return payload.exp * 1000; // Convert seconds to milliseconds + } + } catch (e) { + console.error('Failed to parse ID token for expiry time with error:', e); + } + + // Return undefined if try block fails or 'exp' is missing/invalid + return undefined; + } } diff --git a/packages/core/src/mcp/sa-impersonation-provider.ts b/packages/core/src/mcp/sa-impersonation-provider.ts index e3336693d2..b9335e2622 100644 --- a/packages/core/src/mcp/sa-impersonation-provider.ts +++ b/packages/core/src/mcp/sa-impersonation-provider.ts @@ -11,11 +11,10 @@ import type { OAuthTokens, } from '@modelcontextprotocol/sdk/shared/auth.js'; import { GoogleAuth } from 'google-auth-library'; +import { OAuthUtils, FIVE_MIN_BUFFER_MS } from './oauth-utils.js'; import type { MCPServerConfig } from '../config/config.js'; import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; -const fiveMinBufferMs = 5 * 60 * 1000; - function createIamApiUrl(targetSA: string): string { return `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(targetSA)}:generateIdToken`; } @@ -78,7 +77,7 @@ export class ServiceAccountImpersonationProvider if ( this.cachedToken && this.tokenExpiryTime && - Date.now() < this.tokenExpiryTime - fiveMinBufferMs + Date.now() < this.tokenExpiryTime - FIVE_MIN_BUFFER_MS ) { return this.cachedToken; } @@ -112,7 +111,7 @@ export class ServiceAccountImpersonationProvider return undefined; } - const expiryTime = this.parseTokenExpiry(idToken); + const expiryTime = OAuthUtils.parseTokenExpiry(idToken); // Note: We are placing the OIDC ID Token into the `access_token` field. // This is because the CLI uses this field to construct the // `Authorization: Bearer ` header, which is the correct way to @@ -146,26 +145,4 @@ export class ServiceAccountImpersonationProvider // No-op return ''; } - - /** - * Parses a JWT string to extract its expiry time. - * @param idToken The JWT ID token. - * @returns The expiry time in **milliseconds**, or undefined if parsing fails. - */ - private parseTokenExpiry(idToken: string): number | undefined { - try { - const payload = JSON.parse( - Buffer.from(idToken.split('.')[1], 'base64').toString(), - ); - - if (payload && typeof payload.exp === 'number') { - return payload.exp * 1000; // Convert seconds to milliseconds - } - } catch (e) { - console.error('Failed to parse ID token for expiry time with error:', e); - } - - // Return undefined if try block fails or 'exp' is missing/invalid - return undefined; - } } From d915525c8e31a8ecb4aa3fb0768aec822758c2b4 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Fri, 24 Oct 2025 10:56:21 -0400 Subject: [PATCH 3/4] docs(cli): update telemetry documentation (#11806) --- docs/cli/telemetry.md | 505 +++++++++++++++++++++++++++++++++--------- 1 file changed, 403 insertions(+), 102 deletions(-) diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md index a0c1773fa1..fd59260d2a 100644 --- a/docs/cli/telemetry.md +++ b/docs/cli/telemetry.md @@ -15,8 +15,29 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI. - [Collector-Based Export (Advanced)](#collector-based-export-advanced-1) - [Logs and Metrics](#logs-and-metrics) - [Logs](#logs) + - [Sessions](#sessions) + - [Tools](#tools) + - [Files](#files) + - [API](#api) + - [Model Routing](#model-routing) + - [Chat and Streaming](#chat-and-streaming) + - [Resilience](#resilience) + - [Extensions](#extensions) + - [Agent Runs](#agent-runs) + - [IDE](#ide) + - [UI](#ui) - [Metrics](#metrics) - [Custom](#custom) + - [Sessions](#sessions-1) + - [Tools](#tools-1) + - [API](#api-1) + - [Token Usage](#token-usage) + - [Files](#files-1) + - [Chat and Streaming](#chat-and-streaming-1) + - [Model Routing](#model-routing-1) + - [Agent Runs](#agent-runs-1) + - [UI](#ui-1) + - [Performance](#performance) - [GenAI Semantic Convention](#genai-semantic-convention) ## Key Benefits @@ -210,10 +231,13 @@ attributes on all logs and metrics. ### Logs Logs are timestamped records of specific events. The following events are logged -for Gemini CLI: +for Gemini CLI, grouped by category. -- `gemini_cli.config`: This event occurs once at startup with the CLI's - configuration. +#### Sessions + +Captures startup configuration and user prompt submissions. + +- `gemini_cli.config`: Emitted once at startup with the CLI configuration. - **Attributes**: - `model` (string) - `embedding_model` (string) @@ -222,81 +246,42 @@ for Gemini CLI: - `approval_mode` (string) - `api_key_enabled` (boolean) - `vertex_ai_enabled` (boolean) - - `code_assist_enabled` (boolean) - - `log_prompts_enabled` (boolean) + - `log_user_prompts_enabled` (boolean) - `file_filtering_respect_git_ignore` (boolean) - `debug_mode` (boolean) - `mcp_servers` (string) - - `output_format` (string: "text", "json", or "stream-json") + - `mcp_servers_count` (int) + - `mcp_tools` (string, if applicable) + - `mcp_tools_count` (int, if applicable) + - `output_format` ("text", "json", or "stream-json") -- `gemini_cli.user_prompt`: This event occurs when a user submits a prompt. +- `gemini_cli.user_prompt`: Emitted when a user submits a prompt. - **Attributes**: - `prompt_length` (int) - `prompt_id` (string) - - `prompt` (string, this attribute is excluded if `log_prompts_enabled` is - configured to be `false`) + - `prompt` (string; excluded if `telemetry.logPrompts` is `false`) - `auth_type` (string) -- `gemini_cli.tool_call`: This event occurs for each function call. +#### Tools + +Captures tool executions, output truncation, and Smart Edit behavior. + +- `gemini_cli.tool_call`: Emitted for each tool (function) call. - **Attributes**: - `function_name` - `function_args` - `duration_ms` - `success` (boolean) - - `decision` (string: "accept", "reject", "auto_accept", or "modify", if - applicable) + - `decision` ("accept", "reject", "auto_accept", or "modify", if applicable) - `error` (if applicable) - `error_type` (if applicable) + - `prompt_id` (string) + - `tool_type` ("native" or "mcp") + - `mcp_server_name` (string, if applicable) - `content_length` (int, if applicable) - - `metadata` (if applicable, dictionary of string -> any) + - `metadata` (if applicable) -- `gemini_cli.file_operation`: This event occurs for each file operation. - - **Attributes**: - - `tool_name` (string) - - `operation` (string: "create", "read", "update") - - `lines` (int, if applicable) - - `mimetype` (string, if applicable) - - `extension` (string, if applicable) - - `programming_language` (string, if applicable) - - `diff_stat` (json string, if applicable): A JSON string with the following - members: - - `ai_added_lines` (int) - - `ai_removed_lines` (int) - - `user_added_lines` (int) - - `user_removed_lines` (int) - -- `gemini_cli.api_request`: This event occurs when making a request to Gemini - API. - - **Attributes**: - - `model` - - `request_text` (if applicable) - -- `gemini_cli.api_error`: This event occurs if the API request fails. - - **Attributes**: - - `model` - - `error` - - `error_type` - - `status_code` - - `duration_ms` - - `auth_type` - -- `gemini_cli.api_response`: This event occurs upon receiving a response from - Gemini API. - - **Attributes**: - - `model` - - `status_code` - - `duration_ms` - - `error` (optional) - - `input_token_count` - - `output_token_count` - - `cached_content_token_count` - - `thoughts_token_count` - - `tool_token_count` - - `response_text` (if applicable) - - `auth_type` - -- `gemini_cli.tool_output_truncated`: This event occurs when the output of a - tool call is too large and gets truncated. +- `gemini_cli.tool_output_truncated`: Output of a tool call was truncated. - **Attributes**: - `tool_name` (string) - `original_content_length` (int) @@ -305,32 +290,211 @@ for Gemini CLI: - `lines` (int) - `prompt_id` (string) -- `gemini_cli.malformed_json_response`: This event occurs when a `generateJson` - response from Gemini API cannot be parsed as a json. +- `gemini_cli.smart_edit_strategy`: Smart Edit strategy chosen. - **Attributes**: - - `model` + - `strategy` (string) -- `gemini_cli.flash_fallback`: This event occurs when Gemini CLI switches to - flash as fallback. +- `gemini_cli.smart_edit_correction`: Smart Edit correction result. - **Attributes**: - - `auth_type` + - `correction` ("success" | "failure") -- `gemini_cli.slash_command`: This event occurs when a user executes a slash - command. +#### Files + +Tracks file operations performed by tools. + +- `gemini_cli.file_operation`: Emitted for each file operation. + - **Attributes**: + - `tool_name` (string) + - `operation` ("create" | "read" | "update") + - `lines` (int, optional) + - `mimetype` (string, optional) + - `extension` (string, optional) + - `programming_language` (string, optional) + +#### API + +Captures Gemini API requests, responses, and errors. + +- `gemini_cli.api_request`: Request sent to Gemini API. + - **Attributes**: + - `model` (string) + - `prompt_id` (string) + - `request_text` (string, optional) + +- `gemini_cli.api_response`: Response received from Gemini API. + - **Attributes**: + - `model` (string) + - `status_code` (int|string) + - `duration_ms` (int) + - `input_token_count` (int) + - `output_token_count` (int) + - `cached_content_token_count` (int) + - `thoughts_token_count` (int) + - `tool_token_count` (int) + - `total_token_count` (int) + - `response_text` (string, optional) + - `prompt_id` (string) + - `auth_type` (string) + +- `gemini_cli.api_error`: API request failed. + - **Attributes**: + - `model` (string) + - `error` (string) + - `error_type` (string) + - `status_code` (int|string) + - `duration_ms` (int) + - `prompt_id` (string) + - `auth_type` (string) + +- `gemini_cli.malformed_json_response`: `generateJson` response could not be + parsed. + - **Attributes**: + - `model` (string) + +#### Model Routing + +Tracks model selections via slash commands and router decisions. + +- `gemini_cli.slash_command`: A slash command was executed. - **Attributes**: - `command` (string) - - `subcommand` (string, if applicable) + - `subcommand` (string, optional) + - `status` ("success" | "error") -- `gemini_cli.extension_enable`: This event occurs when an extension is enabled -- `gemini_cli.extension_install`: This event occurs when an extension is - installed +- `gemini_cli.slash_command.model`: Model was selected via slash command. + - **Attributes**: + - `model_name` (string) + +- `gemini_cli.model_routing`: Model router made a decision. + - **Attributes**: + - `decision_model` (string) + - `decision_source` (string) + - `routing_latency_ms` (int) + - `reasoning` (string, optional) + - `failed` (boolean) + - `error_message` (string, optional) + +#### Chat and Streaming + +Observes streaming integrity, compression, and retry behavior. + +- `gemini_cli.chat_compression`: Chat context was compressed. + - **Attributes**: + - `tokens_before` (int) + - `tokens_after` (int) + +- `gemini_cli.chat.invalid_chunk`: Invalid chunk received from a stream. + - **Attributes**: + - `error.message` (string, optional) + +- `gemini_cli.chat.content_retry`: Retry triggered due to a content error. + - **Attributes**: + - `attempt_number` (int) + - `error_type` (string) + - `retry_delay_ms` (int) + - `model` (string) + +- `gemini_cli.chat.content_retry_failure`: All content retries failed. + - **Attributes**: + - `total_attempts` (int) + - `final_error_type` (string) + - `total_duration_ms` (int, optional) + - `model` (string) + +- `gemini_cli.conversation_finished`: Conversation session ended. + - **Attributes**: + - `approvalMode` (string) + - `turnCount` (int) + +- `gemini_cli.next_speaker_check`: Next speaker determination. + - **Attributes**: + - `prompt_id` (string) + - `finish_reason` (string) + - `result` (string) + +#### Resilience + +Records fallback mechanisms for models and network operations. + +- `gemini_cli.flash_fallback`: Switched to a flash model as fallback. + - **Attributes**: + - `auth_type` (string) + +- `gemini_cli.ripgrep_fallback`: Switched to grep as fallback for file search. + - **Attributes**: + - `error` (string, optional) + +- `gemini_cli.web_fetch_fallback_attempt`: Attempted web-fetch fallback. + - **Attributes**: + - `reason` ("private_ip" | "primary_failed") + +#### Extensions + +Tracks extension lifecycle and settings changes. + +- `gemini_cli.extension_install`: An extension was installed. - **Attributes**: - `extension_name` (string) - `extension_version` (string) - `extension_source` (string) - `status` (string) -- `gemini_cli.extension_uninstall`: This event occurs when an extension is - uninstalled + +- `gemini_cli.extension_uninstall`: An extension was uninstalled. + - **Attributes**: + - `extension_name` (string) + - `status` (string) + +- `gemini_cli.extension_enable`: An extension was enabled. + - **Attributes**: + - `extension_name` (string) + - `setting_scope` (string) + +- `gemini_cli.extension_disable`: An extension was disabled. + - **Attributes**: + - `extension_name` (string) + - `setting_scope` (string) + +- `gemini_cli.extension_update`: An extension was updated. + - **Attributes**: + - `extension_name` (string) + - `extension_version` (string) + - `extension_previous_version` (string) + - `extension_source` (string) + - `status` (string) + +#### Agent Runs + +Tracks agent lifecycle and outcomes. + +- `gemini_cli.agent.start`: Agent run started. + - **Attributes**: + - `agent_id` (string) + - `agent_name` (string) + +- `gemini_cli.agent.finish`: Agent run finished. + - **Attributes**: + - `agent_id` (string) + - `agent_name` (string) + - `duration_ms` (int) + - `turn_count` (int) + - `terminate_reason` (string) + +#### IDE + +Captures IDE connectivity and conversation lifecycle events. + +- `gemini_cli.ide_connection`: IDE companion connection. + - **Attributes**: + - `connection_type` (string) + +#### UI + +Tracks terminal rendering issues and related signals. + +- `kitty_sequence_overflow`: Terminal kitty control sequence overflow. + - **Attributes**: + - `sequence_length` (int) + - `truncated_sequence` (string) ### Metrics @@ -338,27 +502,35 @@ Metrics are numerical measurements of behavior over time. #### Custom +##### Sessions + +Counts CLI sessions at startup. + - `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup. +##### Tools + +Measures tool usage and latency. + - `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls. - **Attributes**: - `function_name` - `success` (boolean) - - `decision` (string: "accept", "reject", or "modify", if applicable) - - `tool_type` (string: "mcp", or "native", if applicable) - - `model_added_lines` (Int, optional): Lines added by model in the proposed - changes, if applicable - - `model_removed_lines` (Int, optional): Lines removed by model in the - proposed changes, if applicable - - `user_added_lines` (Int, optional): Lines added by user edits after model - proposal, if applicable - - `user_removed_lines` (Int, optional): Lines removed by user edits after - model proposal, if applicable + - `decision` (string: "accept", "reject", "modify", or "auto_accept", if + applicable) + - `tool_type` (string: "mcp" or "native", if applicable) + - `model_added_lines` (Int, optional) + - `model_removed_lines` (Int, optional) + - `user_added_lines` (Int, optional) + - `user_removed_lines` (Int, optional) - `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency. - **Attributes**: - `function_name` - - `decision` (string: "accept", "reject", or "modify", if applicable) + +##### API + +Tracks API request volume and latency. - `gemini_cli.api.request.count` (Counter, Int): Counts all API requests. - **Attributes**: @@ -370,32 +542,161 @@ Metrics are numerical measurements of behavior over time. latency. - **Attributes**: - `model` - - **Note**: This metric overlaps with `gen_ai.client.operation.duration` below - that's compliant with GenAI Semantic Conventions. + - Note: Overlaps with `gen_ai.client.operation.duration` (GenAI conventions). -- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used. +##### Token Usage + +Tracks tokens used by model and type. + +- `gemini_cli.token.usage` (Counter, Int): Counts tokens used. - **Attributes**: - `model` - - `type` (string: "input", "output", "thought", "cache", or "tool") - - **Note**: This metric overlaps with `gen_ai.client.token.usage` below for - `input`/`output` token types that's compliant with GenAI Semantic - Conventions. + - `type` ("input", "output", "thought", "cache", or "tool") + - Note: Overlaps with `gen_ai.client.token.usage` for `input`/`output`. + +##### Files + +Counts file operations with basic context. - `gemini_cli.file.operation.count` (Counter, Int): Counts file operations. - **Attributes**: - - `operation` (string: "create", "read", "update"): The type of file - operation. - - `lines` (Int, if applicable): Number of lines in the file. - - `mimetype` (string, if applicable): Mimetype of the file. - - `extension` (string, if applicable): File extension of the file. - - `programming_language` (string, if applicable): The programming language - of the file. + - `operation` ("create", "read", "update") + - `lines` (Int, optional) + - `mimetype` (string, optional) + - `extension` (string, optional) + - `programming_language` (string, optional) + +##### Chat and Streaming + +Resilience counters for compression, invalid chunks, and retries. - `gemini_cli.chat_compression` (Counter, Int): Counts chat compression - operations + operations. - **Attributes**: - - `tokens_before`: (Int): Number of tokens in context prior to compression - - `tokens_after`: (Int): Number of tokens in context after compression + - `tokens_before` (Int) + - `tokens_after` (Int) + +- `gemini_cli.chat.invalid_chunk.count` (Counter, Int): Counts invalid chunks + from streams. + +- `gemini_cli.chat.content_retry.count` (Counter, Int): Counts retries due to + content errors. + +- `gemini_cli.chat.content_retry_failure.count` (Counter, Int): Counts requests + where all content retries failed. + +##### Model Routing + +Routing latency/failures and slash-command selections. + +- `gemini_cli.slash_command.model.call_count` (Counter, Int): Counts model + selections via slash command. + - **Attributes**: + - `slash_command.model.model_name` (string) + +- `gemini_cli.model_routing.latency` (Histogram, ms): Model routing decision + latency. + - **Attributes**: + - `routing.decision_model` (string) + - `routing.decision_source` (string) + +- `gemini_cli.model_routing.failure.count` (Counter, Int): Counts model routing + failures. + - **Attributes**: + - `routing.decision_source` (string) + - `routing.error_message` (string) + +##### Agent Runs + +Agent lifecycle metrics: runs, durations, and turns. + +- `gemini_cli.agent.run.count` (Counter, Int): Counts agent runs. + - **Attributes**: + - `agent_name` (string) + - `terminate_reason` (string) + +- `gemini_cli.agent.duration` (Histogram, ms): Agent run durations. + - **Attributes**: + - `agent_name` (string) + +- `gemini_cli.agent.turns` (Histogram, turns): Turns taken per agent run. + - **Attributes**: + - `agent_name` (string) + +##### UI + +UI stability signals such as flicker count. + +- `gemini_cli.ui.flicker.count` (Counter, Int): Counts UI frames that flicker + (render taller than terminal). + +##### Performance + +Optional performance monitoring for startup, CPU/memory, and phase timing. + +- `gemini_cli.startup.duration` (Histogram, ms): CLI startup time by phase. + - **Attributes**: + - `phase` (string) + - `details` (map, optional) + +- `gemini_cli.memory.usage` (Histogram, bytes): Memory usage. + - **Attributes**: + - `memory_type` ("heap_used", "heap_total", "external", "rss") + - `component` (string, optional) + +- `gemini_cli.cpu.usage` (Histogram, percent): CPU usage percentage. + - **Attributes**: + - `component` (string, optional) + +- `gemini_cli.tool.queue.depth` (Histogram, count): Number of tools in the + execution queue. + +- `gemini_cli.tool.execution.breakdown` (Histogram, ms): Tool time by phase. + - **Attributes**: + - `function_name` (string) + - `phase` ("validation", "preparation", "execution", "result_processing") + +- `gemini_cli.api.request.breakdown` (Histogram, ms): API request time by phase. + - **Attributes**: + - `model` (string) + - `phase` ("request_preparation", "network_latency", "response_processing", + "token_processing") + +- `gemini_cli.token.efficiency` (Histogram, ratio): Token efficiency metrics. + - **Attributes**: + - `model` (string) + - `metric` (string) + - `context` (string, optional) + +- `gemini_cli.performance.score` (Histogram, score): Composite performance + score. + - **Attributes**: + - `category` (string) + - `baseline` (number, optional) + +- `gemini_cli.performance.regression` (Counter, Int): Regression detection + events. + - **Attributes**: + - `metric` (string) + - `severity` ("low", "medium", "high") + - `current_value` (number) + - `baseline_value` (number) + +- `gemini_cli.performance.regression.percentage_change` (Histogram, percent): + Percent change from baseline when regression detected. + - **Attributes**: + - `metric` (string) + - `severity` ("low", "medium", "high") + - `current_value` (number) + - `baseline_value` (number) + +- `gemini_cli.performance.baseline.comparison` (Histogram, percent): Comparison + to baseline. + - **Attributes**: + - `metric` (string) + - `category` (string) + - `current_value` (number) + - `baseline_value` (number) #### GenAI Semantic Convention From 978fbcf95ee53c6c2f5e60b5cdfaf0f9043f9224 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Fri, 24 Oct 2025 11:46:52 -0400 Subject: [PATCH 4/4] run bom test on windows (#11828) --- integration-tests/utf-bom-encoding.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/integration-tests/utf-bom-encoding.test.ts b/integration-tests/utf-bom-encoding.test.ts index 4429b70072..a870045453 100644 --- a/integration-tests/utf-bom-encoding.test.ts +++ b/integration-tests/utf-bom-encoding.test.ts @@ -9,9 +9,6 @@ import { writeFileSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { TestRig } from './test-helper.js'; -// Windows skip (Option A: avoid infra scope) -const d = process.platform === 'win32' ? describe.skip : describe; - // BOM encoders const utf8BOM = (s: string) => Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(s, 'utf8')]); @@ -53,7 +50,7 @@ const utf32BE = (s: string) => { let rig: TestRig; let dir: string; -d('BOM end-to-end integration', () => { +describe('BOM end-to-end integraion', () => { beforeAll(async () => { rig = new TestRig(); await rig.setup('bom-integration');