diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts new file mode 100644 index 00000000..85249ad6 --- /dev/null +++ b/packages/core/src/ide/detect-ide.test.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { detectIde, DetectedIde } from './detect-ide.js'; + +describe('detectIde', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it.each([ + { + env: {}, + expected: DetectedIde.VSCode, + }, + { + env: { __COG_BASHRC_SOURCED: '1' }, + expected: DetectedIde.Devin, + }, + { + env: { REPLIT_USER: 'test' }, + expected: DetectedIde.Replit, + }, + { + env: { CURSOR_TRACE_ID: 'test' }, + expected: DetectedIde.Cursor, + }, + { + env: { CODESPACES: 'true' }, + expected: DetectedIde.Codespaces, + }, + { + env: { EDITOR_IN_CLOUD_SHELL: 'true' }, + expected: DetectedIde.CloudShell, + }, + { + env: { CLOUD_SHELL: 'true' }, + expected: DetectedIde.CloudShell, + }, + { + env: { TERM_PRODUCT: 'Trae' }, + expected: DetectedIde.Trae, + }, + { + env: { FIREBASE_DEPLOY_AGENT: 'true' }, + expected: DetectedIde.FirebaseStudio, + }, + { + env: { MONOSPACE_ENV: 'true' }, + expected: DetectedIde.FirebaseStudio, + }, + ])('detects the IDE for $expected', ({ env, expected }) => { + vi.stubEnv('TERM_PROGRAM', 'vscode'); + for (const [key, value] of Object.entries(env)) { + vi.stubEnv(key, value); + } + expect(detectIde()).toBe(expected); + }); + + it('returns undefined for non-vscode', () => { + vi.stubEnv('TERM_PROGRAM', 'definitely-not-vscode'); + expect(detectIde()).toBeUndefined(); + }); +}); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index ef07994c..5cc3cb56 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -5,6 +5,8 @@ */ export enum DetectedIde { + Devin = 'devin', + Replit = 'replit', VSCode = 'vscode', Cursor = 'cursor', CloudShell = 'cloudshell', @@ -19,6 +21,14 @@ export interface IdeInfo { export function getIdeInfo(ide: DetectedIde): IdeInfo { switch (ide) { + case DetectedIde.Devin: + return { + displayName: 'Devin', + }; + case DetectedIde.Replit: + return { + displayName: 'Replit', + }; case DetectedIde.VSCode: return { displayName: 'VS Code', @@ -56,19 +66,25 @@ export function detectIde(): DetectedIde | undefined { if (process.env.TERM_PROGRAM !== 'vscode') { return undefined; } + if (process.env.__COG_BASHRC_SOURCED) { + return DetectedIde.Devin; + } + if (process.env.REPLIT_USER) { + return DetectedIde.Replit; + } if (process.env.CURSOR_TRACE_ID) { return DetectedIde.Cursor; } if (process.env.CODESPACES) { return DetectedIde.Codespaces; } - if (process.env.EDITOR_IN_CLOUD_SHELL) { + if (process.env.EDITOR_IN_CLOUD_SHELL || process.env.CLOUD_SHELL) { return DetectedIde.CloudShell; } if (process.env.TERM_PRODUCT === 'Trae') { return DetectedIde.Trae; } - if (process.env.FIREBASE_DEPLOY_AGENT) { + if (process.env.FIREBASE_DEPLOY_AGENT || process.env.MONOSPACE_ENV) { return DetectedIde.FirebaseStudio; } return DetectedIde.VSCode; 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 96129ad3..f2ce4d19 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -47,7 +47,6 @@ describe('ClearcutLogger', () => { const CLEARCUT_URL = 'https://play.googleapis.com/log'; const MOCK_DATE = new Date('2025-01-02T00:00:00.000Z'); const EXAMPLE_RESPONSE = `["${NEXT_WAIT_MS}",null,[[["ANDROID_BACKUP",0],["BATTERY_STATS",0],["SMART_SETUP",0],["TRON",0]],-3334737594024971225],[]]`; - // A helper to get the internal events array for testing const getEvents = (l: ClearcutLogger): LogEventEntry[][] => l['events'].toArray() as LogEventEntry[][]; @@ -57,6 +56,10 @@ describe('ClearcutLogger', () => { const requeueFailedEvents = (l: ClearcutLogger, events: LogEventEntry[][]) => l['requeueFailedEvents'](events); + afterEach(() => { + vi.unstubAllEnvs(); + }); + function setup({ config = {} as Partial, lifetimeGoogleAccounts = 1, @@ -135,16 +138,84 @@ describe('ClearcutLogger', () => { }); }); - it('logs the current surface', () => { + it('logs the current surface from a github action', () => { const { logger } = setup({}); + vi.stubEnv('GITHUB_SHA', '8675309'); + const event = logger?.createLogEvent('abc', []); expect(event?.event_metadata[0][1]).toEqual({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, - value: 'SURFACE_NOT_SET', + value: 'GitHub', }); }); + + it('honors the value from env.SURFACE over all others', () => { + const { logger } = setup({}); + + vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('SURFACE', 'ide-1234'); + + const event = logger?.createLogEvent('abc', []); + + expect(event?.event_metadata[0][1]).toEqual({ + gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, + value: 'ide-1234', + }); + }); + + it.each([ + { + env: { + CURSOR_TRACE_ID: 'abc123', + GITHUB_SHA: undefined, + }, + expectedValue: 'cursor', + }, + { + env: { + TERM_PROGRAM: 'vscode', + GITHUB_SHA: undefined, + }, + expectedValue: 'vscode', + }, + { + env: { + MONOSPACE_ENV: 'true', + GITHUB_SHA: undefined, + }, + expectedValue: 'firebasestudio', + }, + { + env: { + __COG_BASHRC_SOURCED: 'true', + GITHUB_SHA: undefined, + }, + expectedValue: 'devin', + }, + { + env: { + CLOUD_SHELL: 'true', + GITHUB_SHA: undefined, + }, + expectedValue: 'cloudshell', + }, + ])( + 'logs the current surface for as $expectedValue, preempting vscode detection', + ({ env, expectedValue }) => { + const { logger } = setup({}); + for (const [key, value] of Object.entries(env)) { + vi.stubEnv(key, value); + } + vi.stubEnv('TERM_PROGRAM', 'vscode'); + const event = logger?.createLogEvent('abc', []); + expect(event?.event_metadata[0][1]).toEqual({ + gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, + value: expectedValue, + }); + }, + ); }); describe('enqueueLogEvent', () => { diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 9450f06d..7ccfd440 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -30,6 +30,7 @@ import { } from '../../utils/user_account.js'; import { getInstallationId } from '../../utils/user_id.js'; import { FixedDeque } from 'mnemonist'; +import { DetectedIde, detectIde } from '../../ide/detect-ide.js'; const start_session_event_name = 'start_session'; const new_prompt_event_name = 'new_prompt'; @@ -85,12 +86,14 @@ export interface LogRequest { * methods might have in their runtimes. */ function determineSurface(): string { - if (process.env.CLOUD_SHELL === 'true') { - return 'CLOUD_SHELL'; - } else if (process.env.MONOSPACE_ENV === 'true') { - return 'FIREBASE_STUDIO'; + if (process.env.SURFACE) { + return process.env.SURFACE; + } else if (process.env.GITHUB_SHA) { + return 'GitHub'; + } else if (process.env.TERM_PROGRAM === 'vscode') { + return detectIde() || DetectedIde.VSCode; } else { - return process.env.SURFACE || 'SURFACE_NOT_SET'; + return 'SURFACE_NOT_SET'; } }