Compare commits

...

12 Commits

Author SHA1 Message Date
Srinath Padmanabhan 954dadc02a feat(telemetry): migrate Google Auth events to event-based logging and fix token storage metric
- Add GoogleAuthStartEvent and GoogleAuthEndEvent types
- Implement logGoogleAuthStart and logGoogleAuthEnd in telemetry loggers
- Update ClearcutLogger to support Google Auth events
- Update OAuth2 logic to use the new logging functions
- Fix missing 'type' attribute in recordTokenStorageInitialization metric
2026-03-12 07:43:58 -07:00
Srinath Padmanabhan d4356e6732 Merge branch 'main' into feat/onboarding-telemetry 2026-03-06 06:43:30 -08:00
Srinath Padmanabhan 046d2b4d8e Merge branch 'main' into feat/onboarding-telemetry 2026-03-05 22:12:47 -08:00
Srinath Padmanabhan bc84fcbb02 update docs for telemetry events emitted 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 0178e1ef00 add test for non-GCP Auth types 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan bf88858e33 fixing test error 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 2bd1d45f08 inlining recordGoogleAuthEnd 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 192b1a630f Fixing review comments 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 79dd2bb535 test(telemetry): clean up comments in onboarding test 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 410df6b404 fix(telemetry): record onboarding end for cached credentials success 2026-03-05 22:11:16 -08:00
Srinath Padmanabhan f246cd0754 fix(telemetry): correct onboarding and token storage metrics
This commit addresses two issues with telemetry recording:

1.  The  metric was missing the  attribute, causing a TypeScript error when  was called. The  attribute has been added back to the metric definition.

2.  The  metric was being incorrectly called on every token refresh. This has been corrected by removing the calls from the token refresh listener and refactoring the valid success paths to call a new  helper function to avoid code duplication.
2026-03-05 22:11:16 -08:00
Srinath Padmanabhan 0e4d11db30 feat(telemetry): add onboarding start and end metrics for login with google 2026-03-05 22:11:16 -08:00
7 changed files with 230 additions and 1 deletions
+11
View File
@@ -32,6 +32,7 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI.
- [Metrics](#metrics)
- [Custom](#custom)
- [Sessions](#sessions-1)
- [Authentication](#authentication)
- [Tools](#tools-1)
- [API](#api-1)
- [Token usage](#token-usage)
@@ -650,6 +651,16 @@ Counts CLI sessions at startup.
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
##### Authentication
Tracks "Login with Google" OAuth flow initiation and completion.
- `gemini_cli.google_auth.start` (Counter, Int): Incremented when the "Login
with Google" OAuth flow begins.
- `gemini_cli.google_auth.end` (Counter, Int): Incremented when the "Login with
Google" OAuth flow completes successfully.
##### Tools
Measures tool usage and latency.
@@ -26,6 +26,7 @@ import {
clearOauthClientCache,
authEvents,
} from './oauth2.js';
import { logGoogleAuthStart, logGoogleAuthEnd } from '../telemetry/loggers.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -105,6 +106,11 @@ vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({
})),
}));
vi.mock('../telemetry/loggers.js', () => ({
logGoogleAuthStart: vi.fn(),
logGoogleAuthEnd: vi.fn(),
}));
const mockConfig = {
getNoBrowser: () => false,
getProxy: () => 'http://test.proxy.com:8080',
@@ -1385,6 +1391,62 @@ describe('oauth2', () => {
});
});
describe('onboarding telemetry', () => {
it('should record onboarding start and end events for LOGIN_WITH_GOOGLE', async () => {
const mockOAuth2Client = {
setCredentials: vi.fn(),
getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }),
getTokenInfo: vi.fn().mockResolvedValue({}),
on: vi.fn(),
} as unknown as OAuth2Client;
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
const cachedCreds = { refresh_token: 'test-token' };
const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
expect(logGoogleAuthStart).toHaveBeenCalledWith(
mockConfig,
expect.any(Object),
);
expect(logGoogleAuthEnd).toHaveBeenCalledWith(
mockConfig,
expect.any(Object),
);
});
it('should NOT record google auth events for non-LOGIN_WITH_GOOGLE auth types', async () => {
const mockOAuth2Client = {
setCredentials: vi.fn(),
getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }),
getTokenInfo: vi.fn().mockResolvedValue({}),
on: vi.fn(),
} as unknown as OAuth2Client;
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
const cachedCreds = { refresh_token: 'test-token' };
const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
expect(recordGoogleAuthStart).not.toHaveBeenCalled();
expect(recordGoogleAuthEnd).not.toHaveBeenCalled();
});
});
describe('clearCachedCredentialFile', () => {
it('should clear cached credentials and Google account', async () => {
const cachedCreds = { refresh_token: 'test-token' };
+22
View File
@@ -21,6 +21,11 @@ import { EventEmitter } from 'node:events';
import open from 'open';
import path from 'node:path';
import { promises as fs } from 'node:fs';
import { logGoogleAuthStart, logGoogleAuthEnd } from '../telemetry/loggers.js';
import {
GoogleAuthStartEvent,
GoogleAuthEndEvent,
} from '../telemetry/types.js';
import type { Config } from '../config/config.js';
import {
getErrorMessage,
@@ -113,6 +118,12 @@ async function initOauthClient(
authType: AuthType,
config: Config,
): Promise<AuthClient> {
const logGoogleAuthEndIfApplicable = () => {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
logGoogleAuthEnd(config, new GoogleAuthEndEvent());
}
};
const credentials = await fetchCachedCredentials();
if (
@@ -142,6 +153,11 @@ async function initOauthClient(
proxy: config.getProxy(),
},
});
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
logGoogleAuthStart(config, new GoogleAuthStartEvent());
}
const useEncryptedStorage = getUseEncryptedStorageFlag();
if (
@@ -152,6 +168,9 @@ async function initOauthClient(
access_token: process.env['GOOGLE_CLOUD_ACCESS_TOKEN'],
});
await fetchAndCacheUserInfo(client);
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
recordGoogleAuthEnd(config);
}
return client;
}
@@ -188,6 +207,7 @@ async function initOauthClient(
debugLogger.log('Loaded cached credentials.');
await triggerPostAuthCallbacks(credentials as Credentials);
logGoogleAuthEndIfApplicable();
return client;
}
} catch (error) {
@@ -279,6 +299,7 @@ async function initOauthClient(
}
await triggerPostAuthCallbacks(client.credentials);
logGoogleAuthEndIfApplicable();
} else {
// In ACP mode, we skip the interactive consent and directly open the browser
if (!config.getAcpMode()) {
@@ -385,6 +406,7 @@ async function initOauthClient(
});
await triggerPostAuthCallbacks(client.credentials);
logGoogleAuthEndIfApplicable();
}
return client;
@@ -50,6 +50,8 @@ import type {
KeychainAvailabilityEvent,
TokenStorageInitializationEvent,
StartupStatsEvent,
GoogleAuthStartEvent,
GoogleAuthEndEvent,
} from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js';
import type { Config } from '../../config/config.js';
@@ -116,6 +118,8 @@ export enum EventNames {
TOOL_OUTPUT_MASKING = 'tool_output_masking',
KEYCHAIN_AVAILABILITY = 'keychain_availability',
TOKEN_STORAGE_INITIALIZATION = 'token_storage_initialization',
GOOGLE_AUTH_START = 'google_auth_start',
GOOGLE_AUTH_END = 'google_auth_end',
CONSECA_POLICY_GENERATION = 'conseca_policy_generation',
CONSECA_VERDICT = 'conseca_verdict',
STARTUP_STATS = 'startup_stats',
@@ -1693,6 +1697,16 @@ export class ClearcutLogger {
this.flushIfNeeded();
}
logGoogleAuthStartEvent(_event: GoogleAuthStartEvent): void {
this.enqueueLogEvent(this.createLogEvent(EventNames.GOOGLE_AUTH_START, []));
this.flushIfNeeded();
}
logGoogleAuthEndEvent(_event: GoogleAuthEndEvent): void {
this.enqueueLogEvent(this.createLogEvent(EventNames.GOOGLE_AUTH_END, []));
this.flushIfNeeded();
}
logStartupStatsEvent(event: StartupStatsEvent): void {
const data: EventValue[] = [
{
+38
View File
@@ -56,6 +56,8 @@ import {
type ToolOutputMaskingEvent,
type KeychainAvailabilityEvent,
type TokenStorageInitializationEvent,
type GoogleAuthStartEvent,
type GoogleAuthEndEvent,
} from './types.js';
import {
recordApiErrorMetrics,
@@ -77,6 +79,8 @@ import {
recordKeychainAvailability,
recordTokenStorageInitialization,
recordInvalidChunk,
recordGoogleAuthStart,
recordGoogleAuthEnd,
} from './metrics.js';
import { bufferTelemetryEvent } from './sdk.js';
import { uiTelemetryService, type UiEvent } from './uiTelemetry.js';
@@ -844,6 +848,40 @@ export function logTokenStorageInitialization(
});
}
export function logGoogleAuthStart(
config: Config,
event: GoogleAuthStartEvent,
): void {
ClearcutLogger.getInstance(config)?.logGoogleAuthStartEvent(event);
bufferTelemetryEvent(() => {
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: event.toLogBody(),
attributes: event.toOpenTelemetryAttributes(config),
};
logger.emit(logRecord);
recordGoogleAuthStart(config);
});
}
export function logGoogleAuthEnd(
config: Config,
event: GoogleAuthEndEvent,
): void {
ClearcutLogger.getInstance(config)?.logGoogleAuthEndEvent(event);
bufferTelemetryEvent(() => {
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: event.toLogBody(),
attributes: event.toOpenTelemetryAttributes(config),
};
logger.emit(logRecord);
recordGoogleAuthEnd(config);
});
}
export function logBillingEvent(
config: Config,
event: BillingTelemetryEvent,
+37 -1
View File
@@ -50,6 +50,8 @@ const KEYCHAIN_AVAILABILITY_COUNT = 'gemini_cli.keychain.availability.count';
const TOKEN_STORAGE_TYPE_COUNT = 'gemini_cli.token_storage.type.count';
const OVERAGE_OPTION_COUNT = 'gemini_cli.overage_option.count';
const CREDIT_PURCHASE_COUNT = 'gemini_cli.credit_purchase.count';
const EVENT_GOOGLE_AUTH_START = 'gemini_cli.google_auth.start';
const EVENT_GOOGLE_AUTH_END = 'gemini_cli.google_auth.end';
// Agent Metrics
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
@@ -288,6 +290,18 @@ const COUNTER_DEFINITIONS = {
model: string;
},
},
[EVENT_GOOGLE_AUTH_START]: {
description: 'Counts auth "Login with Google" started.',
valueType: ValueType.INT,
assign: (c: Counter) => (googleAuthStartCounter = c),
attributes: {} as Record<string, never>,
},
[EVENT_GOOGLE_AUTH_END]: {
description: 'Counts auth "Login with Google" succeeded.',
valueType: ValueType.INT,
assign: (c: Counter) => (googleAuthEndCounter = c),
attributes: {} as Record<string, never>,
},
} as const;
const HISTOGRAM_DEFINITIONS = {
@@ -536,7 +550,7 @@ const PERFORMANCE_HISTOGRAM_DEFINITIONS = {
},
} as const;
type AllMetricDefs = typeof COUNTER_DEFINITIONS &
export type AllMetricDefs = typeof COUNTER_DEFINITIONS &
typeof HISTOGRAM_DEFINITIONS &
typeof PERFORMANCE_COUNTER_DEFINITIONS &
typeof PERFORMANCE_HISTOGRAM_DEFINITIONS;
@@ -628,6 +642,8 @@ let keychainAvailabilityCounter: Counter | undefined;
let tokenStorageTypeCounter: Counter | undefined;
let overageOptionCounter: Counter | undefined;
let creditPurchaseCounter: Counter | undefined;
let googleAuthStartCounter: Counter | undefined;
let googleAuthEndCounter: Counter | undefined;
// OpenTelemetry GenAI Semantic Convention Metrics
let genAiClientTokenUsageHistogram: Histogram | undefined;
@@ -800,6 +816,25 @@ export function recordLinesChanged(
// --- New Metric Recording Functions ---
/**
* Records a metric for when the Google auth process starts.
*/
export function recordGoogleAuthStart(config: Config): void {
if (!googleAuthStartCounter || !isMetricsInitialized) return;
googleAuthStartCounter.add(
1,
baseMetricDefinition.getCommonAttributes(config),
);
}
/**
* Records a metric for when the Google auth process ends successfully.
*/
export function recordGoogleAuthEnd(config: Config): void {
if (!googleAuthEndCounter || !isMetricsInitialized) return;
googleAuthEndCounter.add(1, baseMetricDefinition.getCommonAttributes(config));
}
/**
* Records a metric for when a UI frame flickers.
*/
@@ -1363,6 +1398,7 @@ export function recordTokenStorageInitialization(
...baseMetricDefinition.getCommonAttributes(config),
type: event.type,
forced: event.forced,
type: event.type,
});
}
+46
View File
@@ -2307,6 +2307,52 @@ export class KeychainAvailabilityEvent implements BaseTelemetryEvent {
}
}
export const EVENT_GOOGLE_AUTH_START = 'gemini_cli.google_auth.start';
export class GoogleAuthStartEvent implements BaseTelemetryEvent {
'event.name': 'google_auth_start';
'event.timestamp': string;
constructor() {
this['event.name'] = 'google_auth_start';
this['event.timestamp'] = new Date().toISOString();
}
toOpenTelemetryAttributes(config: Config): LogAttributes {
return {
...getCommonAttributes(config),
'event.name': EVENT_GOOGLE_AUTH_START,
'event.timestamp': this['event.timestamp'],
};
}
toLogBody(): string {
return 'Google auth started.';
}
}
export const EVENT_GOOGLE_AUTH_END = 'gemini_cli.google_auth.end';
export class GoogleAuthEndEvent implements BaseTelemetryEvent {
'event.name': 'google_auth_end';
'event.timestamp': string;
constructor() {
this['event.name'] = 'google_auth_end';
this['event.timestamp'] = new Date().toISOString();
}
toOpenTelemetryAttributes(config: Config): LogAttributes {
return {
...getCommonAttributes(config),
'event.name': EVENT_GOOGLE_AUTH_END,
'event.timestamp': this['event.timestamp'],
};
}
toLogBody(): string {
return 'Google auth succeeded.';
}
}
export const EVENT_TOKEN_STORAGE_INITIALIZATION =
'gemini_cli.token_storage.initialization';
export class TokenStorageInitializationEvent implements BaseTelemetryEvent {