mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-30 07:51:07 -07:00
feat(billing): implement G1 AI credits overage flow with billing telemetry (#18590)
This commit is contained in:
206
packages/core/src/telemetry/billingEvents.test.ts
Normal file
206
packages/core/src/telemetry/billingEvents.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import {
|
||||
OverageMenuShownEvent,
|
||||
OverageOptionSelectedEvent,
|
||||
EmptyWalletMenuShownEvent,
|
||||
CreditPurchaseClickEvent,
|
||||
CreditsUsedEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
EVENT_OVERAGE_MENU_SHOWN,
|
||||
EVENT_OVERAGE_OPTION_SELECTED,
|
||||
EVENT_EMPTY_WALLET_MENU_SHOWN,
|
||||
EVENT_CREDIT_PURCHASE_CLICK,
|
||||
EVENT_CREDITS_USED,
|
||||
EVENT_API_KEY_UPDATED,
|
||||
} from './billingEvents.js';
|
||||
|
||||
describe('billingEvents', () => {
|
||||
const fakeConfig = makeFakeConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-15T10:30:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('OverageMenuShownEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new OverageMenuShownEvent(
|
||||
'gemini-3-pro-preview',
|
||||
500,
|
||||
'ask',
|
||||
);
|
||||
expect(event['event.name']).toBe('overage_menu_shown');
|
||||
expect(event.model).toBe('gemini-3-pro-preview');
|
||||
expect(event.credit_balance).toBe(500);
|
||||
expect(event.overage_strategy).toBe('ask');
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new OverageMenuShownEvent(
|
||||
'gemini-3-pro-preview',
|
||||
500,
|
||||
'ask',
|
||||
);
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_OVERAGE_MENU_SHOWN);
|
||||
expect(attrs['model']).toBe('gemini-3-pro-preview');
|
||||
expect(attrs['credit_balance']).toBe(500);
|
||||
expect(attrs['overage_strategy']).toBe('ask');
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new OverageMenuShownEvent(
|
||||
'gemini-3-pro-preview',
|
||||
500,
|
||||
'ask',
|
||||
);
|
||||
expect(event.toLogBody()).toContain('gemini-3-pro-preview');
|
||||
expect(event.toLogBody()).toContain('500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OverageOptionSelectedEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new OverageOptionSelectedEvent(
|
||||
'gemini-3-pro-preview',
|
||||
'use_credits',
|
||||
100,
|
||||
);
|
||||
expect(event['event.name']).toBe('overage_option_selected');
|
||||
expect(event.selected_option).toBe('use_credits');
|
||||
expect(event.credit_balance).toBe(100);
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new OverageOptionSelectedEvent(
|
||||
'gemini-3-pro-preview',
|
||||
'use_fallback',
|
||||
200,
|
||||
);
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_OVERAGE_OPTION_SELECTED);
|
||||
expect(attrs['selected_option']).toBe('use_fallback');
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new OverageOptionSelectedEvent(
|
||||
'gemini-3-pro-preview',
|
||||
'manage',
|
||||
100,
|
||||
);
|
||||
expect(event.toLogBody()).toContain('manage');
|
||||
expect(event.toLogBody()).toContain('gemini-3-pro-preview');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EmptyWalletMenuShownEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new EmptyWalletMenuShownEvent('gemini-3-pro-preview');
|
||||
expect(event['event.name']).toBe('empty_wallet_menu_shown');
|
||||
expect(event.model).toBe('gemini-3-pro-preview');
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new EmptyWalletMenuShownEvent('gemini-3-pro-preview');
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_EMPTY_WALLET_MENU_SHOWN);
|
||||
expect(attrs['model']).toBe('gemini-3-pro-preview');
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new EmptyWalletMenuShownEvent('gemini-3-pro-preview');
|
||||
expect(event.toLogBody()).toContain('gemini-3-pro-preview');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreditPurchaseClickEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new CreditPurchaseClickEvent(
|
||||
'empty_wallet_menu',
|
||||
'gemini-3-pro-preview',
|
||||
);
|
||||
expect(event['event.name']).toBe('credit_purchase_click');
|
||||
expect(event.source).toBe('empty_wallet_menu');
|
||||
expect(event.model).toBe('gemini-3-pro-preview');
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new CreditPurchaseClickEvent(
|
||||
'overage_menu',
|
||||
'gemini-3-pro-preview',
|
||||
);
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_CREDIT_PURCHASE_CLICK);
|
||||
expect(attrs['source']).toBe('overage_menu');
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new CreditPurchaseClickEvent(
|
||||
'manage',
|
||||
'gemini-3-pro-preview',
|
||||
);
|
||||
expect(event.toLogBody()).toContain('manage');
|
||||
expect(event.toLogBody()).toContain('gemini-3-pro-preview');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreditsUsedEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new CreditsUsedEvent('gemini-3-pro-preview', 10, 490);
|
||||
expect(event['event.name']).toBe('credits_used');
|
||||
expect(event.credits_consumed).toBe(10);
|
||||
expect(event.credits_remaining).toBe(490);
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new CreditsUsedEvent('gemini-3-pro-preview', 10, 490);
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_CREDITS_USED);
|
||||
expect(attrs['credits_consumed']).toBe(10);
|
||||
expect(attrs['credits_remaining']).toBe(490);
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new CreditsUsedEvent('gemini-3-pro-preview', 10, 490);
|
||||
const body = event.toLogBody();
|
||||
expect(body).toContain('10');
|
||||
expect(body).toContain('490');
|
||||
expect(body).toContain('gemini-3-pro-preview');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApiKeyUpdatedEvent', () => {
|
||||
it('should construct with correct properties', () => {
|
||||
const event = new ApiKeyUpdatedEvent('google_login', 'api_key');
|
||||
expect(event['event.name']).toBe('api_key_updated');
|
||||
expect(event.previous_auth_type).toBe('google_login');
|
||||
expect(event.new_auth_type).toBe('api_key');
|
||||
});
|
||||
|
||||
it('should produce correct OpenTelemetry attributes', () => {
|
||||
const event = new ApiKeyUpdatedEvent('google_login', 'api_key');
|
||||
const attrs = event.toOpenTelemetryAttributes(fakeConfig);
|
||||
expect(attrs['event.name']).toBe(EVENT_API_KEY_UPDATED);
|
||||
expect(attrs['previous_auth_type']).toBe('google_login');
|
||||
expect(attrs['new_auth_type']).toBe('api_key');
|
||||
});
|
||||
|
||||
it('should produce a human-readable log body', () => {
|
||||
const event = new ApiKeyUpdatedEvent('google_login', 'api_key');
|
||||
const body = event.toLogBody();
|
||||
expect(body).toContain('google_login');
|
||||
expect(body).toContain('api_key');
|
||||
});
|
||||
});
|
||||
});
|
||||
255
packages/core/src/telemetry/billingEvents.ts
Normal file
255
packages/core/src/telemetry/billingEvents.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { LogAttributes } from '@opentelemetry/api-logs';
|
||||
import type { BaseTelemetryEvent } from './types.js';
|
||||
import { getCommonAttributes } from './telemetryAttributes.js';
|
||||
import type { OverageStrategy } from '../billing/billing.js';
|
||||
|
||||
/** Overage menu option that can be selected by the user */
|
||||
export type OverageOption =
|
||||
| 'use_credits'
|
||||
| 'use_fallback'
|
||||
| 'manage'
|
||||
| 'stop'
|
||||
| 'get_credits';
|
||||
|
||||
// ============================================================================
|
||||
// Event: Overage Menu Shown
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_OVERAGE_MENU_SHOWN = 'gemini_cli.overage_menu_shown';
|
||||
|
||||
export class OverageMenuShownEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'overage_menu_shown';
|
||||
'event.timestamp': string;
|
||||
model: string;
|
||||
credit_balance: number;
|
||||
overage_strategy: OverageStrategy;
|
||||
|
||||
constructor(
|
||||
model: string,
|
||||
creditBalance: number,
|
||||
overageStrategy: OverageStrategy,
|
||||
) {
|
||||
this['event.name'] = 'overage_menu_shown';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.model = model;
|
||||
this.credit_balance = creditBalance;
|
||||
this.overage_strategy = overageStrategy;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_OVERAGE_MENU_SHOWN,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
model: this.model,
|
||||
credit_balance: this.credit_balance,
|
||||
overage_strategy: this.overage_strategy,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Overage menu shown for model ${this.model} with ${this.credit_balance} credits available.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event: Overage Option Selected
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_OVERAGE_OPTION_SELECTED =
|
||||
'gemini_cli.overage_option_selected';
|
||||
|
||||
export class OverageOptionSelectedEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'overage_option_selected';
|
||||
'event.timestamp': string;
|
||||
model: string;
|
||||
selected_option: OverageOption;
|
||||
credit_balance: number;
|
||||
|
||||
constructor(
|
||||
model: string,
|
||||
selectedOption: OverageOption,
|
||||
creditBalance: number,
|
||||
) {
|
||||
this['event.name'] = 'overage_option_selected';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.model = model;
|
||||
this.selected_option = selectedOption;
|
||||
this.credit_balance = creditBalance;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_OVERAGE_OPTION_SELECTED,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
model: this.model,
|
||||
selected_option: this.selected_option,
|
||||
credit_balance: this.credit_balance,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Overage option '${this.selected_option}' selected for model ${this.model}.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event: Empty Wallet Menu Shown
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_EMPTY_WALLET_MENU_SHOWN =
|
||||
'gemini_cli.empty_wallet_menu_shown';
|
||||
|
||||
export class EmptyWalletMenuShownEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'empty_wallet_menu_shown';
|
||||
'event.timestamp': string;
|
||||
model: string;
|
||||
|
||||
constructor(model: string) {
|
||||
this['event.name'] = 'empty_wallet_menu_shown';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_EMPTY_WALLET_MENU_SHOWN,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
model: this.model,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Empty wallet menu shown for model ${this.model}.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event: Credit Purchase Click
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_CREDIT_PURCHASE_CLICK = 'gemini_cli.credit_purchase_click';
|
||||
|
||||
export class CreditPurchaseClickEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'credit_purchase_click';
|
||||
'event.timestamp': string;
|
||||
source: 'overage_menu' | 'empty_wallet_menu' | 'manage';
|
||||
model: string;
|
||||
|
||||
constructor(
|
||||
source: 'overage_menu' | 'empty_wallet_menu' | 'manage',
|
||||
model: string,
|
||||
) {
|
||||
this['event.name'] = 'credit_purchase_click';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.source = source;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CREDIT_PURCHASE_CLICK,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
source: this.source,
|
||||
model: this.model,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Credit purchase clicked from ${this.source} for model ${this.model}.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event: Credits Used
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_CREDITS_USED = 'gemini_cli.credits_used';
|
||||
|
||||
export class CreditsUsedEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'credits_used';
|
||||
'event.timestamp': string;
|
||||
model: string;
|
||||
credits_consumed: number;
|
||||
credits_remaining: number;
|
||||
|
||||
constructor(
|
||||
model: string,
|
||||
creditsConsumed: number,
|
||||
creditsRemaining: number,
|
||||
) {
|
||||
this['event.name'] = 'credits_used';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.model = model;
|
||||
this.credits_consumed = creditsConsumed;
|
||||
this.credits_remaining = creditsRemaining;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CREDITS_USED,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
model: this.model,
|
||||
credits_consumed: this.credits_consumed,
|
||||
credits_remaining: this.credits_remaining,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `${this.credits_consumed} credits consumed for model ${this.model}. ${this.credits_remaining} remaining.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event: API Key Updated (Auth Type Changed)
|
||||
// ============================================================================
|
||||
|
||||
export const EVENT_API_KEY_UPDATED = 'gemini_cli.api_key_updated';
|
||||
|
||||
export class ApiKeyUpdatedEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'api_key_updated';
|
||||
'event.timestamp': string;
|
||||
previous_auth_type: string;
|
||||
new_auth_type: string;
|
||||
|
||||
constructor(previousAuthType: string, newAuthType: string) {
|
||||
this['event.name'] = 'api_key_updated';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.previous_auth_type = previousAuthType;
|
||||
this.new_auth_type = newAuthType;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_API_KEY_UPDATED,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
previous_auth_type: this.previous_auth_type,
|
||||
new_auth_type: this.new_auth_type,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Auth type changed from ${this.previous_auth_type} to ${this.new_auth_type}.`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Union type of all billing-related telemetry events */
|
||||
export type BillingTelemetryEvent =
|
||||
| OverageMenuShownEvent
|
||||
| OverageOptionSelectedEvent
|
||||
| EmptyWalletMenuShownEvent
|
||||
| CreditPurchaseClickEvent
|
||||
| CreditsUsedEvent
|
||||
| ApiKeyUpdatedEvent;
|
||||
@@ -39,6 +39,7 @@ describe('conseca-logger', () => {
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
} as unknown as Config;
|
||||
|
||||
mockLogger = {
|
||||
|
||||
@@ -77,6 +77,7 @@ export type { TelemetryEvent } from './types.js';
|
||||
export { SpanStatusCode, ValueType } from '@opentelemetry/api';
|
||||
export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
||||
export * from './uiTelemetry.js';
|
||||
export * from './billingEvents.js';
|
||||
export {
|
||||
MemoryMonitor,
|
||||
initializeMemoryMonitor,
|
||||
@@ -145,6 +146,9 @@ export {
|
||||
GenAiOperationName,
|
||||
GenAiProviderName,
|
||||
GenAiTokenType,
|
||||
// Billing metrics functions
|
||||
recordOverageOptionSelected,
|
||||
recordCreditPurchaseClick,
|
||||
} from './metrics.js';
|
||||
export { runInDevTraceSpan, type SpanMetadata } from './trace.js';
|
||||
export { startupProfiler, StartupProfiler } from './startupProfiler.js';
|
||||
|
||||
@@ -280,6 +280,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log a user prompt', () => {
|
||||
@@ -319,6 +320,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
const event = new UserPromptEvent(
|
||||
11,
|
||||
@@ -356,7 +358,8 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordApiResponseMetrics: vi.fn(),
|
||||
@@ -558,7 +561,8 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordApiResponseMetrics: vi.fn(),
|
||||
@@ -996,6 +1000,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log flash fallback event', () => {
|
||||
@@ -1025,6 +1030,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1121,7 +1127,8 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordToolCallMetrics: vi.fn(),
|
||||
@@ -1741,7 +1748,8 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordFileOperationMetric: vi.fn(),
|
||||
@@ -1803,6 +1811,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log a tool output truncated event', () => {
|
||||
@@ -1842,6 +1851,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2099,6 +2109,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2146,6 +2157,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2193,6 +2205,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2231,6 +2244,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2284,6 +2298,7 @@ describe('loggers', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2322,6 +2337,7 @@ describe('loggers', () => {
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -84,6 +84,7 @@ import type { UiEvent } from './uiTelemetry.js';
|
||||
import { uiTelemetryService } from './uiTelemetry.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import type { BillingTelemetryEvent } from './billingEvents.js';
|
||||
|
||||
export function logCliConfiguration(
|
||||
config: Config,
|
||||
@@ -827,3 +828,17 @@ export function logTokenStorageInitialization(
|
||||
recordTokenStorageInitialization(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export function logBillingEvent(
|
||||
config: Config,
|
||||
event: BillingTelemetryEvent,
|
||||
): void {
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count';
|
||||
const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency';
|
||||
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';
|
||||
|
||||
// Agent Metrics
|
||||
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
|
||||
@@ -259,6 +261,26 @@ const COUNTER_DEFINITIONS = {
|
||||
forced: boolean;
|
||||
},
|
||||
},
|
||||
[OVERAGE_OPTION_COUNT]: {
|
||||
description: 'Counts overage option selections.',
|
||||
valueType: ValueType.INT,
|
||||
assign: (c: Counter) => (overageOptionCounter = c),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
attributes: {} as {
|
||||
selected_option: string;
|
||||
model: string;
|
||||
},
|
||||
},
|
||||
[CREDIT_PURCHASE_COUNT]: {
|
||||
description: 'Counts credit purchase link clicks.',
|
||||
valueType: ValueType.INT,
|
||||
assign: (c: Counter) => (creditPurchaseCounter = c),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
attributes: {} as {
|
||||
source: string;
|
||||
model: string;
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const HISTOGRAM_DEFINITIONS = {
|
||||
@@ -597,6 +619,8 @@ let hookCallCounter: Counter | undefined;
|
||||
let hookCallLatencyHistogram: Histogram | undefined;
|
||||
let keychainAvailabilityCounter: Counter | undefined;
|
||||
let tokenStorageTypeCounter: Counter | undefined;
|
||||
let overageOptionCounter: Counter | undefined;
|
||||
let creditPurchaseCounter: Counter | undefined;
|
||||
|
||||
// OpenTelemetry GenAI Semantic Convention Metrics
|
||||
let genAiClientTokenUsageHistogram: Histogram | undefined;
|
||||
@@ -1334,3 +1358,31 @@ export function recordTokenStorageInitialization(
|
||||
forced: event.forced,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a metric for an overage option selection.
|
||||
*/
|
||||
export function recordOverageOptionSelected(
|
||||
config: Config,
|
||||
attributes: MetricDefinitions[typeof OVERAGE_OPTION_COUNT]['attributes'],
|
||||
): void {
|
||||
if (!overageOptionCounter || !isMetricsInitialized) return;
|
||||
overageOptionCounter.add(1, {
|
||||
...baseMetricDefinition.getCommonAttributes(config),
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a metric for a credit purchase link click.
|
||||
*/
|
||||
export function recordCreditPurchaseClick(
|
||||
config: Config,
|
||||
attributes: MetricDefinitions[typeof CREDIT_PURCHASE_COUNT]['attributes'],
|
||||
): void {
|
||||
if (!creditPurchaseCounter || !isMetricsInitialized) return;
|
||||
creditPurchaseCounter.add(1, {
|
||||
...baseMetricDefinition.getCommonAttributes(config),
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ function createMockConfig(logPromptsEnabled: boolean): Config {
|
||||
getModel: () => 'gemini-1.5-flash',
|
||||
isInteractive: () => true,
|
||||
getUserEmail: () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('Telemetry SDK', () => {
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@ const installationManager = new InstallationManager();
|
||||
export function getCommonAttributes(config: Config): Attributes {
|
||||
const email = userAccountManager.getCachedGoogleAccount();
|
||||
const experiments = config.getExperiments();
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
return {
|
||||
'session.id': config.getSessionId(),
|
||||
'installation.id': installationManager.getInstallationId(),
|
||||
interactive: config.isInteractive(),
|
||||
...(email && { 'user.email': email }),
|
||||
...(authType && { auth_type: authType }),
|
||||
...(experiments &&
|
||||
experiments.experimentIds.length > 0 && {
|
||||
'experiments.ids': experiments.experimentIds,
|
||||
|
||||
Reference in New Issue
Block a user