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
This commit is contained in:
Srinath Padmanabhan
2026-03-11 12:21:45 -07:00
parent 0b2c582d6b
commit df1a6996e0
6 changed files with 121 additions and 18 deletions

View File

@@ -26,10 +26,7 @@ import {
clearOauthClientCache,
authEvents,
} from './oauth2.js';
import {
recordGoogleAuthStart,
recordGoogleAuthEnd,
} from '../telemetry/metrics.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';
@@ -109,9 +106,9 @@ vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({
})),
}));
vi.mock('../telemetry/metrics.js', () => ({
recordGoogleAuthStart: vi.fn(),
recordGoogleAuthEnd: vi.fn(),
vi.mock('../telemetry/loggers.js', () => ({
logGoogleAuthStart: vi.fn(),
logGoogleAuthEnd: vi.fn(),
}));
const mockConfig = {
@@ -1415,8 +1412,14 @@ describe('oauth2', () => {
await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
expect(recordGoogleAuthStart).toHaveBeenCalledWith(mockConfig);
expect(recordGoogleAuthEnd).toHaveBeenCalledWith(mockConfig);
expect(logGoogleAuthStart).toHaveBeenCalledWith(
mockConfig,
expect.any(Object),
);
expect(logGoogleAuthEnd).toHaveBeenCalledWith(
mockConfig,
expect.any(Object),
);
});
it('should NOT record onboarding events for other auth types', async () => {

View File

@@ -21,10 +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 {
recordGoogleAuthStart,
recordGoogleAuthEnd,
} from '../telemetry/metrics.js';
GoogleAuthStartEvent,
GoogleAuthEndEvent,
} from '../telemetry/types.js';
import type { Config } from '../config/config.js';
import {
getErrorMessage,
@@ -117,9 +118,9 @@ async function initOauthClient(
authType: AuthType,
config: Config,
): Promise<AuthClient> {
const recordGoogleAuthEndIfApplicable = () => {
const logGoogleAuthEndIfApplicable = () => {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
recordGoogleAuthEnd(config);
logGoogleAuthEnd(config, new GoogleAuthEndEvent());
}
};
@@ -154,7 +155,7 @@ async function initOauthClient(
});
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
recordGoogleAuthStart(config);
logGoogleAuthStart(config, new GoogleAuthStartEvent());
}
const useEncryptedStorage = getUseEncryptedStorageFlag();
@@ -203,7 +204,7 @@ async function initOauthClient(
debugLogger.log('Loaded cached credentials.');
await triggerPostAuthCallbacks(credentials as Credentials);
recordGoogleAuthEndIfApplicable();
logGoogleAuthEndIfApplicable();
return client;
}
} catch (error) {
@@ -295,7 +296,7 @@ async function initOauthClient(
}
await triggerPostAuthCallbacks(client.credentials);
recordGoogleAuthEndIfApplicable();
logGoogleAuthEndIfApplicable();
} else {
// In ACP mode, we skip the interactive consent and directly open the browser
if (!config.getAcpMode()) {
@@ -402,7 +403,7 @@ async function initOauthClient(
});
await triggerPostAuthCallbacks(client.credentials);
recordGoogleAuthEndIfApplicable();
logGoogleAuthEndIfApplicable();
}
return client;

View File

@@ -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[] = [
{

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,

View File

@@ -1397,6 +1397,7 @@ export function recordTokenStorageInitialization(
tokenStorageTypeCounter.add(1, {
...baseMetricDefinition.getCommonAttributes(config),
forced: event.forced,
type: event.type,
});
}

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 {