feat: Add opt-out setting for anonymous usage reporting (#743)

* refactor: align VS Code update checks with web runtime parity

- VS Code now uses file-based installId (shared with web server)
- Accepts platform/arch from webview for consistent behavior
- Usage data collection now matches web implementation

* feat: Add opt-out setting for anonymous usage reporting in Appearance

- Add privacy control in Appearance settings to opt-out of anonymous usage reports
- Usage data includes only app version, platform, and runtime - no personal data or code collected
- Setting persists across all runtimes and controls the reportUsage parameter in update checks
This commit is contained in:
Bohdan Triapitsyn
2026-03-22 23:02:53 +02:00
committed by GitHub
parent 53c2a0d919
commit 64b55025e1
9 changed files with 116 additions and 25 deletions
@@ -110,6 +110,7 @@ const VisualSectionContent: React.FC = () => {
'spacing',
'inputBarOffset',
...(!isVSCode ? ['terminalQuickKeys' as const] : []),
'reportUsage',
]} />;
};
@@ -143,7 +143,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
return mode === 'markdown' ? 'markdown' : 'plain';
};
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'activityRenderMode' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'showToolFileIcons' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck';
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'activityRenderMode' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'showToolFileIcons' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
interface OpenChamberVisualSettingsProps {
/** Which settings to show. If undefined, shows all. */
@@ -210,6 +210,14 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const [themesReloading, setThemesReloading] = React.useState(false);
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
const reportUsage = useUIStore(state => state.reportUsage);
const setReportUsage = useUIStore(state => state.setReportUsage);
// Sync reportUsage changes to server settings
const handleReportUsageChange = React.useCallback((enabled: boolean) => {
setReportUsage(enabled);
void updateDesktopSettings({ reportUsage: enabled });
}, [setReportUsage]);
const shouldAnimateChatPreview = isSettingsDialogOpen
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
@@ -1244,6 +1252,42 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</div>
)}
{/* --- Privacy & Data --- */}
{shouldShow('reportUsage') && (
<div className="space-y-3">
<section className="px-2 pb-2 pt-0">
<h4 className="typography-ui-header font-medium text-foreground mb-2">Privacy</h4>
<div className="flex items-start gap-2 py-1.5">
<Checkbox
checked={reportUsage}
onChange={handleReportUsageChange}
ariaLabel="Send anonymous usage reports"
/>
<div className="flex min-w-0 flex-col gap-0.5">
<div
className="group flex cursor-pointer"
role="button"
tabIndex={0}
aria-pressed={reportUsage}
onClick={() => handleReportUsageChange(!reportUsage)}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
handleReportUsageChange(!reportUsage);
}
}}
>
<span className="typography-ui-label text-foreground">Send anonymous usage reports</span>
</div>
<span className="typography-meta text-muted-foreground pointer-events-none">
Helps us understand which app versions are actively used so we can prioritize improvements. Only app version, platform, and runtime are collected - no personal data or code.
</span>
</div>
</div>
</section>
</div>
)}
</div>
);
};
+2
View File
@@ -144,6 +144,8 @@ export type DesktopSettings = {
// User-added skills catalogs (persisted to ~/.config/openchamber/settings.json)
skillCatalogs?: SkillCatalogConfig[];
// Opt-in to send anonymous usage reports for update checks (default: true)
reportUsage?: boolean;
};
type TauriGlobal = {
+7
View File
@@ -393,6 +393,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.stickyUserHeader === 'boolean' && settings.stickyUserHeader !== store.stickyUserHeader) {
store.setStickyUserHeader(settings.stickyUserHeader);
}
if (typeof settings.reportUsage === 'boolean' && settings.reportUsage !== store.reportUsage) {
store.setReportUsage(settings.reportUsage);
}
if (typeof settings.fontSize === 'number' && Number.isFinite(settings.fontSize) && settings.fontSize !== store.fontSize) {
store.setFontSize(settings.fontSize);
}
@@ -840,6 +843,10 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
result.skillCatalogs = skillCatalogs;
}
if (typeof candidate.reportUsage === 'boolean') {
result.reportUsage = candidate.reportUsage;
}
return result;
};
+7 -3
View File
@@ -558,10 +558,8 @@ interface UIStore {
stickyUserHeader: boolean;
showMobileSessionStatusBar: boolean;
isMobileSessionStatusBarCollapsed: boolean;
viewPagerPage: 'left' | 'center' | 'right';
isExpandedInput: boolean;
reportUsage: boolean;
shortcutOverrides: Record<string, ShortcutCombo>;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
@@ -669,11 +667,13 @@ interface UIStore {
setStickyUserHeader: (value: boolean) => void;
setShowMobileSessionStatusBar: (value: boolean) => void;
setIsMobileSessionStatusBarCollapsed: (value: boolean) => void;
viewPagerPage: 'left' | 'center' | 'right';
setViewPagerPage: (page: 'left' | 'center' | 'right') => void;
toggleExpandedInput: () => void;
setExpandedInput: (value: boolean) => void;
openMultiRunLauncher: () => void;
openMultiRunLauncherWithPrompt: (prompt: string) => void;
setReportUsage: (value: boolean) => void;
setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void;
clearShortcutOverride: (actionId: string) => void;
resetAllShortcutOverrides: () => void;
@@ -782,6 +782,7 @@ export const useUIStore = create<UIStore>()(
showMobileSessionStatusBar: true,
isMobileSessionStatusBarCollapsed: false,
isExpandedInput: false,
reportUsage: true,
shortcutOverrides: {},
setTheme: (theme) => {
@@ -1690,6 +1691,9 @@ export const useUIStore = create<UIStore>()(
setIsMobileSessionStatusBarCollapsed: (value) => {
set({ isMobileSessionStatusBarCollapsed: value });
},
setReportUsage: (value) => {
set({ reportUsage: value });
},
viewPagerPage: 'center',
setViewPagerPage: (page: 'left' | 'center' | 'right') => {
set({ viewPagerPage: page });
+5 -1
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
import { getDeviceInfo } from '@/lib/device';
import { useUIStore } from './useUIStore';
import {
checkForDesktopUpdates,
downloadDesktopUpdate,
@@ -66,7 +67,10 @@ function detectPlatform(): 'macos' | 'windows' | 'linux' | 'web' {
}
function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
const params = new URLSearchParams({ reportUsage: 'true' });
// Check if user has opted out of usage reporting (default: true/enabled from UI store)
const shouldReportUsage = useUIStore.getState().reportUsage;
const params = new URLSearchParams({ reportUsage: shouldReportUsage ? 'true' : 'false' });
params.set('deviceClass', detectDeviceClass());
params.set('arch', detectArch());
params.set('platform', detectPlatform());