Skip to content

Commit 1323d09

Browse files
committed
chore: implement ClearcutLogger and ClearcutSender dummy
1 parent e8c9192 commit 1323d09

5 files changed

Lines changed: 197 additions & 0 deletions

File tree

src/main.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {loadIssueDescriptions} from './issue-descriptions.js';
1515
import {logger, saveLogsToFile} from './logger.js';
1616
import {McpContext} from './McpContext.js';
1717
import {McpResponse} from './McpResponse.js';
18+
import {ClearcutLogger} from './telemetry/clearcut-logger.js';
1819
import {Mutex} from './Mutex.js';
1920
import {
2021
McpServer,
@@ -34,6 +35,10 @@ const VERSION = '0.12.1';
3435
export const args = parseArguments(VERSION);
3536

3637
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
38+
let clearcutLogger: ClearcutLogger | undefined;
39+
if (args.usageStatistics) {
40+
clearcutLogger = new ClearcutLogger();
41+
}
3742

3843
process.on('unhandledRejection', (reason, promise) => {
3944
logger('Unhandled promise rejection', promise, reason);
@@ -148,6 +153,8 @@ function registerTool(tool: ToolDefinition): void {
148153
},
149154
async (params): Promise<CallToolResult> => {
150155
const guard = await toolMutex.acquire();
156+
const startTime = Date.now();
157+
let success = false;
151158
try {
152159
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
153160
const context = await getContext();
@@ -170,6 +177,7 @@ function registerTool(tool: ToolDefinition): void {
170177
} = {
171178
content,
172179
};
180+
success = true;
173181
if (args.experimentalStructuredContent) {
174182
result.structuredContent = structuredContent as Record<
175183
string,
@@ -193,6 +201,11 @@ function registerTool(tool: ToolDefinition): void {
193201
isError: true,
194202
};
195203
} finally {
204+
void clearcutLogger?.logToolInvocation({
205+
toolName: tool.name,
206+
success,
207+
latencyMs: Date.now() - startTime,
208+
});
196209
guard.dispose();
197210
}
198211
},
@@ -207,4 +220,11 @@ await loadIssueDescriptions();
207220
const transport = new StdioServerTransport();
208221
await server.connect(transport);
209222
logger('Chrome DevTools MCP Server connected');
223+
void clearcutLogger?.logServerStart({
224+
browser_url_present: !!args.browserUrl,
225+
headless: args.headless,
226+
executable_path_present: !!args.executablePath,
227+
isolated: args.isolated,
228+
log_file_present: !!args.logFile,
229+
});
210230
logDisclaimers();

src/telemetry/clearcut-logger.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
FlagUsage,
9+
} from './types.js';
10+
import {ClearcutSender} from './clearcut-sender.js';
11+
12+
export class ClearcutLogger {
13+
#sender: ClearcutSender;
14+
15+
constructor(sender?: ClearcutSender) {
16+
this.#sender = sender ?? new ClearcutSender();
17+
}
18+
19+
async logToolInvocation(args: {
20+
toolName: string;
21+
success: boolean;
22+
latencyMs: number;
23+
}): Promise<void> {
24+
await this.#sender.send({
25+
tool_invocation: {
26+
tool_name: args.toolName,
27+
success: args.success,
28+
latency_ms: args.latencyMs,
29+
},
30+
});
31+
}
32+
33+
async logServerStart(flagUsage: FlagUsage): Promise<void> {
34+
await this.#sender.send({
35+
server_start: {
36+
flag_usage: flagUsage,
37+
},
38+
});
39+
}
40+
}

src/telemetry/clearcut-sender.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {ChromeDevToolsMcpExtension} from './types.js';
8+
import {logger} from '../logger.js';
9+
10+
export class ClearcutSender {
11+
async send(event: ChromeDevToolsMcpExtension): Promise<void> {
12+
logger('Telemetry event', JSON.stringify(event, null, 2));
13+
}
14+
}

src/telemetry/types.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// Protobuf message interfaces
8+
export interface ChromeDevToolsMcpExtension {
9+
os_type?: OsType;
10+
mcp_client?: McpClient;
11+
app_version?: string;
12+
session_id?: string;
13+
tool_invocation?: ToolInvocation;
14+
server_start?: ServerStart;
15+
daily_active?: DailyActive;
16+
first_time_installation?: FirstTimeInstallation;
17+
}
18+
19+
export interface ToolInvocation {
20+
tool_name: string;
21+
success: boolean;
22+
latency_ms: number;
23+
}
24+
25+
export interface ServerStart {
26+
flag_usage?: FlagUsage;
27+
}
28+
29+
export interface DailyActive {
30+
days_since_last_active: number;
31+
}
32+
33+
export interface FirstTimeInstallation {}
34+
35+
export interface FlagUsage {
36+
browser_url_present?: boolean;
37+
headless?: boolean;
38+
executable_path_present?: boolean;
39+
isolated?: boolean;
40+
channel?: ChromeChannel;
41+
log_file_present?: boolean;
42+
}
43+
44+
// Clearcut API interfaces
45+
export interface LogRequest {
46+
log_source: number;
47+
request_time_ms: string;
48+
client_info: {
49+
client_type: number;
50+
};
51+
log_event: Array<{
52+
event_time_ms: string;
53+
source_extension_json: string;
54+
}>;
55+
}
56+
57+
// Enums
58+
export enum OsType {
59+
OS_TYPE_UNSPECIFIED = 0,
60+
OS_TYPE_WINDOWS = 1,
61+
OS_TYPE_MACOS = 2,
62+
OS_TYPE_LINUX = 3,
63+
}
64+
65+
export enum ChromeChannel {
66+
CHROME_CHANNEL_UNSPECIFIED = 0,
67+
CHROME_CHANNEL_CANARY = 1,
68+
CHROME_CHANNEL_DEV = 2,
69+
CHROME_CHANNEL_BETA = 3,
70+
CHROME_CHANNEL_STABLE = 4,
71+
}
72+
73+
export enum McpClient {
74+
MCP_CLIENT_UNSPECIFIED = 0,
75+
MCP_CLIENT_CLAUDE_CODE = 1,
76+
MCP_CLIENT_GEMINI_CLI = 2,
77+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it, mock} from 'node:test';
9+
import {ClearcutLogger} from '../../src/telemetry/clearcut-logger.js';
10+
import {ClearcutSender} from '../../src/telemetry/clearcut-sender.js';
11+
12+
describe('ClearcutLogger', () => {
13+
it('should log tool invocation via sender', async () => {
14+
const sender = new ClearcutSender();
15+
const sendSpy = mock.method(sender, 'send');
16+
const loggerInstance = new ClearcutLogger(sender);
17+
18+
await loggerInstance.logToolInvocation({
19+
toolName: 'test-tool',
20+
success: true,
21+
latencyMs: 100,
22+
});
23+
24+
assert.strictEqual(sendSpy.mock.callCount(), 1);
25+
const event = sendSpy.mock.calls[0].arguments[0];
26+
assert.deepStrictEqual(event.tool_invocation, {
27+
tool_name: 'test-tool',
28+
success: true,
29+
latency_ms: 100,
30+
});
31+
});
32+
33+
it('should log server start via sender', async () => {
34+
const sender = new ClearcutSender();
35+
const sendSpy = mock.method(sender, 'send');
36+
const loggerInstance = new ClearcutLogger(sender);
37+
38+
await loggerInstance.logServerStart({headless: true});
39+
40+
assert.strictEqual(sendSpy.mock.callCount(), 1);
41+
const event = sendSpy.mock.calls[0].arguments[0];
42+
assert.deepStrictEqual(event.server_start, {
43+
flag_usage: {headless: true},
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)