diff --git a/README.md b/README.md index cdc842d46..41074d557 100644 --- a/README.md +++ b/README.md @@ -558,7 +558,7 @@ The Chrome DevTools MCP server supports the following configuration option: - **`--channel`** Specify a different Chrome channel that should be used. The default is the stable channel version. - **Type:** string - - **Choices:** `stable`, `canary`, `beta`, `dev` + - **Choices:** `canary`, `dev`, `beta`, `stable` - **`--logFile`/ `--log-file`** Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports. diff --git a/package.json b/package.json index cae696ec6..82649a46d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "typecheck": "tsc --noEmit", "format": "eslint --cache --fix . && prettier --write --cache .", "check-format": "eslint --cache . && prettier --check --cache .;", - "gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-tool-call-metrics && npm run format", + "gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-tool-call-metrics && npm run update-flag-usage-metrics && npm run format", "docs:generate": "node --experimental-strip-types scripts/generate-docs.ts", "start": "npm run build && node build/src/index.js", "start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js", @@ -28,6 +28,7 @@ "verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts", "update-lighthouse": "node --experimental-strip-types scripts/update-lighthouse.ts", "update-tool-call-metrics": "node --experimental-strip-types scripts/update_tool_call_metrics.ts", + "update-flag-usage-metrics": "node --experimental-strip-types scripts/update_flag_usage_metrics.ts", "verify-npm-package": "node scripts/verify-npm-package.mjs", "eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts", "count-tokens": "node --experimental-strip-types scripts/count_tokens.ts" diff --git a/scripts/update_flag_usage_metrics.ts b/scripts/update_flag_usage_metrics.ts new file mode 100644 index 000000000..8e0f4ffde --- /dev/null +++ b/scripts/update_flag_usage_metrics.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import {cliOptions} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; +import { + getPossibleFlagMetrics, + type FlagMetric, +} from '../build/src/telemetry/flagUtils.js'; +import {applyToExisting} from '../build/src/telemetry/toolMetricsUtils.js'; + +function writeFlagUsageMetrics() { + const outputPath = path.resolve('src/telemetry/flag_usage_metrics.json'); + + const dir = path.dirname(outputPath); + if (!fs.existsSync(dir)) { + throw new Error(`Error: Directory ${dir} does not exist.`); + } + + let existingMetrics: FlagMetric[] = []; + if (fs.existsSync(outputPath)) { + try { + existingMetrics = JSON.parse( + fs.readFileSync(outputPath, 'utf8'), + ) as FlagMetric[]; + } catch { + console.warn( + `Warning: Failed to parse existing metrics from ${outputPath}. Starting fresh.`, + ); + } + } + + const newMetrics = getPossibleFlagMetrics(cliOptions); + const mergedMetrics = applyToExisting( + existingMetrics, + newMetrics, + ); + + fs.writeFileSync(outputPath, JSON.stringify(mergedMetrics, null, 2) + '\n'); + + console.log( + `Successfully wrote ${mergedMetrics.length} flag usage metrics to ${outputPath}`, + ); +} + +writeFlagUsageMetrics(); diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 9feaea84d..804410c05 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -113,7 +113,7 @@ export const cliOptions = { type: 'string', description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.', - choices: ['stable', 'canary', 'beta', 'dev'] as const, + choices: ['canary', 'dev', 'beta', 'stable'] as const, conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'], }, logFile: { diff --git a/src/telemetry/flagUtils.ts b/src/telemetry/flagUtils.ts index a64f26a35..fdaa4ed96 100644 --- a/src/telemetry/flagUtils.ts +++ b/src/telemetry/flagUtils.ts @@ -11,6 +11,15 @@ import type {FlagUsage} from './types.js'; type CliOptions = typeof cliOptions; +/** + * For enums, log the value as uppercase. + * We're going to have an enum for such flags with choices represented + * as an `enum` where the keys of the enum will map to the uppercase `choice`. + */ +function formatEnumChoice(snakeCaseName: string, choice: string): string { + return `${snakeCaseName}_${choice}`.toUpperCase(); +} + /** * Computes telemetry flag usage from parsed arguments and CLI options. * @@ -21,6 +30,8 @@ type CliOptions = typeof cliOptions; * - The provided value differs from the default value. * - Boolean flags are logged with their literal value. * - String flags with defined `choices` (Enums) are logged as their uppercase value. + * + * IMPORTANT: keep getPossibleFlagMetrics() in sync with this function. */ export function computeFlagUsage( args: Record, @@ -49,12 +60,58 @@ export function computeFlagUsage( 'choices' in config && config.choices ) { - // For enums, log the value as uppercase - // We're going to have an enum for such flags with choices represented - // as an `enum` where the keys of the enum will map to the uppercase `choice`. - usage[snakeCaseName] = `${snakeCaseName}_${value}`.toUpperCase(); + usage[snakeCaseName] = formatEnumChoice(snakeCaseName, value); } } return usage; } + +export interface FlagMetric { + name: string; + flagType: 'boolean' | 'enum'; + choices?: string[]; +} + +/** + * Computes the list of possible flag metrics based on the CLI options. + * + * IMPORTANT: keep this function in sync with computeFlagUsage(). + */ +export function getPossibleFlagMetrics(options: CliOptions): FlagMetric[] { + const metrics: FlagMetric[] = []; + + for (const [flagName, config] of Object.entries(options)) { + const snakeCaseName = toSnakeCase(flagName); + + // _present is always a possible metric + metrics.push({ + name: `${snakeCaseName}_present`, + flagType: 'boolean', + }); + + if (config.type === 'boolean') { + metrics.push({ + name: snakeCaseName, + flagType: 'boolean', + }); + } else if ( + config.type === 'string' && + 'choices' in config && + config.choices + ) { + metrics.push({ + name: snakeCaseName, + flagType: 'enum', + choices: [ + `${snakeCaseName.toUpperCase()}_UNSPECIFIED`, + ...config.choices.map(choice => + formatEnumChoice(snakeCaseName, choice), + ), + ], + }); + } + } + + return metrics; +} diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json new file mode 100644 index 000000000..98ea0642f --- /dev/null +++ b/src/telemetry/flag_usage_metrics.json @@ -0,0 +1,257 @@ +[ + { + "name": "accept_insecure_certs", + "flagType": "boolean" + }, + { + "name": "accept_insecure_certs_present", + "flagType": "boolean" + }, + { + "name": "auto_connect", + "flagType": "boolean" + }, + { + "name": "auto_connect_present", + "flagType": "boolean" + }, + { + "name": "browser_url_present", + "flagType": "boolean" + }, + { + "name": "category_emulation", + "flagType": "boolean" + }, + { + "name": "category_emulation_present", + "flagType": "boolean" + }, + { + "name": "category_extensions", + "flagType": "boolean" + }, + { + "name": "category_extensions_present", + "flagType": "boolean" + }, + { + "name": "category_in_page_tools", + "flagType": "boolean" + }, + { + "name": "category_in_page_tools_present", + "flagType": "boolean" + }, + { + "name": "category_network", + "flagType": "boolean" + }, + { + "name": "category_network_present", + "flagType": "boolean" + }, + { + "name": "category_performance", + "flagType": "boolean" + }, + { + "name": "category_performance_present", + "flagType": "boolean" + }, + { + "name": "channel", + "flagType": "enum", + "choices": [ + "CHANNEL_UNSPECIFIED", + "CHANNEL_CANARY", + "CHANNEL_DEV", + "CHANNEL_BETA", + "CHANNEL_STABLE" + ] + }, + { + "name": "channel_present", + "flagType": "boolean" + }, + { + "name": "chrome_arg_present", + "flagType": "boolean" + }, + { + "name": "clearcut_endpoint_present", + "flagType": "boolean" + }, + { + "name": "clearcut_force_flush_interval_ms_present", + "flagType": "boolean" + }, + { + "name": "clearcut_include_pid_header", + "flagType": "boolean" + }, + { + "name": "clearcut_include_pid_header_present", + "flagType": "boolean" + }, + { + "name": "executable_path_present", + "flagType": "boolean" + }, + { + "name": "experimental_devtools", + "flagType": "boolean" + }, + { + "name": "experimental_devtools_present", + "flagType": "boolean" + }, + { + "name": "experimental_include_all_pages", + "flagType": "boolean" + }, + { + "name": "experimental_include_all_pages_present", + "flagType": "boolean" + }, + { + "name": "experimental_interop_tools", + "flagType": "boolean" + }, + { + "name": "experimental_interop_tools_present", + "flagType": "boolean" + }, + { + "name": "experimental_memory", + "flagType": "boolean" + }, + { + "name": "experimental_memory_present", + "flagType": "boolean" + }, + { + "name": "experimental_page_id_routing", + "flagType": "boolean" + }, + { + "name": "experimental_page_id_routing_present", + "flagType": "boolean" + }, + { + "name": "experimental_screencast", + "flagType": "boolean" + }, + { + "name": "experimental_screencast_present", + "flagType": "boolean" + }, + { + "name": "experimental_structured_content", + "flagType": "boolean" + }, + { + "name": "experimental_structured_content_present", + "flagType": "boolean" + }, + { + "name": "experimental_vision", + "flagType": "boolean" + }, + { + "name": "experimental_vision_present", + "flagType": "boolean" + }, + { + "name": "experimental_webmcp", + "flagType": "boolean" + }, + { + "name": "experimental_webmcp_present", + "flagType": "boolean" + }, + { + "name": "headless", + "flagType": "boolean" + }, + { + "name": "headless_present", + "flagType": "boolean" + }, + { + "name": "ignore_default_chrome_arg_present", + "flagType": "boolean" + }, + { + "name": "isolated", + "flagType": "boolean" + }, + { + "name": "isolated_present", + "flagType": "boolean" + }, + { + "name": "log_file_present", + "flagType": "boolean" + }, + { + "name": "performance_crux", + "flagType": "boolean" + }, + { + "name": "performance_crux_present", + "flagType": "boolean" + }, + { + "name": "proxy_server_present", + "flagType": "boolean" + }, + { + "name": "redact_network_headers", + "flagType": "boolean" + }, + { + "name": "redact_network_headers_present", + "flagType": "boolean" + }, + { + "name": "slim", + "flagType": "boolean" + }, + { + "name": "slim_present", + "flagType": "boolean" + }, + { + "name": "usage_statistics", + "flagType": "boolean" + }, + { + "name": "usage_statistics_present", + "flagType": "boolean" + }, + { + "name": "user_data_dir_present", + "flagType": "boolean" + }, + { + "name": "via_cli", + "flagType": "boolean" + }, + { + "name": "via_cli_present", + "flagType": "boolean" + }, + { + "name": "viewport_present", + "flagType": "boolean" + }, + { + "name": "ws_endpoint_present", + "flagType": "boolean" + }, + { + "name": "ws_headers_present", + "flagType": "boolean" + } +] diff --git a/src/telemetry/toolMetricsUtils.ts b/src/telemetry/toolMetricsUtils.ts index 53783645d..74f2af420 100644 --- a/src/telemetry/toolMetricsUtils.ts +++ b/src/telemetry/toolMetricsUtils.ts @@ -62,10 +62,9 @@ export function applyToExistingMetrics( }); } -function applyToExisting( - existing: T[], - update: T[], -): T[] { +export function applyToExisting< + T extends {name: string; isDeprecated?: boolean}, +>(existing: T[], update: T[]): T[] { const existingNames = new Set(existing.map(item => item.name)); const updatedNames = new Set(update.map(item => item.name)); diff --git a/tests/telemetry/flagUtils.test.ts b/tests/telemetry/flagUtils.test.ts index f98ab3172..692ec0746 100644 --- a/tests/telemetry/flagUtils.test.ts +++ b/tests/telemetry/flagUtils.test.ts @@ -8,7 +8,10 @@ import assert from 'node:assert/strict'; import {describe, it} from 'node:test'; import type {cliOptions} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; -import {computeFlagUsage} from '../../src/telemetry/flagUtils.js'; +import { + computeFlagUsage, + getPossibleFlagMetrics, +} from '../../src/telemetry/flagUtils.js'; describe('computeFlagUsage', () => { const mockOptions = { @@ -105,3 +108,37 @@ describe('computeFlagUsage', () => { }); }); }); + +describe('getPossibleFlagMetrics', () => { + const mockOptions = { + boolFlag: { + type: 'boolean' as const, + description: 'A boolean flag', + }, + stringFlag: { + type: 'string' as const, + description: 'A string flag', + }, + enumFlag: { + type: 'string' as const, + description: 'An enum flag', + choices: ['a', 'b'], + }, + } as unknown as typeof cliOptions; + + it('returns all possible metrics for given options', () => { + const metrics = getPossibleFlagMetrics(mockOptions); + + assert.deepEqual(metrics, [ + {name: 'bool_flag_present', flagType: 'boolean'}, + {name: 'bool_flag', flagType: 'boolean'}, + {name: 'string_flag_present', flagType: 'boolean'}, + {name: 'enum_flag_present', flagType: 'boolean'}, + { + name: 'enum_flag', + flagType: 'enum', + choices: ['ENUM_FLAG_UNSPECIFIED', 'ENUM_FLAG_A', 'ENUM_FLAG_B'], + }, + ]); + }); +});