From 867dc0fdda7c59fab627f6a1db65cf0f3fde84c2 Mon Sep 17 00:00:00 2001 From: Gaurav <39389231+gsquared94@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:16:27 -0700 Subject: [PATCH] feat(telemetry): add Clearcut instrumentation for AI credits billing events (#22153) --- .../clearcut-logger/clearcut-logger.test.ts | 101 ++++++++++++++++++ .../clearcut-logger/clearcut-logger.ts | 88 +++++++++++++++ .../clearcut-logger/event-metadata-key.ts | 24 ++++- packages/core/src/telemetry/loggers.ts | 19 ++++ 4 files changed, 231 insertions(+), 1 deletion(-) diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 93eebd651e..dd641e3955 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -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 { 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"', + ]); + }); + }); }); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 5e19d7f49b..5953578eae 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -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. diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index 20c983aa63..632730aeeb 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -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, } diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 52e0fb35bb..d5cc605e65 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -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); + } + } }