豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 20 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
import {McpResponse} from './McpResponse.js';
import {ClearcutLogger} from './telemetry/clearcut-logger.js';
import {Mutex} from './Mutex.js';

Check failure on line 19 in src/main.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

`./Mutex.js` import should occur before import of `./telemetry/clearcut-logger.js`
import {
McpServer,
StdioServerTransport,
Expand All @@ -34,6 +35,10 @@
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 +153,8 @@
},
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 +177,7 @@
} = {
content,
};
success = true;
if (args.experimentalStructuredContent) {
result.structuredContent = structuredContent as Record<
string,
Expand All @@ -193,6 +201,11 @@
isError: true,
};
} finally {
void clearcutLogger?.logToolInvocation({
toolName: tool.name,
success,
latencyMs: Date.now() - startTime,
});
guard.dispose();
}
},
Expand All @@ -207,4 +220,11 @@
const transport = new StdioServerTransport();
await server.connect(transport);
logger('Chrome DevTools MCP Server connected');
void clearcutLogger?.logServerStart({
browser_url_present: !!args.browserUrl,
Comment thread
OrKoN marked this conversation as resolved.
Outdated
headless: args.headless,
executable_path_present: !!args.executablePath,
isolated: args.isolated,
log_file_present: !!args.logFile,
});
logDisclaimers();
40 changes: 40 additions & 0 deletions src/telemetry/clearcut-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2025 Google LLC
Comment thread
ergunsh marked this conversation as resolved.
Outdated
* SPDX-License-Identifier: Apache-2.0
*/

import {

Check failure on line 7 in src/telemetry/clearcut-logger.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

All imports in the declaration are only used as types. Use `import type`
FlagUsage,
} from './types.js';
import {ClearcutSender} from './clearcut-sender.js';

Check failure on line 10 in src/telemetry/clearcut-logger.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

`./clearcut-sender.js` import should occur before import of `./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,
},
});
}
}
14 changes: 14 additions & 0 deletions src/telemetry/clearcut-sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @license
* Copyright 2025 Google LLC
Comment thread
ergunsh marked this conversation as resolved.
Outdated
* SPDX-License-Identifier: Apache-2.0
*/

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

Check failure on line 7 in src/telemetry/clearcut-sender.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

There should be at least one empty line between import groups

Check failure on line 7 in src/telemetry/clearcut-sender.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

All imports in the declaration are only used as types. Use `import type`
import {logger} from '../logger.js';

Check failure on line 8 in src/telemetry/clearcut-sender.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

`../logger.js` import should occur before import of `./types.js`

export class ClearcutSender {
async send(event: ChromeDevToolsMcpExtension): Promise<void> {
logger('Telemetry event', JSON.stringify(event, null, 2));
}
}
77 changes: 77 additions & 0 deletions src/telemetry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @license
* Copyright 2025 Google LLC
Comment thread
ergunsh marked this conversation as resolved.
Outdated
* 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 interface FirstTimeInstallation {}

export interface FlagUsage {
browser_url_present?: boolean;
headless?: boolean;
executable_path_present?: boolean;
isolated?: boolean;
channel?: ChromeChannel;
log_file_present?: boolean;
}

// 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,
}
46 changes: 46 additions & 0 deletions tests/telemetry/clearcut-logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @license
* Copyright 2025 Google LLC
Comment thread
ergunsh marked this conversation as resolved.
Outdated
* SPDX-License-Identifier: Apache-2.0
*/

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

Check failure on line 8 in tests/telemetry/clearcut-logger.test.ts

View workflow job for this annotation

GitHub Actions / [Required] Check correct format

There should be at least one empty line between import groups
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},
});
});
});
Loading