Skip to content

Commit 4a00520

Browse files
jeropgemini-cli-robot
authored andcommitted
Create line change metrics (#12299)
1 parent a019826 commit 4a00520

5 files changed

Lines changed: 114 additions & 21 deletions

File tree

docs/cli/telemetry.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,10 +539,6 @@ Measures tool usage and latency.
539539
- `decision` (string: "accept", "reject", "modify", or "auto_accept", if
540540
applicable)
541541
- `tool_type` (string: "mcp" or "native", if applicable)
542-
- `model_added_lines` (Int, optional)
543-
- `model_removed_lines` (Int, optional)
544-
- `user_added_lines` (Int, optional)
545-
- `user_removed_lines` (Int, optional)
546542

547543
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
548544
- **Attributes**:
@@ -586,6 +582,12 @@ Counts file operations with basic context.
586582
- `extension` (string, optional)
587583
- `programming_language` (string, optional)
588584

585+
- `gemini_cli.lines.changed` (Counter, Int): Number of lines changed (from file
586+
diffs).
587+
- **Attributes**:
588+
- `function_name`
589+
- `type` ("added" or "removed")
590+
589591
##### Chat and Streaming
590592

591593
Resilience counters for compression, invalid chunks, and retries.

packages/core/src/telemetry/loggers.test.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,12 +786,16 @@ describe('loggers', () => {
786786

787787
const mockMetrics = {
788788
recordToolCallMetrics: vi.fn(),
789+
recordLinesChanged: vi.fn(),
789790
};
790791

791792
beforeEach(() => {
792793
vi.spyOn(metrics, 'recordToolCallMetrics').mockImplementation(
793794
mockMetrics.recordToolCallMetrics,
794795
);
796+
vi.spyOn(metrics, 'recordLinesChanged').mockImplementation(
797+
mockMetrics.recordLinesChanged,
798+
);
795799
mockLogger.emit.mockReset();
796800
});
797801

@@ -888,10 +892,6 @@ describe('loggers', () => {
888892
success: true,
889893
decision: ToolCallDecision.ACCEPT,
890894
tool_type: 'native',
891-
model_added_lines: 1,
892-
model_removed_lines: 2,
893-
user_added_lines: 5,
894-
user_removed_lines: 6,
895895
},
896896
);
897897

@@ -900,6 +900,19 @@ describe('loggers', () => {
900900
'event.name': EVENT_TOOL_CALL,
901901
'event.timestamp': '2025-01-01T00:00:00.000Z',
902902
});
903+
904+
expect(mockMetrics.recordLinesChanged).toHaveBeenCalledWith(
905+
mockConfig,
906+
1,
907+
'added',
908+
{ function_name: 'test-function' },
909+
);
910+
expect(mockMetrics.recordLinesChanged).toHaveBeenCalledWith(
911+
mockConfig,
912+
2,
913+
'removed',
914+
{ function_name: 'test-function' },
915+
);
903916
});
904917
it('should log a tool call with a reject decision', () => {
905918
const call: ErroredToolCall = {

packages/core/src/telemetry/loggers.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
recordTokenUsageMetrics,
6464
recordApiResponseMetrics,
6565
recordAgentRunMetrics,
66+
recordLinesChanged,
6667
} from './metrics.js';
6768
import { isTelemetrySdkInitialized } from './sdk.js';
6869
import type { UiEvent } from './uiTelemetry.js';
@@ -118,15 +119,22 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
118119
success: event.success,
119120
decision: event.decision,
120121
tool_type: event.tool_type,
121-
...(event.metadata
122-
? {
123-
model_added_lines: event.metadata['model_added_lines'],
124-
model_removed_lines: event.metadata['model_removed_lines'],
125-
user_added_lines: event.metadata['user_added_lines'],
126-
user_removed_lines: event.metadata['user_removed_lines'],
127-
}
128-
: {}),
129122
});
123+
124+
if (event.metadata) {
125+
const added = event.metadata['model_added_lines'];
126+
if (typeof added === 'number' && added > 0) {
127+
recordLinesChanged(config, added, 'added', {
128+
function_name: event.function_name,
129+
});
130+
}
131+
const removed = event.metadata['model_removed_lines'];
132+
if (typeof removed === 'number' && removed > 0) {
133+
recordLinesChanged(config, removed, 'removed', {
134+
function_name: event.function_name,
135+
});
136+
}
137+
}
130138
}
131139

132140
export function logToolOutputTruncated(

packages/core/src/telemetry/metrics.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ describe('Telemetry Metrics', () => {
9494
let recordFlickerFrameModule: typeof import('./metrics.js').recordFlickerFrame;
9595
let recordExitFailModule: typeof import('./metrics.js').recordExitFail;
9696
let recordAgentRunMetricsModule: typeof import('./metrics.js').recordAgentRunMetrics;
97+
let recordLinesChangedModule: typeof import('./metrics.js').recordLinesChanged;
9798

9899
beforeEach(async () => {
99100
vi.resetModules();
@@ -136,6 +137,7 @@ describe('Telemetry Metrics', () => {
136137
recordFlickerFrameModule = metricsJsModule.recordFlickerFrame;
137138
recordExitFailModule = metricsJsModule.recordExitFail;
138139
recordAgentRunMetricsModule = metricsJsModule.recordAgentRunMetrics;
140+
recordLinesChangedModule = metricsJsModule.recordLinesChanged;
139141

140142
const otelApiModule = await import('@opentelemetry/api');
141143

@@ -348,6 +350,53 @@ describe('Telemetry Metrics', () => {
348350
});
349351
});
350352

353+
describe('recordLinesChanged metric', () => {
354+
const mockConfig = {
355+
getSessionId: () => 'test-session-id',
356+
getTelemetryEnabled: () => true,
357+
} as unknown as Config;
358+
359+
it('should not record lines added/removed if not initialized', () => {
360+
recordLinesChangedModule(mockConfig, 10, 'added', {
361+
function_name: 'fn',
362+
});
363+
recordLinesChangedModule(mockConfig, 5, 'removed', {
364+
function_name: 'fn',
365+
});
366+
expect(mockCounterAddFn).not.toHaveBeenCalled();
367+
});
368+
369+
it('should record lines added with function_name after initialization', () => {
370+
initializeMetricsModule(mockConfig);
371+
mockCounterAddFn.mockClear();
372+
recordLinesChangedModule(mockConfig, 10, 'added', {
373+
function_name: 'my-fn',
374+
});
375+
expect(mockCounterAddFn).toHaveBeenCalledWith(10, {
376+
'session.id': 'test-session-id',
377+
'installation.id': 'test-installation-id',
378+
'user.email': 'test@example.com',
379+
type: 'added',
380+
function_name: 'my-fn',
381+
});
382+
});
383+
384+
it('should record lines removed with function_name after initialization', () => {
385+
initializeMetricsModule(mockConfig);
386+
mockCounterAddFn.mockClear();
387+
recordLinesChangedModule(mockConfig, 7, 'removed', {
388+
function_name: 'my-fn',
389+
});
390+
expect(mockCounterAddFn).toHaveBeenCalledWith(7, {
391+
'session.id': 'test-session-id',
392+
'installation.id': 'test-installation-id',
393+
'user.email': 'test@example.com',
394+
type: 'removed',
395+
function_name: 'my-fn',
396+
});
397+
});
398+
});
399+
351400
describe('recordFileOperationMetric', () => {
352401
const mockConfig = {
353402
getSessionId: () => 'test-session-id',

packages/core/src/telemetry/metrics.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const API_REQUEST_LATENCY = 'gemini_cli.api.request.latency';
2424
const TOKEN_USAGE = 'gemini_cli.token.usage';
2525
const SESSION_COUNT = 'gemini_cli.session.count';
2626
const FILE_OPERATION_COUNT = 'gemini_cli.file.operation.count';
27+
const LINES_CHANGED = 'gemini_cli.lines.changed';
2728
const INVALID_CHUNK_COUNT = 'gemini_cli.chat.invalid_chunk.count';
2829
const CONTENT_RETRY_COUNT = 'gemini_cli.chat.content_retry.count';
2930
const CONTENT_RETRY_FAILURE_COUNT =
@@ -72,11 +73,6 @@ const COUNTER_DEFINITIONS = {
7273
success: boolean;
7374
decision?: 'accept' | 'reject' | 'modify' | 'auto_accept';
7475
tool_type?: 'native' | 'mcp';
75-
// Optional diff statistics for file-modifying tools
76-
model_added_lines?: number;
77-
model_removed_lines?: number;
78-
user_added_lines?: number;
79-
user_removed_lines?: number;
8076
},
8177
},
8278
[API_REQUEST_COUNT]: {
@@ -116,6 +112,15 @@ const COUNTER_DEFINITIONS = {
116112
programming_language?: string;
117113
},
118114
},
115+
[LINES_CHANGED]: {
116+
description: 'Number of lines changed (from file diffs).',
117+
valueType: ValueType.INT,
118+
assign: (c: Counter) => (linesChangedCounter = c),
119+
attributes: {} as {
120+
function_name?: string;
121+
type: 'added' | 'removed';
122+
},
123+
},
119124
[INVALID_CHUNK_COUNT]: {
120125
description: 'Counts invalid chunks received from a stream.',
121126
valueType: ValueType.INT,
@@ -454,6 +459,7 @@ let apiRequestLatencyHistogram: Histogram | undefined;
454459
let tokenUsageCounter: Counter | undefined;
455460
let sessionCounter: Counter | undefined;
456461
let fileOperationCounter: Counter | undefined;
462+
let linesChangedCounter: Counter | undefined;
457463
let chatCompressionCounter: Counter | undefined;
458464
let invalidChunkCounter: Counter | undefined;
459465
let contentRetryCounter: Counter | undefined;
@@ -621,6 +627,21 @@ export function recordFileOperationMetric(
621627
});
622628
}
623629

630+
export function recordLinesChanged(
631+
config: Config,
632+
lines: number,
633+
changeType: 'added' | 'removed',
634+
attributes?: { function_name?: string },
635+
): void {
636+
if (!linesChangedCounter || !isMetricsInitialized) return;
637+
if (!Number.isFinite(lines) || lines <= 0) return;
638+
linesChangedCounter.add(lines, {
639+
...baseMetricDefinition.getCommonAttributes(config),
640+
type: changeType,
641+
...(attributes ?? {}),
642+
});
643+
}
644+
624645
// --- New Metric Recording Functions ---
625646

626647
/**

0 commit comments

Comments
 (0)