feat(telemetry): add Clearcut instrumentation for AI credits billing events (#22153)

This commit is contained in:
Gaurav
2026-03-12 08:16:27 -07:00
committed by GitHub
parent 7506b00488
commit 867dc0fdda
4 changed files with 231 additions and 1 deletions

View File

@@ -51,6 +51,12 @@ import { InstallationManager } from '../../utils/installationManager.js';
import si, { type Systeminformation } from 'systeminformation';
import * as os from 'node:os';
import {
CreditsUsedEvent,
OverageOptionSelectedEvent,
EmptyWalletMenuShownEvent,
CreditPurchaseClickEvent,
} from '../billingEvents.js';
interface CustomMatchers<R = unknown> {
toHaveMetadataValue: ([key, value]: [EventMetadataKey, string]) => R;
@@ -1551,4 +1557,99 @@ describe('ClearcutLogger', () => {
]);
});
});
describe('logCreditsUsedEvent', () => {
it('logs an event with model, consumed, and remaining credits', () => {
const { logger } = setup();
const event = new CreditsUsedEvent('gemini-3-pro-preview', 10, 490);
logger?.logCreditsUsedEvent(event);
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.CREDITS_USED);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
'"gemini-3-pro-preview"',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_CREDITS_CONSUMED,
'10',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_CREDITS_REMAINING,
'490',
]);
});
});
describe('logOverageOptionSelectedEvent', () => {
it('logs an event with model, selected option, and credit balance', () => {
const { logger } = setup();
const event = new OverageOptionSelectedEvent(
'gemini-3-pro-preview',
'use_credits',
350,
);
logger?.logOverageOptionSelectedEvent(event);
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.OVERAGE_OPTION_SELECTED);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
'"gemini-3-pro-preview"',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_SELECTED_OPTION,
'"use_credits"',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_CREDIT_BALANCE,
'350',
]);
});
});
describe('logEmptyWalletMenuShownEvent', () => {
it('logs an event with the model', () => {
const { logger } = setup();
const event = new EmptyWalletMenuShownEvent('gemini-3-pro-preview');
logger?.logEmptyWalletMenuShownEvent(event);
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.EMPTY_WALLET_MENU_SHOWN);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
'"gemini-3-pro-preview"',
]);
});
});
describe('logCreditPurchaseClickEvent', () => {
it('logs an event with model and source', () => {
const { logger } = setup();
const event = new CreditPurchaseClickEvent(
'empty_wallet_menu',
'gemini-3-pro-preview',
);
logger?.logCreditPurchaseClickEvent(event);
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.CREDIT_PURCHASE_CLICK);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
'"gemini-3-pro-preview"',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BILLING_PURCHASE_SOURCE,
'"empty_wallet_menu"',
]);
});
});
});

View File

@@ -52,6 +52,12 @@ import type {
TokenStorageInitializationEvent,
StartupStatsEvent,
} from '../types.js';
import type {
CreditsUsedEvent,
OverageOptionSelectedEvent,
EmptyWalletMenuShownEvent,
CreditPurchaseClickEvent,
} from '../billingEvents.js';
import { EventMetadataKey } from './event-metadata-key.js';
import type { Config } from '../../config/config.js';
import { InstallationManager } from '../../utils/installationManager.js';
@@ -121,6 +127,10 @@ export enum EventNames {
CONSECA_POLICY_GENERATION = 'conseca_policy_generation',
CONSECA_VERDICT = 'conseca_verdict',
STARTUP_STATS = 'startup_stats',
CREDITS_USED = 'credits_used',
OVERAGE_OPTION_SELECTED = 'overage_option_selected',
EMPTY_WALLET_MENU_SHOWN = 'empty_wallet_menu_shown',
CREDIT_PURCHASE_CLICK = 'credit_purchase_click',
}
export interface LogResponse {
@@ -1806,6 +1816,84 @@ export class ClearcutLogger {
this.flushIfNeeded();
}
// ==========================================================================
// Billing / AI Credits Events
// ==========================================================================
logCreditsUsedEvent(event: CreditsUsedEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
value: JSON.stringify(event.model),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_CREDITS_CONSUMED,
value: JSON.stringify(event.credits_consumed),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_CREDITS_REMAINING,
value: JSON.stringify(event.credits_remaining),
},
];
this.enqueueLogEvent(this.createLogEvent(EventNames.CREDITS_USED, data));
this.flushIfNeeded();
}
logOverageOptionSelectedEvent(event: OverageOptionSelectedEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
value: JSON.stringify(event.model),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_SELECTED_OPTION,
value: JSON.stringify(event.selected_option),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_CREDIT_BALANCE,
value: JSON.stringify(event.credit_balance),
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.OVERAGE_OPTION_SELECTED, data),
);
this.flushIfNeeded();
}
logEmptyWalletMenuShownEvent(event: EmptyWalletMenuShownEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
value: JSON.stringify(event.model),
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.EMPTY_WALLET_MENU_SHOWN, data),
);
this.flushIfNeeded();
}
logCreditPurchaseClickEvent(event: CreditPurchaseClickEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_MODEL,
value: JSON.stringify(event.model),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BILLING_PURCHASE_SOURCE,
value: JSON.stringify(event.source),
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.CREDIT_PURCHASE_CLICK, data),
);
this.flushIfNeeded();
}
/**
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.

View File

@@ -7,7 +7,7 @@
// Defines valid event metadata keys for Clearcut logging.
export enum EventMetadataKey {
// Deleted enums: 24
// Next ID: 180
// Next ID: 191
GEMINI_CLI_KEY_UNKNOWN = 0,
@@ -687,4 +687,26 @@ export enum EventMetadataKey {
// Logs the error type for a network retry.
GEMINI_CLI_NETWORK_RETRY_ERROR_TYPE = 182,
// ==========================================================================
// Billing / AI Credits Event Keys
// ==========================================================================
// Logs the model associated with a billing event.
GEMINI_CLI_BILLING_MODEL = 185,
// Logs the number of AI credits consumed in a request.
GEMINI_CLI_BILLING_CREDITS_CONSUMED = 186,
// Logs the remaining AI credits after a request.
GEMINI_CLI_BILLING_CREDITS_REMAINING = 187,
// Logs the overage option selected by the user (e.g. use_credits, use_fallback, manage, stop).
GEMINI_CLI_BILLING_SELECTED_OPTION = 188,
// Logs the user's credit balance when the overage menu was shown.
GEMINI_CLI_BILLING_CREDIT_BALANCE = 189,
// Logs the source of a credit purchase click (e.g. overage_menu, empty_wallet_menu, manage).
GEMINI_CLI_BILLING_PURCHASE_SOURCE = 190,
}

View File

@@ -85,6 +85,12 @@ import { uiTelemetryService, type UiEvent } from './uiTelemetry.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
import { debugLogger } from '../utils/debugLogger.js';
import type { BillingTelemetryEvent } from './billingEvents.js';
import {
CreditsUsedEvent,
OverageOptionSelectedEvent,
EmptyWalletMenuShownEvent,
CreditPurchaseClickEvent,
} from './billingEvents.js';
export function logCliConfiguration(
config: Config,
@@ -877,4 +883,17 @@ export function logBillingEvent(
};
logger.emit(logRecord);
});
const cc = ClearcutLogger.getInstance(config);
if (cc) {
if (event instanceof CreditsUsedEvent) {
cc.logCreditsUsedEvent(event);
} else if (event instanceof OverageOptionSelectedEvent) {
cc.logOverageOptionSelectedEvent(event);
} else if (event instanceof EmptyWalletMenuShownEvent) {
cc.logEmptyWalletMenuShownEvent(event);
} else if (event instanceof CreditPurchaseClickEvent) {
cc.logCreditPurchaseClickEvent(event);
}
}
}