From c10fb55598735c9e8c7a480f169e8612e9d5a0b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:12:23 +0000 Subject: [PATCH 1/2] Initial plan From 72315ec42efe9b6a09daab889c95e91f1daec3a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:16:29 +0000 Subject: [PATCH 2/2] Remove all telemetry/analytics code from the app Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> --- docs/TELEMETRY.md | 25 ----- electron/analytics.ts | 134 -------------------------- electron/ipc/app-handlers.ts | 13 --- electron/main.ts | 4 - electron/preload.ts | 11 --- frontend/components/SettingsModal.tsx | 52 ---------- frontend/vite-env.d.ts | 3 - 7 files changed, 242 deletions(-) delete mode 100644 docs/TELEMETRY.md delete mode 100644 electron/analytics.ts diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md deleted file mode 100644 index b1e8a3eb..00000000 --- a/docs/TELEMETRY.md +++ /dev/null @@ -1,25 +0,0 @@ -# Telemetry - -LTX Desktop collects minimal, anonymous telemetry to help the team understand how the app is used and prioritize development. -No personal information, generated content, prompts, file paths, or IP-derived location data is collected or stored. - -## Opting out - -Analytics is enabled by default. You can disable it at any time in **Settings > General > Anonymous Analytics**. When disabled, no events are sent. - -To disable telemetry before the first launch, create an `app_state.json` file in the app data folder with the following content: - -```json -{ "analyticsEnabled": false } -``` - -App data folder locations: - -- **Windows:** `%LOCALAPPDATA%\LTXDesktop\` -- **macOS:** `~/Library/Application Support/LTXDesktop/` - -Your preference is respected immediately — no restart required. - -## Implementation - -The telemetry implementation is fully contained in [`electron/analytics.ts`](../electron/analytics.ts). Events are sent to an ingestion endpoint over HTTPS. No third-party analytics SDKs are used. diff --git a/electron/analytics.ts b/electron/analytics.ts deleted file mode 100644 index 77c010f5..00000000 --- a/electron/analytics.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { randomUUID } from 'crypto'; -import { app } from 'electron'; -import fs from 'fs'; -import path from 'path'; -import { isDev } from './config'; - -const ANALYTICS_ENDPOINT = 'https://ltx-desktop.lightricks.com/v2/ingest'; -const REQUEST_TIMEOUT_MS = 5000; -const MAX_RETRIES = 3; -const RETRY_DELAYS_MS = [1000, 3000, 10000] - -interface AppState { - analyticsEnabled?: boolean - installationId?: string - [key: string]: unknown -} - -function getAppStatePath(): string { - return path.join(app.getPath('userData'), 'app_state.json') -} - -function readAppState(): AppState { - const statePath = getAppStatePath() - try { - if (fs.existsSync(statePath)) { - return JSON.parse(fs.readFileSync(statePath, 'utf-8')) as AppState - } - } catch (err) { - console.warn('[analytics] failed to read app state:', err) - } - return {} -} - -function writeAppState(state: AppState): void { - fs.writeFileSync(getAppStatePath(), JSON.stringify(state, null, 2)) -} - -export function getAnalyticsState(): { analyticsEnabled: boolean; installationId: string } { - const state = readAppState() - return { - analyticsEnabled: state.analyticsEnabled !== false, - installationId: state.installationId ?? '', - } -} - -export function setAnalyticsEnabled(enabled: boolean): void { - const state = readAppState() - state.analyticsEnabled = enabled - // Generate installationId on first enable; persist forever after - if (enabled && !state.installationId) { - state.installationId = randomUUID() - } - writeAppState(state) -} - -function delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -function isRetryable(status: number): boolean { - return status === 429 || status >= 500 -} - -async function sendWithRetry( - url: string, - options: RequestInit, -): Promise { - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { - try { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) - const response = await fetch(url, { ...options, signal: controller.signal }) - clearTimeout(timeout) - - if (response.ok || !isRetryable(response.status)) return - } catch (err) { - console.warn('[analytics] request attempt failed:', err) - } - - if (attempt < MAX_RETRIES) { - await delay(RETRY_DELAYS_MS[attempt]) - } - } -} - -export async function sendAnalyticsEvent( - eventName: string, - extraDetails?: Record | null, -): Promise { - try { - // Skip analytics in dev builds - if (isDev) return; - - const state = readAppState() - if (state.analyticsEnabled === false) return - - // Generate installationId on first send - if (!state.installationId) { - state.installationId = randomUUID() - writeAppState(state) - } - - const platform = process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux' - const now = Date.now() - - const payload = { - events: [ - { - subject: eventName, - eventId: randomUUID(), - eventTimestamp: now, - event: { - app_version: app.getVersion(), - device_timestamp: now, - installation_id: state.installationId, - platform, - extra_details: extraDetails ? JSON.stringify(extraDetails) : null, - }, - }, - ], - } - - // Fire-and-forget with retries — never throws - void sendWithRetry(ANALYTICS_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) - } catch (err) { - console.error('[analytics] failed to send event:', err) - } -} diff --git a/electron/ipc/app-handlers.ts b/electron/ipc/app-handlers.ts index cf6effc5..bea2901d 100644 --- a/electron/ipc/app-handlers.ts +++ b/electron/ipc/app-handlers.ts @@ -6,7 +6,6 @@ import { checkGPU } from '../gpu' import { isPythonReady, downloadPythonEmbed } from '../python-setup' import { getBackendHealthStatus, startPythonBackend } from '../python-backend' import { getMainWindow } from '../window' -import { getAnalyticsState, setAnalyticsEnabled, sendAnalyticsEvent } from '../analytics' function getModelsPath(): string { const modelsPath = path.join(app.getPath('userData'), 'models') @@ -148,16 +147,4 @@ export function registerAppHandlers(): void { return getBackendHealthStatus() }) - ipcMain.handle('get-analytics-state', () => { - return getAnalyticsState() - }) - - ipcMain.handle('set-analytics-enabled', (_event, enabled: boolean) => { - setAnalyticsEnabled(enabled) - }) - - ipcMain.handle('send-analytics-event', async (_event, eventName: string, extraDetails?: Record | null) => { - await sendAnalyticsEvent(eventName, extraDetails) - }) - } diff --git a/electron/main.ts b/electron/main.ts index 2adee5a5..7a81f7c8 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -11,7 +11,6 @@ import { initSessionLog } from './logging-management' import { stopPythonBackend } from './python-backend' import { initAutoUpdater } from './updater' import { createWindow, getMainWindow } from './window' -import { sendAnalyticsEvent } from './analytics' const gotLock = app.requestSingleInstanceLock() @@ -48,9 +47,6 @@ if (!gotLock) { createWindow() // initAutoUpdater() // Python setup + backend start are now driven by the renderer via IPC - - // Fire analytics event (no-op if user hasn't opted in) - void sendAnalyticsEvent('ltxdesktop_app_launched') }) app.on('window-all-closed', () => { diff --git a/electron/preload.ts b/electron/preload.ts index 5f2efd2a..81aa0a4b 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -114,14 +114,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.send('renderer-console', level, args) }, - // Analytics - getAnalyticsState: (): Promise<{ analyticsEnabled: boolean; installationId: string }> => - ipcRenderer.invoke('get-analytics-state'), - setAnalyticsEnabled: (enabled: boolean): Promise => - ipcRenderer.invoke('set-analytics-enabled', enabled), - sendAnalyticsEvent: (eventName: string, extraDetails?: Record | null): Promise => - ipcRenderer.invoke('send-analytics-event', eventName, extraDetails), - // Platform info platform: process.platform, }) @@ -185,9 +177,6 @@ declare global { extractVideoFrame: (videoUrl: string, seekTime: number, width?: number, quality?: number) => Promise<{ path: string; url: string }> writeLog: (level: string, message: string) => Promise sendRendererLog: (level: 'log' | 'info' | 'warn' | 'error', ...args: unknown[]) => void - getAnalyticsState: () => Promise<{ analyticsEnabled: boolean; installationId: string }> - setAnalyticsEnabled: (enabled: boolean) => Promise - sendAnalyticsEvent: (eventName: string, extraDetails?: Record | null) => Promise platform: string } } diff --git a/frontend/components/SettingsModal.tsx b/frontend/components/SettingsModal.tsx index de439935..d9550436 100644 --- a/frontend/components/SettingsModal.tsx +++ b/frontend/components/SettingsModal.tsx @@ -43,7 +43,6 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp const [modelLicenseText, setModelLicenseText] = useState(null) const [modelLicenseLoading, setModelLicenseLoading] = useState(false) const [showModelLicense, setShowModelLicense] = useState(false) - const [analyticsEnabled, setAnalyticsEnabled] = useState(false) // Sync active tab with initialTab prop when modal opens useEffect(() => { @@ -99,14 +98,6 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp window.electronAPI.getAppInfo().then(info => setAppVersion(info.version)).catch(() => {}) }, [activeTab, appVersion]) - // Fetch analytics state when modal opens - useEffect(() => { - if (!isOpen) return - window.electronAPI.getAnalyticsState() - .then((state: { analyticsEnabled: boolean }) => setAnalyticsEnabled(state.analyticsEnabled)) - .catch(() => {}) - }, [isOpen]) - // Fetch text encoder status when modal opens useEffect(() => { if (!isOpen) return @@ -236,13 +227,6 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp onSettingsChange({ ...settings, promptEnhancerEnabledI2V: !settings.promptEnhancerEnabledI2V }) } } - // Analytics handler - const handleToggleAnalytics = () => { - const next = !analyticsEnabled - setAnalyticsEnabled(next) - window.electronAPI.setAnalyticsEnabled(next).catch(() => {}) - } - // Seed handlers const handleToggleSeedLock = () => { onSettingsChange({ @@ -708,42 +692,6 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp - {/* Anonymous Analytics Setting */} -
-
-
-
- - - - - - -
-

- Share anonymous usage data to help improve Director's Desktop. - Only basic technical information is collected — never personal data or generated content. -

-
- - {/* Toggle Switch */} - -
- -
)} diff --git a/frontend/vite-env.d.ts b/frontend/vite-env.d.ts index 8fb65219..8e68b0bf 100644 --- a/frontend/vite-env.d.ts +++ b/frontend/vite-env.d.ts @@ -58,9 +58,6 @@ interface Window { extractVideoFrame: (videoUrl: string, seekTime: number, width?: number, quality?: number) => Promise<{ path: string; url: string }> writeLog: (level: string, message: string) => Promise sendRendererLog: (level: 'log' | 'info' | 'warn' | 'error', ...args: unknown[]) => void - getAnalyticsState: () => Promise<{ analyticsEnabled: boolean; installationId: string }> - setAnalyticsEnabled: (enabled: boolean) => Promise - sendAnalyticsEvent: (eventName: string, extraDetails?: Record | null) => Promise platform: string } }