From 53e79e4bae2f0d62c499353190cff456015f7aea Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Mon, 29 Jun 2026 18:30:09 +0800 Subject: [PATCH] chore(telemetry): add ripgrep fallback telemetry (#1203) --- packages/agent-core/src/agent/tool/index.ts | 4 +- .../src/services/fs/fsSearchService.ts | 13 +++++- .../agent-core/src/tools/builtin/file/glob.ts | 11 +++++ .../agent-core/src/tools/builtin/file/grep.ts | 14 +++++- packages/agent-core/test/tools/glob.test.ts | 21 +++++++++ packages/agent-core/test/tools/grep.test.ts | 21 +++++++++ .../server/src/services/serviceCollection.ts | 8 ++++ packages/server/test/fs-search.e2e.test.ts | 45 ++++++++++++++++++- 8 files changed, 130 insertions(+), 7 deletions(-) diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index 78173f505..ca47f82da 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -481,8 +481,8 @@ export class ToolManager { new b.ReadTool(kaos, workspace), new b.WriteTool(kaos, workspace), new b.EditTool(kaos, workspace), - new b.GrepTool(kaos, workspace), - new b.GlobTool(kaos, workspace), + new b.GrepTool(kaos, workspace, this.agent.telemetry), + new b.GlobTool(kaos, workspace, this.agent.telemetry), new b.BashTool(kaos, cwd, background, { allowBackground, }), diff --git a/packages/agent-core/src/services/fs/fsSearchService.ts b/packages/agent-core/src/services/fs/fsSearchService.ts index 184243f60..306e00e69 100644 --- a/packages/agent-core/src/services/fs/fsSearchService.ts +++ b/packages/agent-core/src/services/fs/fsSearchService.ts @@ -4,7 +4,7 @@ import { spawn } from 'node:child_process'; import { promises as fs } from 'node:fs'; import path from 'node:path'; -import { Disposable, InstantiationType, registerSingleton } from '../../di'; +import { Disposable, SyncDescriptor, registerSingleton } from '../../di'; import type { FsGrepFileHit, FsGrepMatch, @@ -19,6 +19,7 @@ import ignore, { type Ignore } from 'ignore'; import { ISessionService } from '../session/session'; import { ILogService } from '../logger/logger'; +import { noopTelemetryClient, type TelemetryClient } from '../../telemetry'; import { IFsSearchService, FsGrepTimeoutError } from './fsSearch'; const SEARCH_HARD_CAP = 500; @@ -39,11 +40,15 @@ export class FsSearchService protected rgMissingWarned = false; + protected readonly telemetry: TelemetryClient; + constructor( + telemetry: TelemetryClient, @ISessionService protected readonly sessions: ISessionService, @ILogService protected readonly logger: ILogService, ) { super(); + this.telemetry = telemetry; } override dispose(): void { @@ -119,6 +124,7 @@ export class FsSearchService ); return out; } + this.telemetry.track('fs_grep_node_fallback', { reason: 'rg_missing' }); const out = await this.grepWithNode( realCwd, req, @@ -644,4 +650,7 @@ async function whichBinary(name: string): Promise { return null; } -registerSingleton(IFsSearchService, FsSearchService, InstantiationType.Delayed); +registerSingleton( + IFsSearchService, + new SyncDescriptor(FsSearchService, [noopTelemetryClient], true), +); diff --git a/packages/agent-core/src/tools/builtin/file/glob.ts b/packages/agent-core/src/tools/builtin/file/glob.ts index 04bc30a5c..69d01c602 100644 --- a/packages/agent-core/src/tools/builtin/file/glob.ts +++ b/packages/agent-core/src/tools/builtin/file/glob.ts @@ -32,6 +32,7 @@ import type { BuiltinTool } from '../../../agent/tool'; import { isAbortError } from '../../../loop/errors'; import { ToolAccesses } from '../../../loop/tool-access'; import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { noopTelemetryClient, type TelemetryClient } from '../../../telemetry'; import { isWithinDirectory, resolvePathAccessPath } from '../../policies/path-access'; import type { PathClass } from '../../policies/path-access'; import { isSensitiveFile } from '../../policies/sensitive'; @@ -104,10 +105,13 @@ export class GlobTool implements BuiltinTool { readonly name = 'Glob' as const; readonly description: string; readonly parameters: Record = toInputJsonSchema(GlobInputSchema); + private readonly telemetry: TelemetryClient; constructor( private readonly kaos: Kaos, private readonly workspace: WorkspaceConfig, + telemetry: TelemetryClient = noopTelemetryClient, ) { + this.telemetry = telemetry; this.description = this.kaos.pathClass() === 'win32' ? GLOB_DESCRIPTION + WINDOWS_PATH_HINT @@ -176,10 +180,17 @@ export class GlobTool implements BuiltinTool { try { const resolution = await ensureRgPath({ signal }); rgPath = resolution.path; + if (resolution.source !== 'system-path') { + this.telemetry.track('glob_tool_rg_fallback', { + source: resolution.source, + outcome: 'resolved', + }); + } } catch (error) { if (isAbortError(error)) { return { isError: true, output: 'Glob aborted' }; } + this.telemetry.track('glob_tool_rg_fallback', { outcome: 'failed' }); return { isError: true, output: rgUnavailableMessage(error) }; } diff --git a/packages/agent-core/src/tools/builtin/file/grep.ts b/packages/agent-core/src/tools/builtin/file/grep.ts index 0da86de43..f4e3622b7 100644 --- a/packages/agent-core/src/tools/builtin/file/grep.ts +++ b/packages/agent-core/src/tools/builtin/file/grep.ts @@ -25,6 +25,7 @@ import type { BuiltinTool } from '../../../agent/tool'; import { isAbortError } from '../../../loop/errors'; import { ToolAccesses } from '../../../loop/tool-access'; import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { noopTelemetryClient, type TelemetryClient } from '../../../telemetry'; import { resolvePathAccessPath } from '../../policies/path-access'; import type { PathClass } from '../../policies/path-access'; import { isSensitiveFile } from '../../policies/sensitive'; @@ -158,10 +159,14 @@ export class GrepTool implements BuiltinTool { readonly name = 'Grep' as const; readonly description = GREP_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(GrepInputSchema); + private readonly telemetry: TelemetryClient; constructor( private readonly kaos: Kaos, private readonly workspace: WorkspaceConfig, - ) {} + telemetry: TelemetryClient = noopTelemetryClient, + ) { + this.telemetry = telemetry; + } resolveExecution(args: GrepInput): ToolExecution { let path: string | undefined; @@ -199,10 +204,17 @@ export class GrepTool implements BuiltinTool { try { const resolution = await ensureRgPath({ signal }); rgPath = resolution.path; + if (resolution.source !== 'system-path') { + this.telemetry.track('grep_tool_rg_fallback', { + source: resolution.source, + outcome: 'resolved', + }); + } } catch (error) { if (isAbortError(error)) { return { isError: true, output: 'Grep aborted' }; } + this.telemetry.track('grep_tool_rg_fallback', { outcome: 'failed' }); return { isError: true, output: rgUnavailableMessage(error) }; } diff --git a/packages/agent-core/test/tools/glob.test.ts b/packages/agent-core/test/tools/glob.test.ts index ebdc8703b..dbe0119a5 100644 --- a/packages/agent-core/test/tools/glob.test.ts +++ b/packages/agent-core/test/tools/glob.test.ts @@ -18,6 +18,7 @@ import { ensureRgPath } from '../../src/tools/support/rg-locator'; import type { WorkspaceConfig } from '../../src/tools/support/workspace'; import { createFakeKaos } from './fixtures/fake-kaos'; import { executeTool } from './fixtures/execute-tool'; +import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry'; vi.mock('../../src/tools/support/rg-locator', () => ({ ensureRgPath: vi.fn(async () => ({ path: '/mock/rg', source: 'system-path' })), @@ -108,6 +109,26 @@ describe('GlobTool', () => { expect(schema.properties['include_dirs']?.description?.toLowerCase()).toContain('deprecated'); }); + it('tracks when glob uses a non-system ripgrep fallback', async () => { + vi.mocked(ensureRgPath).mockResolvedValueOnce({ + path: '/mock/rg', + source: 'share-bin-downloaded', + }); + const records: TelemetryRecord[] = []; + const exec = execReturning('/workspace/src/a.ts\n'); + const tool = new GlobTool(kaosWithExec(exec), workspace, recordingTelemetry(records)); + + const result = await executeTool(tool, context({ pattern: 'src/**/*.ts', path: '/workspace' })); + + expect(result.output).toBe('src/a.ts'); + expect(records).toEqual([ + { + event: 'glob_tool_rg_fallback', + properties: { source: 'share-bin-downloaded', outcome: 'resolved' }, + }, + ]); + }); + it('injects the Windows path hint into the description on a win32 backend', () => { const tool = new GlobTool(createFakeKaos({ pathClass: () => 'win32' }), workspace); diff --git a/packages/agent-core/test/tools/grep.test.ts b/packages/agent-core/test/tools/grep.test.ts index c22f8bb2a..ff9027a8d 100644 --- a/packages/agent-core/test/tools/grep.test.ts +++ b/packages/agent-core/test/tools/grep.test.ts @@ -9,6 +9,7 @@ import { ensureRgPath } from '../../src/tools/support/rg-locator'; import type { WorkspaceConfig } from '../../src/tools/support/workspace'; import { createFakeKaos, toolContentString } from './fixtures/fake-kaos'; import { executeTool } from './fixtures/execute-tool'; +import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry'; vi.mock('../../src/tools/support/rg-locator', () => ({ ensureRgPath: vi.fn(async () => ({ path: '/mock/rg', source: 'system-path' })), @@ -1391,6 +1392,26 @@ describe('GrepTool', () => { expect(exec).not.toHaveBeenCalled(); }); + it('tracks when grep uses a non-system ripgrep fallback', async () => { + vi.mocked(ensureRgPath).mockResolvedValueOnce({ + path: '/mock/rg', + source: 'share-bin-downloaded', + }); + const records: TelemetryRecord[] = []; + const exec = vi.fn().mockResolvedValue(processWithOutput('/workspace/src/a.ts\n')); + const tool = new GrepTool(createFakeKaos({ exec }), workspace, recordingTelemetry(records)); + + const result = await executeTool(tool, context({ pattern: 'hit' })); + + expect(result.isError).not.toBe(true); + expect(records).toEqual([ + { + event: 'grep_tool_rg_fallback', + properties: { source: 'share-bin-downloaded', outcome: 'resolved' }, + }, + ]); + }); + it('returns an install hint when spawning the resolved ripgrep path hits ENOENT', async () => { const error = Object.assign(new Error('spawn /mock/rg ENOENT'), { code: 'ENOENT' }); const exec = vi.fn().mockRejectedValue(error); diff --git a/packages/server/src/services/serviceCollection.ts b/packages/server/src/services/serviceCollection.ts index 203641d3c..7eaf3f2ff 100644 --- a/packages/server/src/services/serviceCollection.ts +++ b/packages/server/src/services/serviceCollection.ts @@ -52,6 +52,14 @@ export function createServerServiceCollection( } services.set(Services.ILogService, new PinoLoggerAdapter(pinoLogger)); + services.set( + Services.IFsSearchService, + new SyncDescriptor( + Services.FsSearchService, + [server.coreProcessOptions?.telemetry ?? Services.noopTelemetryClient], + true, + ), + ); services.set(IRestGateway, new FastifyRestGateway(app)); services.set(Services.IEnvironmentService, envService); diff --git a/packages/server/test/fs-search.e2e.test.ts b/packages/server/test/fs-search.e2e.test.ts index 4a887dfb2..06b88e967 100644 --- a/packages/server/test/fs-search.e2e.test.ts +++ b/packages/server/test/fs-search.e2e.test.ts @@ -12,7 +12,14 @@ import { join } from 'node:path'; import { pino } from 'pino'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { ISessionService, FsSearchService, ILogService } from '@moonshot-ai/agent-core'; +import { + ISessionService, + FsSearchService, + ILogService, + noopTelemetryClient, + type TelemetryClient, + type TelemetryProperties, +} from '@moonshot-ai/agent-core'; import { IRestGateway, startServer, type RunningServer } from '../src'; import { fixedTokenAuth } from './helpers/serverHarness'; @@ -398,6 +405,14 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => { } class StubMissingRg extends FsSearchService { + constructor( + sessions: ISessionService, + logger: ILogService, + telemetry: TelemetryClient = noopTelemetryClient, + ) { + super(telemetry, sessions, logger); + } + public override probeRg(): Promise { if (this.rgPath !== undefined) return Promise.resolve(this.rgPath); this.rgPath = null; @@ -445,6 +460,32 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => { svc.dispose(); }); + it('tracks fs grep node fallback when rg is missing', async () => { + const sessions = makeStubSession(workspace); + const logger = makeStubLogger(); + const events: Array<{ event: string; properties?: TelemetryProperties }> = []; + const svc = new StubMissingRg(sessions, logger, { + track: (event, properties) => events.push({ event, properties }), + }); + writeFileSync(join(workspace, 'a.txt'), 'needle\n'); + + await svc.grep('sess_stub', { + pattern: 'needle', + regex: false, + case_sensitive: true, + follow_gitignore: true, + max_files: 200, + max_matches_per_file: 50, + max_total_matches: 5000, + context_lines: 0, + }); + + expect(events).toEqual([ + { event: 'fs_grep_node_fallback', properties: { reason: 'rg_missing' } }, + ]); + svc.dispose(); + }); + it('grep timeout fires FsGrepTimeoutError → 41305', async () => { const sessions = makeStubSession(workspace); const logger = makeStubLogger(); @@ -467,7 +508,7 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => { return Promise.resolve(null); } } - const svc = new StubTimeout(sessions, logger); + const svc = new StubTimeout(noopTelemetryClient, sessions, logger); writeFileSync(join(workspace, 'a.txt'), 'needle\n'); await expect( svc.grep('sess_stub', {