feat(telemetry): report whether tool call is made on localhost. (#2455)

This commit is contained in:
yulunz
2026-08-03 14:53:33 +02:00
committed by GitHub
parent c5ebf9e202
commit 6398b7e9a4
5 changed files with 63 additions and 7 deletions
+6
View File
@@ -214,6 +214,7 @@ export class ToolHandler {
const startTime = Date.now();
let success = false;
let devToolsData: DevToolsData | undefined;
let pageUrl: string | undefined;
try {
logger?.(
`${this.tool.name} request: ${JSON.stringify(params, null, ' ')}`,
@@ -270,6 +271,10 @@ export class ToolHandler {
response.setError(err);
}
devToolsData = await context.getDevToolsData(page);
const targetPage = page ?? context.getSelectedMcpPage();
if (targetPage?.pptrPage?.isClosed() === false) {
pageUrl = targetPage.pptrPage.url();
}
// Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat
let dataFormat: DataFormat = 'default';
if (this.serverArgs.experimentalDataFormat) {
@@ -321,6 +326,7 @@ export class ToolHandler {
success,
latencyMs: bucketizeLatency(Date.now() - startTime),
isDevToolsOpen,
pageUrl,
});
guard[Symbol.dispose]();
}
+11 -3
View File
@@ -9,6 +9,7 @@ import process from 'node:process';
import {DAEMON_CLIENT_NAME} from '../daemon/utils.js';
import type {zod, ShapeOutput} from '../third_party/index.js';
import {logger} from '../utils/logger.js';
import {isLocalhost} from '../utils/url.js';
import type {ErrorCode} from './errors.js';
import type {LocalState, Persistence} from './persistence.js';
@@ -19,6 +20,7 @@ import {
WatchdogMessageType,
OsType,
type ToolInvocation,
type ToolInvocationContext,
} from './types.js';
import {WatchdogClient} from './WatchdogClient.js';
@@ -113,6 +115,7 @@ export class ClearcutLogger {
success: boolean;
latencyMs: number;
isDevToolsOpen?: boolean;
pageUrl?: string;
}): Promise<void> {
const sanitizedToolName = stripUnderscoreBeforeNumber(args.toolName);
const tool_invocation: ToolInvocation = {
@@ -120,10 +123,15 @@ export class ClearcutLogger {
success: args.success,
latency_ms: args.latencyMs,
};
const context: ToolInvocationContext = {};
if (args.isDevToolsOpen !== undefined) {
tool_invocation.context = {
is_devtools_open: args.isDevToolsOpen,
};
context.is_devtools_open = args.isDevToolsOpen;
}
if (args.pageUrl !== undefined) {
context.is_localhost = isLocalhost(args.pageUrl);
}
if (Object.keys(context).length > 0) {
tool_invocation.context = context;
}
if (Object.keys(args.params).length > 0) {
tool_invocation.tool_params = {
+1
View File
@@ -28,6 +28,7 @@ export type ServerShutdown = Record<string, never>;
export interface ToolInvocationContext {
is_devtools_open?: boolean;
is_localhost?: boolean;
}
export interface ToolInvocation {
+16 -4
View File
@@ -104,7 +104,7 @@ describe('ToolHandler', () => {
const result = await toolHandler.handle({});
assert.strictEqual(mockContext.getDevToolsData.calledOnce, true);
assert.strictEqual(mockContext.getSelectedMcpPage.called, false);
assert.strictEqual(mockContext.getSelectedMcpPage.calledOnce, true);
assert.strictEqual(mockContext.getPageById.called, false);
assert.strictEqual(handlerCalled, true);
assert.strictEqual(result.isError, undefined);
@@ -112,9 +112,10 @@ describe('ToolHandler', () => {
it('logs isDevToolsOpen telemetry based on getDevToolsData', async () => {
let handlerCalled = false;
const tool: ToolDefinition = {
name: 'global_tool',
description: 'A global tool',
const tool: DefinedPageTool = {
name: 'page_tool',
description: 'A page tool',
pageScoped: true,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
@@ -129,6 +130,13 @@ describe('ToolHandler', () => {
const mockContext = sinon.createStubInstance(McpContext);
mockContext.getDevToolsData.resolves({cdpBackendNodeId: 1});
const mockPage = {
pptrPage: {
isClosed: () => false,
url: () => 'http://localhost:9222/',
},
} as unknown as McpPage;
mockContext.getSelectedMcpPage.returns(mockPage);
const logSpy = sinon.spy();
sinon.stub(ClearcutLogger, 'get').returns({
@@ -151,6 +159,10 @@ describe('ToolHandler', () => {
assert.strictEqual(logSpy.calledOnce, true);
assert.strictEqual(logSpy.firstCall.args[0].isDevToolsOpen, true);
assert.strictEqual(
logSpy.firstCall.args[0].pageUrl,
'http://localhost:9222/',
);
assert.strictEqual(handlerCalled, true);
});
+29
View File
@@ -81,6 +81,35 @@ describe('ClearcutLogger', () => {
is_devtools_open: true,
});
});
it('sends context with correct is_localhost based on the URL', async () => {
const logger = ClearcutLogger.initialize({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
for (const {pageUrl, isLocalhost} of [
{pageUrl: 'http://localhost:9222/test', isLocalhost: true},
{pageUrl: 'https://example.com/test', isLocalhost: false},
]) {
mockWatchdogClient.send.resetHistory();
await logger.logToolInvocation({
toolName: 'test_tool',
params: {},
schema: {},
success: true,
latencyMs: 123,
pageUrl,
});
assert(mockWatchdogClient.send.calledOnce);
const msg = mockWatchdogClient.send.firstCall.args[0];
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
assert.deepStrictEqual(msg.payload.tool_invocation?.context, {
is_localhost: isLocalhost,
});
}
});
it('sends sanitized params', async () => {
const logger = ClearcutLogger.initialize({
persistence: mockPersistence,