mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 08:31:14 -07:00
feat(telemetry): Add telemetry for web_fetch fallback attempts (#10749)
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
ToolCallEvent,
|
||||
AgentStartEvent,
|
||||
AgentFinishEvent,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from '../types.js';
|
||||
import { AgentTerminateMode } from '../../agents/types.js';
|
||||
import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
|
||||
@@ -945,4 +946,21 @@ describe('ClearcutLogger', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logWebFetchFallbackAttemptEvent', () => {
|
||||
it('logs an event with the proper name and reason', () => {
|
||||
const { logger } = setup();
|
||||
const event = new WebFetchFallbackAttemptEvent('private_ip');
|
||||
|
||||
logger?.logWebFetchFallbackAttemptEvent(event);
|
||||
|
||||
const events = getEvents(logger!);
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0]).toHaveEventName(EventNames.WEB_FETCH_FALLBACK_ATTEMPT);
|
||||
expect(events[0]).toHaveMetadataValue([
|
||||
EventMetadataKey.GEMINI_CLI_WEB_FETCH_FALLBACK_REASON,
|
||||
'private_ip',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
SmartEditCorrectionEvent,
|
||||
AgentStartEvent,
|
||||
AgentFinishEvent,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from '../types.js';
|
||||
import { EventMetadataKey } from './event-metadata-key.js';
|
||||
import type { Config } from '../../config/config.js';
|
||||
@@ -83,6 +84,7 @@ export enum EventNames {
|
||||
SMART_EDIT_CORRECTION = 'smart_edit_correction',
|
||||
AGENT_START = 'agent_start',
|
||||
AGENT_FINISH = 'agent_finish',
|
||||
WEB_FETCH_FALLBACK_ATTEMPT = 'web_fetch_fallback_attempt',
|
||||
}
|
||||
|
||||
export interface LogResponse {
|
||||
@@ -1111,6 +1113,20 @@ export class ClearcutLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logWebFetchFallbackAttemptEvent(event: WebFetchFallbackAttemptEvent): void {
|
||||
const data: EventValue[] = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_WEB_FETCH_FALLBACK_REASON,
|
||||
value: event.reason,
|
||||
},
|
||||
];
|
||||
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.WEB_FETCH_FALLBACK_ATTEMPT, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds default fields to data, and returns a new data array. This fields
|
||||
* should exist on all log events.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// Defines valid event metadata keys for Clearcut logging.
|
||||
export enum EventMetadataKey {
|
||||
// Deleted enums: 24
|
||||
// Next ID: 117
|
||||
|
||||
GEMINI_CLI_KEY_UNKNOWN = 0,
|
||||
|
||||
@@ -98,6 +99,9 @@ export enum EventMetadataKey {
|
||||
// Logs a smart edit correction event.
|
||||
GEMINI_CLI_SMART_EDIT_CORRECTION = 110,
|
||||
|
||||
// Logs the reason for web fetch fallback.
|
||||
GEMINI_CLI_WEB_FETCH_FALLBACK_REASON = 116,
|
||||
|
||||
// ==========================================================================
|
||||
// GenAI API Request Event Keys
|
||||
// ===========================================================================
|
||||
|
||||
@@ -35,6 +35,8 @@ export const EVENT_MODEL_SLASH_COMMAND = 'gemini_cli.slash_command.model';
|
||||
export const EVENT_SMART_EDIT_STRATEGY = 'gemini_cli.smart_edit.strategy';
|
||||
export const EVENT_MODEL_ROUTING = 'gemini_cli.model_routing';
|
||||
export const EVENT_SMART_EDIT_CORRECTION = 'gemini_cli.smart_edit.correction';
|
||||
export const EVENT_WEB_FETCH_FALLBACK_ATTEMPT =
|
||||
'gemini_cli.web_fetch_fallback_attempt';
|
||||
|
||||
// Agent Events
|
||||
export const EVENT_AGENT_START = 'gemini_cli.agent.start';
|
||||
|
||||
@@ -44,6 +44,7 @@ export {
|
||||
logExtensionEnable,
|
||||
logExtensionInstallEvent,
|
||||
logExtensionUninstall,
|
||||
logWebFetchFallbackAttempt,
|
||||
} from './loggers.js';
|
||||
export type { SlashCommandEvent, ChatCompressionEvent } from './types.js';
|
||||
export {
|
||||
@@ -59,6 +60,7 @@ export {
|
||||
ConversationFinishedEvent,
|
||||
KittySequenceOverflowEvent,
|
||||
ToolOutputTruncatedEvent,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from './types.js';
|
||||
export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js';
|
||||
export type { TelemetryEvent } from './types.js';
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
EVENT_TOOL_OUTPUT_TRUNCATED,
|
||||
EVENT_AGENT_START,
|
||||
EVENT_AGENT_FINISH,
|
||||
EVENT_WEB_FETCH_FALLBACK_ATTEMPT,
|
||||
} from './constants.js';
|
||||
import {
|
||||
logApiRequest,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
logExtensionUninstall,
|
||||
logAgentStart,
|
||||
logAgentFinish,
|
||||
logWebFetchFallbackAttempt,
|
||||
} from './loggers.js';
|
||||
import { ToolCallDecision } from './tool-call-decision.js';
|
||||
import {
|
||||
@@ -81,6 +83,7 @@ import {
|
||||
ExtensionUninstallEvent,
|
||||
AgentStartEvent,
|
||||
AgentFinishEvent,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from './types.js';
|
||||
import * as metrics from './metrics.js';
|
||||
import {
|
||||
@@ -1494,4 +1497,36 @@ describe('loggers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logWebFetchFallbackAttempt', () => {
|
||||
const mockConfig = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(ClearcutLogger.prototype, 'logWebFetchFallbackAttemptEvent');
|
||||
});
|
||||
|
||||
it('should log web fetch fallback attempt event', () => {
|
||||
const event = new WebFetchFallbackAttemptEvent('private_ip');
|
||||
|
||||
logWebFetchFallbackAttempt(mockConfig, event);
|
||||
|
||||
expect(
|
||||
ClearcutLogger.prototype.logWebFetchFallbackAttemptEvent,
|
||||
).toHaveBeenCalledWith(event);
|
||||
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith({
|
||||
body: 'Web fetch fallback attempt. Reason: private_ip',
|
||||
attributes: {
|
||||
'session.id': 'test-session-id',
|
||||
'user.email': 'test-user@example.com',
|
||||
'event.name': EVENT_WEB_FETCH_FALLBACK_ATTEMPT,
|
||||
'event.timestamp': '2025-01-01T00:00:00.000Z',
|
||||
reason: 'private_ip',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
EVENT_SMART_EDIT_CORRECTION,
|
||||
EVENT_AGENT_START,
|
||||
EVENT_AGENT_FINISH,
|
||||
EVENT_WEB_FETCH_FALLBACK_ATTEMPT,
|
||||
} from './constants.js';
|
||||
import type {
|
||||
ApiErrorEvent,
|
||||
@@ -73,6 +74,7 @@ import type {
|
||||
SmartEditCorrectionEvent,
|
||||
AgentStartEvent,
|
||||
AgentFinishEvent,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from './types.js';
|
||||
import {
|
||||
recordApiErrorMetrics,
|
||||
@@ -863,7 +865,7 @@ export function logSmartEditCorrectionEvent(
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Smart Edit Tool Correction: ${event.correction}`,
|
||||
body: `Smart Edit Correction`,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
@@ -906,3 +908,24 @@ export function logAgentFinish(config: Config, event: AgentFinishEvent): void {
|
||||
|
||||
recordAgentRunMetrics(config, event);
|
||||
}
|
||||
|
||||
export function logWebFetchFallbackAttempt(
|
||||
config: Config,
|
||||
event: WebFetchFallbackAttemptEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logWebFetchFallbackAttemptEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
...event,
|
||||
'event.name': EVENT_WEB_FETCH_FALLBACK_ATTEMPT,
|
||||
};
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Web fetch fallback attempt. Reason: ${event.reason}`,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
@@ -690,7 +690,8 @@ export type TelemetryEvent =
|
||||
| ToolOutputTruncatedEvent
|
||||
| ModelSlashCommandEvent
|
||||
| AgentStartEvent
|
||||
| AgentFinishEvent;
|
||||
| AgentFinishEvent
|
||||
| WebFetchFallbackAttemptEvent;
|
||||
|
||||
export class ExtensionDisableEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'extension_disable';
|
||||
@@ -769,3 +770,15 @@ export class AgentFinishEvent implements BaseTelemetryEvent {
|
||||
this.terminate_reason = terminate_reason;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebFetchFallbackAttemptEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'web_fetch_fallback_attempt';
|
||||
'event.timestamp': string;
|
||||
reason: 'private_ip' | 'primary_failed';
|
||||
|
||||
constructor(reason: 'private_ip' | 'primary_failed') {
|
||||
this['event.name'] = 'web_fetch_fallback_attempt';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,21 @@ import { ApprovalMode } from '../config/config.js';
|
||||
import { ToolConfirmationOutcome } from './tools.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import * as fetchUtils from '../utils/fetch.js';
|
||||
import {
|
||||
logWebFetchFallbackAttempt,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from '../telemetry/index.js';
|
||||
|
||||
const mockGenerateContent = vi.fn();
|
||||
const mockGetGeminiClient = vi.fn(() => ({
|
||||
generateContent: mockGenerateContent,
|
||||
}));
|
||||
|
||||
vi.mock('../telemetry/index.js', () => ({
|
||||
logWebFetchFallbackAttempt: vi.fn(),
|
||||
WebFetchFallbackAttemptEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/fetch.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof fetchUtils>();
|
||||
return {
|
||||
@@ -70,6 +79,59 @@ describe('WebFetchTool', () => {
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.error?.type).toBe(ToolErrorType.WEB_FETCH_PROCESSING_ERROR);
|
||||
});
|
||||
|
||||
it('should log telemetry when falling back due to private IP', async () => {
|
||||
vi.spyOn(fetchUtils, 'isPrivateIp').mockReturnValue(true);
|
||||
// Mock fetchWithTimeout to succeed so fallback proceeds
|
||||
vi.spyOn(fetchUtils, 'fetchWithTimeout').mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('some content'),
|
||||
} as Response);
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: 'fallback response' }] } }],
|
||||
});
|
||||
|
||||
const tool = new WebFetchTool(mockConfig);
|
||||
const params = { prompt: 'fetch https://private.ip' };
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(logWebFetchFallbackAttempt).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
expect.any(WebFetchFallbackAttemptEvent),
|
||||
);
|
||||
expect(WebFetchFallbackAttemptEvent).toHaveBeenCalledWith('private_ip');
|
||||
});
|
||||
|
||||
it('should log telemetry when falling back due to primary fetch failure', async () => {
|
||||
vi.spyOn(fetchUtils, 'isPrivateIp').mockReturnValue(false);
|
||||
// Mock primary fetch to return empty response, triggering fallback
|
||||
mockGenerateContent.mockResolvedValueOnce({
|
||||
candidates: [],
|
||||
});
|
||||
// Mock fetchWithTimeout to succeed so fallback proceeds
|
||||
vi.spyOn(fetchUtils, 'fetchWithTimeout').mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('some content'),
|
||||
} as Response);
|
||||
// Mock fallback LLM call
|
||||
mockGenerateContent.mockResolvedValueOnce({
|
||||
candidates: [{ content: { parts: [{ text: 'fallback response' }] } }],
|
||||
});
|
||||
|
||||
const tool = new WebFetchTool(mockConfig);
|
||||
const params = { prompt: 'fetch https://public.ip' };
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(logWebFetchFallbackAttempt).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
expect.any(WebFetchFallbackAttemptEvent),
|
||||
);
|
||||
expect(WebFetchFallbackAttemptEvent).toHaveBeenCalledWith(
|
||||
'primary_failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldConfirmExecute', () => {
|
||||
|
||||
@@ -23,6 +23,10 @@ import { getResponseText } from '../utils/partUtils.js';
|
||||
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
|
||||
import { convert } from 'html-to-text';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
import {
|
||||
logWebFetchFallbackAttempt,
|
||||
WebFetchFallbackAttemptEvent,
|
||||
} from '../telemetry/index.js';
|
||||
|
||||
const URL_FETCH_TIMEOUT_MS = 10000;
|
||||
const MAX_CONTENT_LENGTH = 100000;
|
||||
@@ -184,6 +188,10 @@ ${textContent}
|
||||
const isPrivate = isPrivateIp(url);
|
||||
|
||||
if (isPrivate) {
|
||||
logWebFetchFallbackAttempt(
|
||||
this.config,
|
||||
new WebFetchFallbackAttemptEvent('private_ip'),
|
||||
);
|
||||
return this.executeFallback(signal);
|
||||
}
|
||||
|
||||
@@ -243,6 +251,10 @@ ${textContent}
|
||||
}
|
||||
|
||||
if (processingError) {
|
||||
logWebFetchFallbackAttempt(
|
||||
this.config,
|
||||
new WebFetchFallbackAttemptEvent('primary_failed'),
|
||||
);
|
||||
return this.executeFallback(signal);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user