mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-28 11:22:57 +08:00
feat(telemetry): recording a sanitized version of the client name (#2757)
This commit is contained in:
@@ -16,10 +16,12 @@ import {
|
||||
bucketizeDaysSince,
|
||||
bucketizeLatency,
|
||||
buildContext,
|
||||
sanitizeClientName,
|
||||
sanitizeParams,
|
||||
stripUnderscoreBeforeNumber,
|
||||
} from './transformation.js';
|
||||
import {
|
||||
type ChromeDevToolsMcpExtension,
|
||||
McpClient,
|
||||
type FlagUsage,
|
||||
WatchdogMessageType,
|
||||
@@ -88,6 +90,7 @@ export class ClearcutLogger {
|
||||
#persistence: Persistence;
|
||||
#watchdog: WatchdogClient;
|
||||
#mcpClient: McpClient;
|
||||
#rawClientName?: string;
|
||||
#state?: LocalState;
|
||||
|
||||
static initialize(options: ClearcutLoggerOptions): ClearcutLogger {
|
||||
@@ -157,9 +160,28 @@ export class ClearcutLogger {
|
||||
this.#mcpClient = McpClient.MCP_CLIENT_HERMES;
|
||||
} else {
|
||||
this.#mcpClient = McpClient.MCP_CLIENT_OTHER;
|
||||
this.#rawClientName = sanitizeClientName(clientName);
|
||||
}
|
||||
}
|
||||
|
||||
#addMcpClient(
|
||||
payload: ChromeDevToolsMcpExtension,
|
||||
): ChromeDevToolsMcpExtension {
|
||||
const ext: ChromeDevToolsMcpExtension = {
|
||||
...payload,
|
||||
mcp_client: this.#mcpClient,
|
||||
};
|
||||
if (
|
||||
this.#mcpClient === McpClient.MCP_CLIENT_OTHER &&
|
||||
this.#rawClientName !== undefined
|
||||
) {
|
||||
ext.raw_mcp_client_info = {
|
||||
raw_client_name: this.#rawClientName,
|
||||
};
|
||||
}
|
||||
return ext;
|
||||
}
|
||||
|
||||
async logToolInvocation(args: {
|
||||
toolName: string;
|
||||
params: ShapeOutput<zod.ZodRawShape>;
|
||||
@@ -194,22 +216,20 @@ export class ClearcutLogger {
|
||||
|
||||
this.#watchdog.send({
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: this.#mcpClient,
|
||||
payload: this.#addMcpClient({
|
||||
tool_invocation: tool_invocation,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async logServerStart(flagUsage: FlagUsage): Promise<void> {
|
||||
this.#watchdog.send({
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: this.#mcpClient,
|
||||
payload: this.#addMcpClient({
|
||||
server_start: {
|
||||
flag_usage: flagUsage,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,12 +242,11 @@ export class ClearcutLogger {
|
||||
|
||||
this.#watchdog.send({
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: this.#mcpClient,
|
||||
payload: this.#addMcpClient({
|
||||
daily_active: {
|
||||
days_since_last_active: bucketizeDaysSince(daysSince),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
this.#state.lastActive = new Date().toISOString();
|
||||
@@ -244,15 +263,14 @@ export class ClearcutLogger {
|
||||
}): Promise<void> {
|
||||
this.#watchdog.send({
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: this.#mcpClient,
|
||||
payload: this.#addMcpClient({
|
||||
server_error: {
|
||||
tool_name: args.toolName
|
||||
? stripUnderscoreBeforeNumber(args.toolName)
|
||||
: '',
|
||||
error_code: args.errorCode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,12 +300,11 @@ export class ClearcutLogger {
|
||||
|
||||
this.#watchdog.send({
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: this.#mcpClient,
|
||||
payload: this.#addMcpClient({
|
||||
tool_active: {
|
||||
days_since_last_tool_call: bucketizeDaysSince(daysSinceToolCall),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
this.#state.lastToolCall = now.toISOString();
|
||||
|
||||
@@ -27,6 +27,21 @@ export function bucketizeDaysSince(days: number): number {
|
||||
return Math.min(days, MAX_ACTIVE_DAYS);
|
||||
}
|
||||
|
||||
export const REDACTED_CLIENT_NAME = '<redacted>';
|
||||
const VALID_CLIENT_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
const MAX_CLIENT_NAME_LENGTH = 32;
|
||||
|
||||
export function sanitizeClientName(clientName: string): string {
|
||||
if (
|
||||
clientName.length > 0 &&
|
||||
clientName.length < MAX_CLIENT_NAME_LENGTH &&
|
||||
VALID_CLIENT_NAME_REGEX.test(clientName)
|
||||
) {
|
||||
return clientName;
|
||||
}
|
||||
return REDACTED_CLIENT_NAME;
|
||||
}
|
||||
|
||||
export const PARAM_BLOCKLIST = new Set(['uid', 'reqid', 'msgid']);
|
||||
|
||||
const SUPPORTED_ZOD_TYPES = [
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {ErrorCode} from './errors.js';
|
||||
export interface ChromeDevToolsMcpExtension {
|
||||
os_type?: OsType;
|
||||
mcp_client?: McpClient;
|
||||
raw_mcp_client_info?: RawMcpClientInfo;
|
||||
app_version?: string;
|
||||
session_id?: string;
|
||||
tool_invocation?: ToolInvocation;
|
||||
@@ -20,6 +21,10 @@ export interface ChromeDevToolsMcpExtension {
|
||||
tool_active?: ToolActive;
|
||||
}
|
||||
|
||||
export interface RawMcpClientInfo {
|
||||
raw_client_name: string;
|
||||
}
|
||||
|
||||
export interface ServerError {
|
||||
tool_name?: string;
|
||||
error_code: ErrorCode;
|
||||
|
||||
@@ -137,11 +137,12 @@ describe('ClearcutLogger', () => {
|
||||
{name: 'xai-sdk', expected: 8}, // MCP_CLIENT_GROK
|
||||
{name: 'github-copilot-developer', expected: 11}, // MCP_CLIENT_GITHUB_COPILOT
|
||||
{name: 'copilot-intellij', expected: 11}, // MCP_CLIENT_GITHUB_COPILOT
|
||||
{name: 'unknown-client', expected: 3}, // MCP_CLIENT_OTHER
|
||||
{name: 'unknown-client', expected: 3, rawName: 'unknown-client'}, // MCP_CLIENT_OTHER
|
||||
{name: 'unknown client', expected: 3, rawName: '<redacted>'}, // MCP_CLIENT_OTHER
|
||||
{name: 'hermes-agent/1.0.0', expected: 12}, // MCP_CLIENT_HERMES
|
||||
];
|
||||
|
||||
for (const {name, expected} of clients) {
|
||||
for (const {name, expected, rawName} of clients) {
|
||||
it(`maps ${name} client correctly`, async () => {
|
||||
const logger = ClearcutLogger.initialize({
|
||||
persistence: mockPersistence,
|
||||
@@ -156,6 +157,9 @@ describe('ClearcutLogger', () => {
|
||||
type: WatchdogMessageType.LOG_EVENT,
|
||||
payload: {
|
||||
mcp_client: expected,
|
||||
...(rawName
|
||||
? {raw_mcp_client_info: {raw_client_name: rawName}}
|
||||
: {}),
|
||||
server_start: {
|
||||
flag_usage: {headless: true},
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
buildContext,
|
||||
getEnumValues,
|
||||
MAX_ACTIVE_DAYS,
|
||||
sanitizeClientName,
|
||||
sanitizeParams,
|
||||
stripUnderscoreBeforeNumber,
|
||||
transformArgName,
|
||||
@@ -277,3 +278,32 @@ describe('buildContext', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeClientName', () => {
|
||||
it('returns valid alphanumeric client names with hyphens and underscores as is', () => {
|
||||
const validNames = [
|
||||
'a',
|
||||
'my-custom_client123',
|
||||
'Client_Name-1',
|
||||
'a'.repeat(31),
|
||||
];
|
||||
for (const name of validNames) {
|
||||
assert.strictEqual(sanitizeClientName(name), name);
|
||||
}
|
||||
});
|
||||
|
||||
it('redacts invalid or overly long client names', () => {
|
||||
const invalidNames = [
|
||||
'',
|
||||
'a'.repeat(32),
|
||||
'a'.repeat(100),
|
||||
'client with spaces',
|
||||
'client/1.0',
|
||||
'client.name',
|
||||
'client@home',
|
||||
];
|
||||
for (const name of invalidNames) {
|
||||
assert.strictEqual(sanitizeClientName(name), '<redacted>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user