feat(billing): implement G1 AI credits overage flow with billing telemetry (#18590)

This commit is contained in:
Gaurav
2026-02-27 10:15:06 -08:00
committed by GitHub
parent fdd844b405
commit b2d6844f9b
55 changed files with 3182 additions and 23 deletions

View 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');
});
});
});

View 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;

View File

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

View File

@@ -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';

View File

@@ -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(() => {

View File

@@ -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);
});
}

View File

@@ -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,
});
}

View File

@@ -32,6 +32,7 @@ function createMockConfig(logPromptsEnabled: boolean): Config {
getModel: () => 'gemini-1.5-flash',
isInteractive: () => true,
getUserEmail: () => undefined,
getContentGeneratorConfig: () => undefined,
} as unknown as Config;
}

View File

@@ -77,6 +77,7 @@ describe('Telemetry SDK', () => {
isInteractive: () => false,
getExperiments: () => undefined,
getExperimentsAsync: async () => undefined,
getContentGeneratorConfig: () => undefined,
} as unknown as Config;
});

View File

@@ -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,