diff --git a/packages/opencode/src/altimate/telemetry/index.ts b/packages/opencode/src/altimate/telemetry/index.ts index b2b0e7103..ac275a861 100644 --- a/packages/opencode/src/altimate/telemetry/index.ts +++ b/packages/opencode/src/altimate/telemetry/index.ts @@ -1198,6 +1198,10 @@ export namespace Telemetry { let flushTimer: ReturnType | undefined let userEmail = "" let machineId = "" + // Where this serve process was launched from ("ide" when spawned by an + // editor extension, "" for CLI/TUI). Stamped onto every event so analytics + // can attribute sessions to IDE vs terminal usage. + let launchSurface = "" let sessionId = "" let projectId = "" let appInsights: AppInsightsConfig | undefined @@ -1228,6 +1232,7 @@ export namespace Telemetry { cli_version: Installation.VERSION, project_id: fields.project_id ?? projectId, ...(machineId && { machine_id: machineId }), + ...(launchSurface && { launch_surface: launchSurface }), } const measurements: Record = {} @@ -1319,6 +1324,13 @@ export namespace Telemetry { } catch { // Account unavailable — proceed without user ID } + // Set by the IDE extension when it spawns `altimate serve`; absent for + // direct CLI/TUI invocations. Normalize casing/whitespace and restrict to + // a known set so a stray env value can't inflate the launch_surface + // dimension's cardinality. New surfaces must be added to KNOWN_LAUNCH_SURFACES. + const KNOWN_LAUNCH_SURFACES = new Set(["ide", "cli", "tui"]) + const rawLaunchSurface = (process.env.ALTIMATE_LAUNCH_SURFACE ?? "").trim().toLowerCase() + launchSurface = KNOWN_LAUNCH_SURFACES.has(rawLaunchSurface) ? rawLaunchSurface : "" try { const machineIdPath = path.join(os.homedir(), ".altimate", "machine-id") try { @@ -1449,6 +1461,7 @@ export namespace Telemetry { sessionId = "" projectId = "" machineId = "" + launchSurface = "" initPromise = undefined initDone = false } diff --git a/packages/opencode/test/telemetry/telemetry.test.ts b/packages/opencode/test/telemetry/telemetry.test.ts index 2f0d2dce8..03f8153b9 100644 --- a/packages/opencode/test/telemetry/telemetry.test.ts +++ b/packages/opencode/test/telemetry/telemetry.test.ts @@ -596,6 +596,82 @@ describe("telemetry.toAppInsightsEnvelopes (indirect)", () => { } }) + test("launch_surface is stamped on every event when ALTIMATE_LAUNCH_SURFACE is set", async () => { + const origSurface = process.env.ALTIMATE_LAUNCH_SURFACE + process.env.ALTIMATE_LAUNCH_SURFACE = "ide" + const { fetchCalls, cleanup } = await initWithMockedFetch() + try { + Telemetry.track({ type: "session_start", timestamp: 1700000000000, session_id: "sess-ide" }) + await Telemetry.flush() + + expect(fetchCalls.length).toBeGreaterThan(0) + const envelopes = JSON.parse(fetchCalls[0].body) + expect(envelopes.length).toBeGreaterThan(0) + expect(envelopes[0].data.baseData.properties.launch_surface).toBe("ide") + } finally { + cleanup() + if (origSurface !== undefined) process.env.ALTIMATE_LAUNCH_SURFACE = origSurface + else delete process.env.ALTIMATE_LAUNCH_SURFACE + } + }) + + test("launch_surface is omitted when ALTIMATE_LAUNCH_SURFACE is unset (CLI/TUI)", async () => { + const origSurface = process.env.ALTIMATE_LAUNCH_SURFACE + delete process.env.ALTIMATE_LAUNCH_SURFACE + const { fetchCalls, cleanup } = await initWithMockedFetch() + try { + Telemetry.track({ type: "session_start", timestamp: 1700000000000, session_id: "sess-cli" }) + await Telemetry.flush() + + expect(fetchCalls.length).toBeGreaterThan(0) + const envelopes = JSON.parse(fetchCalls[0].body) + expect(envelopes.length).toBeGreaterThan(0) + expect(envelopes[0].data.baseData.properties.launch_surface).toBeUndefined() + } finally { + cleanup() + if (origSurface !== undefined) process.env.ALTIMATE_LAUNCH_SURFACE = origSurface + else delete process.env.ALTIMATE_LAUNCH_SURFACE + } + }) + + test("launch_surface normalizes casing and surrounding whitespace", async () => { + const origSurface = process.env.ALTIMATE_LAUNCH_SURFACE + process.env.ALTIMATE_LAUNCH_SURFACE = " IDE " + const { fetchCalls, cleanup } = await initWithMockedFetch() + try { + Telemetry.track({ type: "session_start", timestamp: 1700000000000, session_id: "sess-norm" }) + await Telemetry.flush() + + expect(fetchCalls.length).toBeGreaterThan(0) + const envelopes = JSON.parse(fetchCalls[0].body) + expect(envelopes.length).toBeGreaterThan(0) + expect(envelopes[0].data.baseData.properties.launch_surface).toBe("ide") + } finally { + cleanup() + if (origSurface !== undefined) process.env.ALTIMATE_LAUNCH_SURFACE = origSurface + else delete process.env.ALTIMATE_LAUNCH_SURFACE + } + }) + + test("launch_surface drops unrecognized values to keep the dimension low-cardinality", async () => { + const origSurface = process.env.ALTIMATE_LAUNCH_SURFACE + process.env.ALTIMATE_LAUNCH_SURFACE = "totally-bogus" + const { fetchCalls, cleanup } = await initWithMockedFetch() + try { + Telemetry.track({ type: "session_start", timestamp: 1700000000000, session_id: "sess-bogus" }) + await Telemetry.flush() + + expect(fetchCalls.length).toBeGreaterThan(0) + const envelopes = JSON.parse(fetchCalls[0].body) + expect(envelopes.length).toBeGreaterThan(0) + expect(envelopes[0].data.baseData.properties.launch_surface).toBeUndefined() + } finally { + cleanup() + if (origSurface !== undefined) process.env.ALTIMATE_LAUNCH_SURFACE = origSurface + else delete process.env.ALTIMATE_LAUNCH_SURFACE + } + }) + test("numeric fields go to measurements, string fields go to properties", async () => { const { fetchCalls, cleanup } = await initWithMockedFetch() try {