From 143bb63483ad432cb3184a693e95f7a7179cb563 Mon Sep 17 00:00:00 2001 From: Gaurav <39389231+gsquared94@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:26:25 -0800 Subject: [PATCH] Add exp.gws_experiment field to LogEventEntry (#16062) --- .../clearcut-logger/clearcut-logger.test.ts | 58 +++++++++++++++++-- .../clearcut-logger/clearcut-logger.ts | 25 +++++--- 2 files changed, 71 insertions(+), 12 deletions(-) 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 c7cc10cdaa..349fa182eb 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -93,6 +93,22 @@ expect.extend({ `event ${received} ${isNot ? 'has' : 'does not have'} the metadata key ${key}`, }; }, + + toHaveGwsExperiments(received: LogEventEntry[], exps: number[]) { + const { isNot } = this; + const gwsExperiment = received[0].exp?.gws_experiment; + + const pass = + gwsExperiment !== undefined && + gwsExperiment.length === exps.length && + gwsExperiment.every((val, idx) => val === exps[idx]); + + return { + pass, + message: () => + `exp.gws_experiment ${JSON.stringify(gwsExperiment)} does${isNot ? '' : ' not'} match ${JSON.stringify(exps)}`, + }; + }, }); vi.mock('../../utils/userAccountManager.js'); @@ -850,19 +866,53 @@ describe('ClearcutLogger', () => { }); describe('logExperiments', () => { - it('logs an event with gws_experiment field containing exp ids', () => { + it('async path includes exp.gws_experiment field with experiment IDs', async () => { const { logger } = setup(); - const event = new AgentStartEvent('agent-123', 'TestAgent'); + const event = logger!.createLogEvent(EventNames.START_SESSION, []); - logger?.logAgentStartEvent(event); + await logger?.enqueueLogEventAfterExperimentsLoadAsync(event); + await vi.runAllTimersAsync(); const events = getEvents(logger!); expect(events.length).toBe(1); - expect(events[0]).toHaveEventName(EventNames.AGENT_START); + expect(events[0]).toHaveEventName(EventNames.START_SESSION); + // Both metadata and exp.gws_experiment should be populated expect(events[0]).toHaveMetadataValue([ EventMetadataKey.GEMINI_CLI_EXPERIMENT_IDS, '123,456,789', ]); + expect(events[0]).toHaveGwsExperiments([123, 456, 789]); + }); + + it('async path includes empty gws_experiment array when no experiments', async () => { + const { logger } = setup({ + config: { + experiments: { + experimentIds: [], + }, + } as unknown as Partial, + }); + const event = logger!.createLogEvent(EventNames.START_SESSION, []); + + await logger?.enqueueLogEventAfterExperimentsLoadAsync(event); + await vi.runAllTimersAsync(); + + const events = getEvents(logger!); + expect(events.length).toBe(1); + expect(events[0]).toHaveGwsExperiments([]); + }); + + it('non-async path does not include exp.gws_experiment field', () => { + const { logger } = setup(); + const event = new AgentStartEvent('agent-123', 'TestAgent'); + + // logAgentStartEvent uses the non-async enqueueLogEvent path + logger?.logAgentStartEvent(event); + + const events = getEvents(logger!); + expect(events.length).toBe(1); + // exp.gws_experiment should NOT be present for non-async events + expect(events[0][0].exp).toBeUndefined(); }); }); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 443f9365c9..f3fc7e1347 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -106,6 +106,9 @@ export interface LogResponse { export interface LogEventEntry { event_time_ms: number; source_extension_json: string; + exp?: { + gws_experiment: number[]; + }; } export interface EventValue { @@ -250,7 +253,7 @@ export class ClearcutLogger { ClearcutLogger.instance = undefined; } - enqueueHelper(event: LogEvent): void { + enqueueHelper(event: LogEvent, experimentIds?: number[]): void { // Manually handle overflow for FixedDeque, which throws when full. const wasAtCapacity = this.events.size >= MAX_EVENTS; @@ -258,12 +261,18 @@ export class ClearcutLogger { this.events.shift(); // Evict oldest element to make space. } - this.events.push([ - { - event_time_ms: Date.now(), - source_extension_json: safeJsonStringify(event), - }, - ]); + const logEventEntry: LogEventEntry = { + event_time_ms: Date.now(), + source_extension_json: safeJsonStringify(event), + }; + + if (experimentIds !== undefined) { + logEventEntry.exp = { + gws_experiment: experimentIds, + }; + } + + this.events.push([logEventEntry]); if (wasAtCapacity && this.config?.getDebugMode()) { debugLogger.debug( @@ -298,7 +307,7 @@ export class ClearcutLogger { event.event_metadata = [[...event.event_metadata[0], ...exp_id_data]]; } - this.enqueueHelper(event); + this.enqueueHelper(event, experiments?.experimentIds); }); } catch (error) { debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);