feat(core): Download ripgrep at runtime, if enabled. (#7818)

This commit is contained in:
joshualitt
2025-09-08 14:44:56 -07:00
committed by GitHub
parent 097b5c734f
commit f0bbfe5f0a
17 changed files with 408 additions and 123 deletions
@@ -392,6 +392,24 @@ describe('ClearcutLogger', () => {
});
});
describe('logRipgrepFallbackEvent', () => {
it('logs an event with the proper name', () => {
const { logger } = setup();
// Spy on flushToClearcut to prevent it from clearing the queue
const flushSpy = vi
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn(logger!, 'flushToClearcut' as any)
.mockResolvedValue({ nextRequestWaitMs: 0 });
logger?.logRipgrepFallbackEvent();
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.RIPGREP_FALLBACK);
expect(flushSpy).toHaveBeenCalledOnce();
});
});
describe('enqueueLogEvent', () => {
it('should add events to the queue', () => {
const { logger } = setup();
@@ -44,6 +44,7 @@ export enum EventNames {
API_ERROR = 'api_error',
END_SESSION = 'end_session',
FLASH_FALLBACK = 'flash_fallback',
RIPGREP_FALLBACK = 'ripgrep_fallback',
LOOP_DETECTED = 'loop_detected',
NEXT_SPEAKER_CHECK = 'next_speaker_check',
SLASH_COMMAND = 'slash_command',
@@ -631,6 +632,13 @@ export class ClearcutLogger {
});
}
logRipgrepFallbackEvent(): void {
this.enqueueLogEvent(this.createLogEvent(EventNames.RIPGREP_FALLBACK, []));
this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error);
});
}
logLoopDetectedEvent(event: LoopDetectedEvent): void {
const data: EventValue[] = [
{
+1
View File
@@ -13,6 +13,7 @@ export const EVENT_API_ERROR = 'gemini_cli.api_error';
export const EVENT_API_RESPONSE = 'gemini_cli.api_response';
export const EVENT_CLI_CONFIG = 'gemini_cli.config';
export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback';
export const EVENT_RIPGREP_FALLBACK = 'gemini_cli.ripgrep_fallback';
export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check';
export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command';
export const EVENT_IDE_CONNECTION = 'gemini_cli.ide_connection';
@@ -30,6 +30,7 @@ import {
EVENT_FLASH_FALLBACK,
EVENT_MALFORMED_JSON_RESPONSE,
EVENT_FILE_OPERATION,
EVENT_RIPGREP_FALLBACK,
} from './constants.js';
import {
logApiRequest,
@@ -41,6 +42,7 @@ import {
logChatCompression,
logMalformedJsonResponse,
logFileOperation,
logRipgrepFallback,
} from './loggers.js';
import { ToolCallDecision } from './tool-call-decision.js';
import {
@@ -50,6 +52,7 @@ import {
ToolCallEvent,
UserPromptEvent,
FlashFallbackEvent,
RipgrepFallbackEvent,
MalformedJsonResponseEvent,
makeChatCompressionEvent,
FileOperationEvent,
@@ -453,6 +456,59 @@ describe('loggers', () => {
});
});
describe('logRipgrepFallback', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
} as unknown as Config;
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logRipgrepFallbackEvent');
});
it('should log ripgrep fallback event', () => {
const event = new RipgrepFallbackEvent();
logRipgrepFallback(mockConfig, event);
expect(
ClearcutLogger.prototype.logRipgrepFallbackEvent,
).toHaveBeenCalled();
const emittedEvent = mockLogger.emit.mock.calls[0][0];
expect(emittedEvent.body).toBe('Switching to grep as fallback.');
expect(emittedEvent.attributes).toEqual(
expect.objectContaining({
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_RIPGREP_FALLBACK,
error: undefined,
}),
);
});
it('should log ripgrep fallback event with an error', () => {
const event = new RipgrepFallbackEvent('rg not found');
logRipgrepFallback(mockConfig, event);
expect(
ClearcutLogger.prototype.logRipgrepFallbackEvent,
).toHaveBeenCalled();
const emittedEvent = mockLogger.emit.mock.calls[0][0];
expect(emittedEvent.body).toBe('Switching to grep as fallback.');
expect(emittedEvent.attributes).toEqual(
expect.objectContaining({
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_RIPGREP_FALLBACK,
error: 'rg not found',
}),
);
});
});
describe('logToolCall', () => {
const cfg1 = {
getSessionId: () => 'test-session-id',
+24
View File
@@ -27,6 +27,7 @@ import {
EVENT_CONTENT_RETRY,
EVENT_CONTENT_RETRY_FAILURE,
EVENT_FILE_OPERATION,
EVENT_RIPGREP_FALLBACK,
} from './constants.js';
import type {
ApiErrorEvent,
@@ -48,6 +49,7 @@ import type {
InvalidChunkEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
RipgrepFallbackEvent,
} from './types.js';
import {
recordApiErrorMetrics,
@@ -268,6 +270,28 @@ export function logFlashFallback(
logger.emit(logRecord);
}
export function logRipgrepFallback(
config: Config,
event: RipgrepFallbackEvent,
): void {
ClearcutLogger.getInstance(config)?.logRipgrepFallbackEvent();
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_RIPGREP_FALLBACK,
'event.timestamp': new Date().toISOString(),
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Switching to grep as fallback.`,
attributes,
};
logger.emit(logRecord);
}
export function logApiError(config: Config, event: ApiErrorEvent): void {
const uiEvent = {
...event,
+10
View File
@@ -281,6 +281,16 @@ export class FlashFallbackEvent implements BaseTelemetryEvent {
}
}
export class RipgrepFallbackEvent implements BaseTelemetryEvent {
'event.name': 'ripgrep_fallback';
'event.timestamp': string;
constructor(public error?: string) {
this['event.name'] = 'ripgrep_fallback';
this['event.timestamp'] = new Date().toISOString();
}
}
export enum LoopType {
CONSECUTIVE_IDENTICAL_TOOL_CALLS = 'consecutive_identical_tool_calls',
CHANTING_IDENTICAL_SENTENCES = 'chanting_identical_sentences',