From c87ad607bb85e20bf40b10ff48233af47e98d210 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 4 Mar 2026 18:39:08 -0800 Subject: [PATCH 1/5] fix: address telemetry review issues from PR #39 - Standardize error message truncation to 500 chars (was 1000 in processor.ts) - Implement ALTIMATE_TELEMETRY_DISABLED env var check in telemetry init() - Track MCP transport type on connect, use correct transport in disconnect - Add sessionHadError flag so agent_outcome produces "error" outcome - Use Telemetry.getContext().sessionId in auth events instead of hardcoded "cli" Co-Authored-By: Claude Opus 4.6 --- packages/altimate-code/src/cli/cmd/auth.ts | 20 +++++++++---------- packages/altimate-code/src/mcp/index.ts | 15 ++++++++++++-- .../altimate-code/src/session/processor.ts | 2 +- packages/altimate-code/src/session/prompt.ts | 16 +++++++++++---- packages/altimate-code/src/telemetry/index.ts | 1 + 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/altimate-code/src/cli/cmd/auth.ts b/packages/altimate-code/src/cli/cmd/auth.ts index 12871961b7..308b4ff48f 100644 --- a/packages/altimate-code/src/cli/cmd/auth.ts +++ b/packages/altimate-code/src/cli/cmd/auth.ts @@ -82,7 +82,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: provider, method: "oauth", status: "error", @@ -111,7 +111,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: saveProvider, method: "oauth", status: "success", @@ -131,7 +131,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: provider, method: "oauth", status: "error", @@ -160,7 +160,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: saveProvider, method: "oauth", status: "success", @@ -180,7 +180,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: provider, method: "api_key", status: "error", @@ -197,7 +197,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: saveProvider, method: "api_key", status: "success", @@ -325,7 +325,7 @@ export const AuthLoginCommand = cmd({ Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: args.url!, method: "api_key", status: "error", @@ -344,7 +344,7 @@ export const AuthLoginCommand = cmd({ Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: args.url!, method: "api_key", status: "success", @@ -483,7 +483,7 @@ export const AuthLoginCommand = cmd({ Telemetry.track({ type: "auth_login", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: provider, method: "api_key", status: "success", @@ -519,7 +519,7 @@ export const AuthLogoutCommand = cmd({ Telemetry.track({ type: "auth_logout", timestamp: Date.now(), - session_id: "cli", + session_id: Telemetry.getContext().sessionId || "cli", provider_id: providerID, }) prompts.outro("Logout successful") diff --git a/packages/altimate-code/src/mcp/index.ts b/packages/altimate-code/src/mcp/index.ts index 182d882f21..73b9fd2a42 100644 --- a/packages/altimate-code/src/mcp/index.ts +++ b/packages/altimate-code/src/mcp/index.ts @@ -167,6 +167,7 @@ export namespace MCP { const config = cfg.mcp ?? {} const clients: Record = {} const status: Record = {} + const transports: Record = {} await Promise.all( Object.entries(config).map(async ([key, mcp]) => { @@ -188,12 +189,14 @@ export namespace MCP { if (result.mcpClient) { clients[key] = result.mcpClient + if (result.transport) transports[key] = result.transport } }), ) return { status, clients, + transports, } }, async (state) => { @@ -283,6 +286,7 @@ export namespace MCP { } s.clients[name] = result.mcpClient s.status[name] = result.status + if (result.transport) s.transports[name] = result.transport return { status: s.status, @@ -301,6 +305,7 @@ export namespace MCP { log.info("found", { key, type: mcp.type }) let mcpClient: MCPClient | undefined let status: Status | undefined = undefined + let connectedTransport: "stdio" | "sse" | "streamable-http" | undefined = undefined if (mcp.type === "remote") { // OAuth is enabled by default for remote servers unless explicitly disabled with oauth: false @@ -355,6 +360,7 @@ export namespace MCP { await withTimeout(client.connect(transport), connectTimeout) registerNotificationHandlers(client, key) mcpClient = client + connectedTransport = name === "SSE" ? "sse" : "streamable-http" log.info("connected", { key, transport: name }) status = { status: "connected" } Telemetry.track({ @@ -362,7 +368,7 @@ export namespace MCP { timestamp: Date.now(), session_id: Telemetry.getContext().sessionId, server_name: key, - transport: name === "SSE" ? "sse" : "streamable-http", + transport: connectedTransport, status: "connected", duration_ms: Date.now() - connectStart, }) @@ -470,6 +476,7 @@ export namespace MCP { await withTimeout(client.connect(transport), connectTimeout) registerNotificationHandlers(client, key) mcpClient = client + connectedTransport = "stdio" status = { status: "connected", } @@ -563,6 +570,7 @@ export namespace MCP { return { mcpClient, status, + transport: connectedTransport, } } @@ -621,11 +629,13 @@ export namespace MCP { }) } s.clients[name] = result.mcpClient + if (result.transport) s.transports[name] = result.transport } } export async function disconnect(name: string) { const s = await state() + const transport = s.transports[name] ?? "stdio" const client = s.clients[name] if (client) { await client.close().catch((error) => { @@ -638,9 +648,10 @@ export namespace MCP { timestamp: Date.now(), session_id: Telemetry.getContext().sessionId, server_name: name, - transport: "stdio", + transport, status: "disconnected", }) + delete s.transports[name] s.status[name] = { status: "disabled" } } diff --git a/packages/altimate-code/src/session/processor.ts b/packages/altimate-code/src/session/processor.ts index 63fee8bcac..83e49e0872 100644 --- a/packages/altimate-code/src/session/processor.ts +++ b/packages/altimate-code/src/session/processor.ts @@ -458,7 +458,7 @@ export namespace SessionProcessor { timestamp: Date.now(), session_id: input.sessionID, error_name: e?.name ?? "UnknownError", - error_message: (e?.message ?? String(e)).slice(0, 1000), + error_message: (e?.message ?? String(e)).slice(0, 500), context: "processor", }) const error = MessageV2.fromError(e, { providerID: input.model.providerID }) diff --git a/packages/altimate-code/src/session/prompt.ts b/packages/altimate-code/src/session/prompt.ts index 7d9edac428..fe8e4f47d3 100644 --- a/packages/altimate-code/src/session/prompt.ts +++ b/packages/altimate-code/src/session/prompt.ts @@ -298,6 +298,7 @@ export namespace SessionPrompt { let compactionAttempts = 0 let totalCompactions = 0 let sessionAgentName = "" + let sessionHadError = false const MAX_COMPACTION_ATTEMPTS = 3 const session = await Session.get(sessionID) await Telemetry.init() @@ -567,6 +568,7 @@ export namespace SessionPrompt { message: `Context still too large after ${MAX_COMPACTION_ATTEMPTS} compaction attempts. Try starting a new conversation.`, }).toObject(), }) + sessionHadError = true break } Telemetry.track({ @@ -752,7 +754,10 @@ export namespace SessionPrompt { } } - if (result === "stop") break + if (result === "stop") { + if (processor.message.error) sessionHadError = true + break + } if (result === "continue") { // Reset compaction counter after a successful non-compaction step. // The counter protects against tight compact→overflow loops within @@ -770,6 +775,7 @@ export namespace SessionPrompt { message: `Context still too large after ${MAX_COMPACTION_ATTEMPTS} compaction attempts. Try starting a new conversation.`, }).toObject(), }) + sessionHadError = true break } Telemetry.track({ @@ -792,9 +798,11 @@ export namespace SessionPrompt { } finally { const outcome: "completed" | "abandoned" | "error" = abort.aborted ? "abandoned" - : sessionTotalCost === 0 && toolCallCount === 0 - ? "abandoned" - : "completed" + : sessionHadError + ? "error" + : sessionTotalCost === 0 && toolCallCount === 0 + ? "abandoned" + : "completed" Telemetry.track({ type: "agent_outcome", timestamp: Date.now(), diff --git a/packages/altimate-code/src/telemetry/index.ts b/packages/altimate-code/src/telemetry/index.ts index fd9b88807a..c0cd66dab8 100644 --- a/packages/altimate-code/src/telemetry/index.ts +++ b/packages/altimate-code/src/telemetry/index.ts @@ -367,6 +367,7 @@ export namespace Telemetry { export async function init() { if (enabled || flushTimer) return + if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") return const userConfig = await Config.get() if (userConfig.telemetry?.disabled) return try { From fce463470ab37eacae6f9bf1df93d75e535f53c5 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 4 Mar 2026 18:47:37 -0800 Subject: [PATCH 2/5] fix: initialize telemetry early and buffer pre-init events Telemetry.init() was only called in SessionPrompt.next(), but track() calls from MCP connect, engine startup, and auth commands fire earlier. Events tracked before init() were silently dropped. - Buffer events in track() regardless of enabled state (pre-init events are kept and flushed once init() completes) - Clear buffer when init() determines telemetry is disabled - Call init() in main CLI middleware (covers auth, serve, etc.) - Call init() in worker thread startup (covers MCP/engine events) - Keep existing init() in session prompt as idempotent safety net Co-Authored-By: Claude Opus 4.6 --- packages/altimate-code/src/cli/cmd/tui/worker.ts | 4 ++++ packages/altimate-code/src/index.ts | 5 +++++ packages/altimate-code/src/telemetry/index.ts | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/altimate-code/src/cli/cmd/tui/worker.ts b/packages/altimate-code/src/cli/cmd/tui/worker.ts index 7744a488bb..1dbb8df064 100644 --- a/packages/altimate-code/src/cli/cmd/tui/worker.ts +++ b/packages/altimate-code/src/cli/cmd/tui/worker.ts @@ -10,6 +10,7 @@ import { GlobalBus } from "@/bus/global" import { createOpencodeClient, type Event } from "@altimateai/altimate-code-sdk/v2" import type { BunWebSocketData } from "hono/bun" import { Flag } from "@/flag/flag" +import { Telemetry } from "@/telemetry" await Log.init({ print: process.argv.includes("--print-logs"), @@ -32,6 +33,9 @@ process.on("uncaughtException", (e) => { }) }) +// Initialize telemetry early so MCP/engine events are captured before session starts +Telemetry.init().catch(() => {}) + // Subscribe to global events and forward them via RPC GlobalBus.on("event", (event) => { Rpc.emit("global.event", event) diff --git a/packages/altimate-code/src/index.ts b/packages/altimate-code/src/index.ts index 04e036d960..d659c084de 100644 --- a/packages/altimate-code/src/index.ts +++ b/packages/altimate-code/src/index.ts @@ -32,6 +32,7 @@ import path from "path" import { Global } from "./global" import { JsonMigration } from "./storage/json-migration" import { Database } from "./storage/db" +import { Telemetry } from "./telemetry" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -76,6 +77,10 @@ const cli = yargs(hideBin(process.argv)) process.env.AGENT = "1" process.env.DATAPILOT = "1" + // Initialize telemetry early so events from MCP, engine, auth are captured. + // init() is idempotent — safe to call again later in session prompt. + Telemetry.init().catch(() => {}) + Log.Default.info("altimate-code", { version: Installation.VERSION, args: process.argv.slice(2), diff --git a/packages/altimate-code/src/telemetry/index.ts b/packages/altimate-code/src/telemetry/index.ts index c0cd66dab8..34e73fc244 100644 --- a/packages/altimate-code/src/telemetry/index.ts +++ b/packages/altimate-code/src/telemetry/index.ts @@ -367,15 +367,22 @@ export namespace Telemetry { export async function init() { if (enabled || flushTimer) return - if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") return + if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") { + buffer = [] + return + } const userConfig = await Config.get() - if (userConfig.telemetry?.disabled) return + if (userConfig.telemetry?.disabled) { + buffer = [] + return + } try { // App Insights: env var overrides default (for dev/testing), otherwise use the baked-in key const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ?? DEFAULT_CONNECTION_STRING const cfg = parseConnectionString(connectionString) if (!cfg) { enabled = false + buffer = [] return } appInsights = cfg @@ -388,6 +395,7 @@ export namespace Telemetry { flushTimer = timer } catch { enabled = false + buffer = [] } } @@ -401,7 +409,8 @@ export namespace Telemetry { } export function track(event: Event) { - if (!enabled) return + // Always buffer — events tracked before init() are kept and flushed + // once init() completes. If init() disables telemetry, the buffer is cleared. buffer.push(event) if (buffer.length > MAX_BUFFER_SIZE) { buffer.shift() From 1a24fa35c62c760f2509693a40e725fd482283ac Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 4 Mar 2026 18:53:17 -0800 Subject: [PATCH 3/5] fix: robust telemetry init with promise deduplication and pre-init buffering The previous commit moved init() earlier but had subtle issues: - No init-once guard when telemetry disabled (init ran repeatedly) - Events during async init() were dropped - Concurrent non-awaited init() calls could race Fix: - Use promise deduplication: init() returns same promise on repeated calls - Use initDone flag: track() buffers during init, drops after disable - Clear buffer on every disabled/error exit path in doInit() - Reset initPromise/initDone in shutdown() for test isolation - Add tests for pre-init buffering, post-disable dropping, idempotency Co-Authored-By: Claude Opus 4.6 --- packages/altimate-code/src/telemetry/index.ts | 41 +++++++----- .../test/telemetry/telemetry.test.ts | 63 ++++++++++++++++++- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/packages/altimate-code/src/telemetry/index.ts b/packages/altimate-code/src/telemetry/index.ts index 34e73fc244..1e05169db9 100644 --- a/packages/altimate-code/src/telemetry/index.ts +++ b/packages/altimate-code/src/telemetry/index.ts @@ -297,6 +297,8 @@ export namespace Telemetry { let sessionId = "" let projectId = "" let appInsights: AppInsightsConfig | undefined + let initPromise: Promise | undefined + let initDone = false function parseConnectionString(cs: string): AppInsightsConfig | undefined { const parts: Record = {} @@ -365,23 +367,30 @@ export namespace Telemetry { const DEFAULT_CONNECTION_STRING = "InstrumentationKey=5095f5e6-477e-4262-b7ae-2118de18550d;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=6564474f-329b-4b7d-849e-e70cb4181294" - export async function init() { - if (enabled || flushTimer) return - if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") { - buffer = [] - return - } - const userConfig = await Config.get() - if (userConfig.telemetry?.disabled) { - buffer = [] - return + // Deduplicates concurrent calls: non-awaited init() in middleware/worker + // won't race with await init() in session prompt. + export function init(): Promise { + if (!initPromise) { + initPromise = doInit() } + return initPromise + } + + async function doInit() { try { + if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") { + buffer = [] + return + } + const userConfig = await Config.get() + if (userConfig.telemetry?.disabled) { + buffer = [] + return + } // App Insights: env var overrides default (for dev/testing), otherwise use the baked-in key const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ?? DEFAULT_CONNECTION_STRING const cfg = parseConnectionString(connectionString) if (!cfg) { - enabled = false buffer = [] return } @@ -394,8 +403,9 @@ export namespace Telemetry { if (typeof timer === "object" && timer && "unref" in timer) (timer as any).unref() flushTimer = timer } catch { - enabled = false buffer = [] + } finally { + initDone = true } } @@ -409,8 +419,9 @@ export namespace Telemetry { } export function track(event: Event) { - // Always buffer — events tracked before init() are kept and flushed - // once init() completes. If init() disables telemetry, the buffer is cleared. + // Before init completes: buffer (flushed once init enables, or cleared if disabled). + // After init completed and disabled telemetry: drop silently. + if (initDone && !enabled) return buffer.push(event) if (buffer.length > MAX_BUFFER_SIZE) { buffer.shift() @@ -452,5 +463,7 @@ export namespace Telemetry { buffer = [] sessionId = "" projectId = "" + initPromise = undefined + initDone = false } } diff --git a/packages/altimate-code/test/telemetry/telemetry.test.ts b/packages/altimate-code/test/telemetry/telemetry.test.ts index 2cd66cd2f5..a614ca7915 100644 --- a/packages/altimate-code/test/telemetry/telemetry.test.ts +++ b/packages/altimate-code/test/telemetry/telemetry.test.ts @@ -91,14 +91,14 @@ describe("telemetry.bucketCount", () => { }) // --------------------------------------------------------------------------- -// 3. track — basic smoke tests (telemetry disabled by default) +// 3. track — buffering behavior // --------------------------------------------------------------------------- describe("telemetry.track", () => { test("track is a function", () => { expect(typeof Telemetry.track).toBe("function") }) - test("track does not throw when called with valid events while disabled", () => { + test("track does not throw when called with valid events before init", () => { expect(() => { Telemetry.track({ type: "session_start", @@ -111,6 +111,65 @@ describe("telemetry.track", () => { }) }).not.toThrow() }) + + test("pre-init events are buffered, not dropped", async () => { + // Ensure clean state + await Telemetry.shutdown() + + // Track before init — should buffer + Telemetry.track({ + type: "mcp_server_status", + timestamp: Date.now(), + session_id: "pre-init", + server_name: "test", + transport: "stdio", + status: "connected", + }) + Telemetry.track({ + type: "engine_started", + timestamp: Date.now(), + session_id: "pre-init", + engine_version: "1.0", + python_version: "3.12", + status: "started", + duration_ms: 100, + }) + + // init() with telemetry disabled via env var — should clear buffer + const origEnv = process.env.ALTIMATE_TELEMETRY_DISABLED + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + try { + await Telemetry.init() + // After init disables, track should drop + Telemetry.track({ + type: "session_start", + timestamp: Date.now(), + session_id: "post-disable", + model_id: "m", + provider_id: "p", + agent: "a", + project_id: "x", + }) + } finally { + process.env.ALTIMATE_TELEMETRY_DISABLED = origEnv + await Telemetry.shutdown() + } + }) + + test("init() is idempotent — second call returns same promise", async () => { + await Telemetry.shutdown() + const origEnv = process.env.ALTIMATE_TELEMETRY_DISABLED + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + try { + const p1 = Telemetry.init() + const p2 = Telemetry.init() + expect(p1).toBe(p2) // same promise object + await p1 + } finally { + process.env.ALTIMATE_TELEMETRY_DISABLED = origEnv + await Telemetry.shutdown() + } + }) }) // --------------------------------------------------------------------------- From ada57fcced1b7a9d187ee3d7f84f226e07a47707 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 4 Mar 2026 18:56:52 -0800 Subject: [PATCH 4/5] fix: handle Config.get() and Control.account() failures in telemetry init Config.get() throws "No context found for instance" when called before Instance.provide() (e.g. CLI middleware, worker thread startup). This caused the outer catch to clear the buffer and disable telemetry entirely. Fix: wrap Config.get() and Control.account() in individual try/catch blocks so failures don't prevent telemetry initialization. The env var ALTIMATE_TELEMETRY_DISABLED is the early-init escape hatch. Verified end-to-end: pre-init events are buffered and flushed to App Insights after init completes. Co-Authored-By: Claude Opus 4.6 --- packages/altimate-code/src/telemetry/index.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/altimate-code/src/telemetry/index.ts b/packages/altimate-code/src/telemetry/index.ts index 1e05169db9..f42f72d5bb 100644 --- a/packages/altimate-code/src/telemetry/index.ts +++ b/packages/altimate-code/src/telemetry/index.ts @@ -382,10 +382,17 @@ export namespace Telemetry { buffer = [] return } - const userConfig = await Config.get() - if (userConfig.telemetry?.disabled) { - buffer = [] - return + // Config.get() may throw outside Instance context (e.g. CLI middleware + // before Instance.provide()). Treat config failures as "not disabled" — + // the env var check above is the early-init escape hatch. + try { + const userConfig = await Config.get() + if (userConfig.telemetry?.disabled) { + buffer = [] + return + } + } catch { + // Config unavailable — proceed with telemetry enabled } // App Insights: env var overrides default (for dev/testing), otherwise use the baked-in key const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ?? DEFAULT_CONNECTION_STRING @@ -395,8 +402,12 @@ export namespace Telemetry { return } appInsights = cfg - const account = Control.account() - if (account) userEmail = account.email + try { + const account = Control.account() + if (account) userEmail = account.email + } catch { + // Account unavailable — proceed without user email + } enabled = true log.info("telemetry initialized", { mode: "appinsights" }) const timer = setInterval(flush, FLUSH_INTERVAL_MS) From f2f40046c3407c29df408b5269cf77d386f557d6 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 4 Mar 2026 18:59:26 -0800 Subject: [PATCH 5/5] fix: hash user email before sending to telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raw email is PII — replace with SHA-256 hash to preserve anonymous user correlation without leaking personally identifiable information. Co-Authored-By: Claude Opus 4.6 --- docs/docs/configure/telemetry.md | 2 +- packages/altimate-code/src/telemetry/index.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docs/configure/telemetry.md b/docs/docs/configure/telemetry.md index c320d3aad7..9cbdcf3a51 100644 --- a/docs/docs/configure/telemetry.md +++ b/docs/docs/configure/telemetry.md @@ -73,7 +73,7 @@ We take your privacy seriously. Altimate Code telemetry **never** collects: - Code content, file contents, or file paths - Credentials, API keys, or tokens - Database connection strings or hostnames -- Personally identifiable information beyond your email (used only for user correlation) +- Personally identifiable information (your email is SHA-256 hashed before sending — used only for anonymous user correlation) - Tool arguments or outputs - AI prompt content or responses diff --git a/packages/altimate-code/src/telemetry/index.ts b/packages/altimate-code/src/telemetry/index.ts index f42f72d5bb..ff91b68225 100644 --- a/packages/altimate-code/src/telemetry/index.ts +++ b/packages/altimate-code/src/telemetry/index.ts @@ -2,6 +2,7 @@ import { Control } from "@/control" import { Config } from "@/config/config" import { Installation } from "@/installation" import { Log } from "@/util/log" +import { createHash } from "crypto" const log = Log.create({ service: "telemetry" }) @@ -404,9 +405,11 @@ export namespace Telemetry { appInsights = cfg try { const account = Control.account() - if (account) userEmail = account.email + if (account) { + userEmail = createHash("sha256").update(account.email.toLowerCase().trim()).digest("hex") + } } catch { - // Account unavailable — proceed without user email + // Account unavailable — proceed without user ID } enabled = true log.info("telemetry initialized", { mode: "appinsights" })