豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import process from 'node:process';

import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {parseArguments} from './cli.js';
import {cliOptions, parseArguments} from './cli.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
import {McpResponse} from './McpResponse.js';
import {Mutex} from './Mutex.js';
import {ClearcutLogger} from './telemetry/clearcut-logger.js';
import {computeFlagUsage} from './telemetry/flag-utils.js';
import {
McpServer,
StdioServerTransport,
Expand All @@ -34,6 +36,10 @@ const VERSION = '0.12.1';
export const args = parseArguments(VERSION);

const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
let clearcutLogger: ClearcutLogger | undefined;
if (args.usageStatistics) {
clearcutLogger = new ClearcutLogger();
}

process.on('unhandledRejection', (reason, promise) => {
logger('Unhandled promise rejection', promise, reason);
Expand Down Expand Up @@ -148,6 +154,8 @@ function registerTool(tool: ToolDefinition): void {
},
async (params): Promise<CallToolResult> => {
const guard = await toolMutex.acquire();
const startTime = Date.now();
let success = false;
try {
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
const context = await getContext();
Expand All @@ -170,6 +178,7 @@ function registerTool(tool: ToolDefinition): void {
} = {
content,
};
success = true;
if (args.experimentalStructuredContent) {
result.structuredContent = structuredContent as Record<
string,
Expand All @@ -193,6 +202,11 @@ function registerTool(tool: ToolDefinition): void {
isError: true,
};
} finally {
void clearcutLogger?.logToolInvocation({
toolName: tool.name,
success,
latencyMs: Date.now() - startTime,
});
guard.dispose();
}
},
Expand All @@ -208,3 +222,4 @@ const transport = new StdioServerTransport();
await server.connect(transport);
logger('Chrome DevTools MCP Server connected');
logDisclaimers();
void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));
38 changes: 38 additions & 0 deletions src/telemetry/clearcut-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {ClearcutSender} from './clearcut-sender.js';
import type {FlagUsage} from './types.js';

export class ClearcutLogger {
#sender: ClearcutSender;

constructor(sender?: ClearcutSender) {
this.#sender = sender ?? new ClearcutSender();
}

async logToolInvocation(args: {
toolName: string;
success: boolean;
latencyMs: number;
}): Promise<void> {
await this.#sender.send({
tool_invocation: {
tool_name: args.toolName,
success: args.success,
latency_ms: args.latencyMs,
},
});
}

async logServerStart(flagUsage: FlagUsage): Promise<void> {
await this.#sender.send({
server_start: {
flag_usage: flagUsage,
},
});
}
}
15 changes: 15 additions & 0 deletions src/telemetry/clearcut-sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {logger} from '../logger.js';

import type {ChromeDevToolsMcpExtension} from './types.js';

export class ClearcutSender {
async send(event: ChromeDevToolsMcpExtension): Promise<void> {
logger('Telemetry event', JSON.stringify(event, null, 2));
}
}
60 changes: 60 additions & 0 deletions src/telemetry/flag-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {cliOptions} from '../cli.js';
import {toSnakeCase} from '../utils/string.js';

import type {FlagUsage} from './types.js';

type CliOptions = typeof cliOptions;

/**
* Computes telemetry flag usage from parsed arguments and CLI options.
*
* Iterates over the defined CLI options to construct a payload:
* - Flag names are converted to snake_case (e.g. `browserUrl` -> `browser_url`).
* - A flag is logged as `{flag_name}_present` if:
* - It has no default value, OR
* - 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.
*/
export function computeFlagUsage(
args: Record<string, unknown>,
options: CliOptions,
): FlagUsage {
const usage: FlagUsage = {};

for (const [flagName, config] of Object.entries(options)) {
const value = args[flagName];
const snakeCaseName = toSnakeCase(flagName);

// If there isn't a default value provided for the flag,
// we're going to log whether it's present on the args user
// provided or not. If there is a default value, we only log presence
// if the value differs from the default, implying explicit user intent.
if (!('default' in config) || value !== config.default) {
usage[`${snakeCaseName}_present`] = value !== undefined && value !== null;
}

if (config.type === 'boolean' && typeof value === 'boolean') {
// For boolean options, we're going to log the value directly.
usage[snakeCaseName] = value;
} else if (
config.type === 'string' &&
typeof value === 'string' &&
'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] = value.toUpperCase();
}
}

return usage;
}
70 changes: 70 additions & 0 deletions src/telemetry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

// Protobuf message interfaces
export interface ChromeDevToolsMcpExtension {
os_type?: OsType;
mcp_client?: McpClient;
app_version?: string;
session_id?: string;
tool_invocation?: ToolInvocation;
server_start?: ServerStart;
daily_active?: DailyActive;
first_time_installation?: FirstTimeInstallation;
}

export interface ToolInvocation {
tool_name: string;
success: boolean;
latency_ms: number;
}

export interface ServerStart {
flag_usage?: FlagUsage;
}

export interface DailyActive {
days_since_last_active: number;
}

export type FirstTimeInstallation = Record<string, never>;

export type FlagUsage = Record<string, boolean | string | number | undefined>;

// Clearcut API interfaces
export interface LogRequest {
log_source: number;
request_time_ms: string;
client_info: {
client_type: number;
};
log_event: Array<{
event_time_ms: string;
source_extension_json: string;
}>;
}

// Enums
export enum OsType {
OS_TYPE_UNSPECIFIED = 0,
OS_TYPE_WINDOWS = 1,
OS_TYPE_MACOS = 2,
OS_TYPE_LINUX = 3,
}

export enum ChromeChannel {
CHROME_CHANNEL_UNSPECIFIED = 0,
CHROME_CHANNEL_CANARY = 1,
CHROME_CHANNEL_DEV = 2,
CHROME_CHANNEL_BETA = 3,
CHROME_CHANNEL_STABLE = 4,
}

export enum McpClient {
MCP_CLIENT_UNSPECIFIED = 0,
MCP_CLIENT_CLAUDE_CODE = 1,
MCP_CLIENT_GEMINI_CLI = 2,
}
38 changes: 38 additions & 0 deletions src/utils/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Converts a given string to snake_case.
* This function handles camelCase, PascalCase, and acronyms, including transitions between letters and numbers.
* It uses Unicode-aware regular expressions (`\p{L}`, `\p{N}`, `\p{Lu}`, `\p{Ll}` with the `u` flag)
* to correctly process letters and numbers from various languages.
*
* @param text The input string to convert to snake_case.
* @returns The snake_case version of the input string.
*/
export function toSnakeCase(text: string): string {
if (!text) {
return '';
}
// First, handle case-based transformations to insert underscores correctly.
// 1. Add underscore between a letter and a number.
// e.g., "version2" -> "version_2"
// 2. Add underscore between an uppercase letter sequence and a following uppercase+lowercase sequence.
// e.g., "APIFlags" -> "API_Flags"
// 3. Add underscore between a lowercase/number and an uppercase letter.
// e.g., "lastName" -> "last_Name", "version_2Update" -> "version_2_Update"
// 4. Replace sequences of non-alphanumeric with a single underscore
// 5. Remove any leading or trailing underscores.
const result = text
.replace(/(\p{L})(\p{N})/gu, '$1_$2') // 1
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1_$2') // 2
.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, '$1_$2') // 3
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '_') // 4
.replace(/^_|_$/g, ''); // 5

return result;
}
47 changes: 47 additions & 0 deletions tests/telemetry/clearcut-logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'node:assert';
import {describe, it, mock} from 'node:test';

import {ClearcutLogger} from '../../src/telemetry/clearcut-logger.js';
import {ClearcutSender} from '../../src/telemetry/clearcut-sender.js';

describe('ClearcutLogger', () => {
it('should log tool invocation via sender', async () => {
const sender = new ClearcutSender();
const sendSpy = mock.method(sender, 'send');
const loggerInstance = new ClearcutLogger(sender);

await loggerInstance.logToolInvocation({
toolName: 'test-tool',
success: true,
latencyMs: 100,
});

assert.strictEqual(sendSpy.mock.callCount(), 1);
const event = sendSpy.mock.calls[0].arguments[0];
assert.deepStrictEqual(event.tool_invocation, {
tool_name: 'test-tool',
success: true,
latency_ms: 100,
});
});

it('should log server start via sender', async () => {
const sender = new ClearcutSender();
const sendSpy = mock.method(sender, 'send');
const loggerInstance = new ClearcutLogger(sender);

await loggerInstance.logServerStart({headless: true});

assert.strictEqual(sendSpy.mock.callCount(), 1);
const event = sendSpy.mock.calls[0].arguments[0];
assert.deepStrictEqual(event.server_start, {
flag_usage: {headless: true},
});
});
});
37 changes: 37 additions & 0 deletions tests/telemetry/flag-utils.test.js.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
exports[`computeFlagUsage > matches snapshot for all current CLI options 1`] = `
{
"auto_connect_present": true,
"auto_connect": true,
"browser_url_present": true,
"ws_endpoint_present": true,
"ws_headers_present": true,
"headless_present": true,
"headless": true,
"executable_path_present": true,
"isolated_present": true,
"isolated": true,
"user_data_dir_present": true,
"channel_present": true,
"channel": "STABLE",
"log_file_present": true,
"viewport_present": true,
"proxy_server_present": true,
"accept_insecure_certs_present": true,
"accept_insecure_certs": true,
"experimental_devtools_present": true,
"experimental_devtools": true,
"experimental_vision_present": true,
"experimental_vision": true,
"experimental_structured_content_present": true,
"experimental_structured_content": true,
"experimental_include_all_pages_present": true,
"experimental_include_all_pages": true,
"chrome_arg_present": true,
"ignore_default_chrome_arg_present": true,
"category_emulation": true,
"category_performance": true,
"category_network": true,
"usage_statistics_present": true,
"usage_statistics": true
}
`;
Loading
Loading