feat: add anonymized device_id for telemetry (#350)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
cthomas
2025-12-22 16:00:13 -08:00
committed by GitHub
parent 5dac7172e6
commit dad7f3a297
4 changed files with 37 additions and 34 deletions

View File

@@ -23,11 +23,7 @@ export async function getClient() {
if (expiresAt - now < 5 * 60 * 1000) {
try {
// Get or generate device ID (should always exist, but fallback just in case)
let deviceId = settings.deviceId;
if (!deviceId) {
deviceId = crypto.randomUUID();
settingsManager.updateSettings({ deviceId });
}
const deviceId = settingsManager.getOrCreateDeviceId();
const deviceName = hostname();
const tokens = await refreshAccessToken(

View File

@@ -72,11 +72,7 @@ export function SetupUI({ onComplete }: SetupUIProps) {
}
// Get or generate device ID
let deviceId = settingsManager.getSetting("deviceId");
if (!deviceId) {
deviceId = crypto.randomUUID();
settingsManager.updateSettings({ deviceId });
}
const deviceId = settingsManager.getOrCreateDeviceId();
const deviceName = hostname();
// Start polling in background

View File

@@ -122,6 +122,19 @@ class SettingsManager {
return this.getSettings()[key];
}
/**
* Get or create device ID (generates UUID if not exists)
*/
getOrCreateDeviceId(): string {
const settings = this.getSettings();
let deviceId = settings.deviceId;
if (!deviceId) {
deviceId = crypto.randomUUID();
this.updateSettings({ deviceId });
}
return deviceId;
}
/**
* Update settings (synchronous in-memory, async persist)
*/

View File

@@ -1,3 +1,5 @@
import { settingsManager } from "../settings-manager";
export interface TelemetryEvent {
type: "session_start" | "session_end" | "tool_usage" | "error" | "user_input";
timestamp: string;
@@ -54,6 +56,7 @@ export interface UserInputData {
class TelemetryManager {
private events: TelemetryEvent[] = [];
private sessionId: string;
private deviceId: string | null = null;
private currentAgentId: string | null = null;
private sessionStartTime: number;
private messageCount = 0;
@@ -95,18 +98,6 @@ class TelemetryManager {
return false;
}
// Disable telemetry if using self-hosted server (not api.letta.com)
const baseURL = process.env.LETTA_BASE_URL;
if (baseURL && !baseURL.includes("api.letta.com")) {
return false;
}
// Disable telemetry if no API key is set
const apiKey = process.env.LETTA_API_KEY;
if (!apiKey) {
return false;
}
return true;
}
@@ -118,6 +109,9 @@ class TelemetryManager {
return;
}
// Initialize device ID (persistent across sessions)
this.deviceId = settingsManager.getOrCreateDeviceId();
this.trackSessionStart();
// Set up periodic flushing
@@ -180,6 +174,7 @@ class TelemetryManager {
data: {
...data,
session_id: this.sessionId,
device_id: this.deviceId || undefined,
agent_id: this.currentAgentId || undefined,
},
};
@@ -382,7 +377,6 @@ class TelemetryManager {
const eventsToSend = [...this.events];
this.events = [];
const baseURL = process.env.LETTA_BASE_URL || "https://api.letta.com";
const apiKey = process.env.LETTA_API_KEY;
try {
@@ -391,18 +385,22 @@ class TelemetryManager {
setTimeout(() => reject(new Error("Telemetry request timeout")), 5000),
);
const fetchPromise = fetch(`${baseURL}/v1/metadata/telemetry`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Letta-Source": "letta-code",
const fetchPromise = fetch(
"https://api.letta.com/v1/metadata/telemetry",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Letta-Source": "letta-code",
"X-Letta-Code-Device-ID": this.deviceId || "",
},
body: JSON.stringify({
service: "letta-code",
events: eventsToSend,
}),
},
body: JSON.stringify({
service: "letta-code",
events: eventsToSend,
}),
});
);
const response = (await Promise.race([
fetchPromise,