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
+40 -20
View File
@@ -113,9 +113,39 @@ const execFileAsync = promisify(execFile);
const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf'];
const OPENCHAMBER_SHARED_SETTINGS_PATH = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
const VSCODE_INSTALL_ID_KEY = 'openchamber.installId.vscode';
const UPDATE_CHECK_URL = process.env.OPENCHAMBER_UPDATE_API_URL || 'https://api.openchamber.dev/v1/update/check';
const getOpenChamberConfigDir = (): string => {
if (process.platform === 'win32') {
const appData = process.env.APPDATA;
if (appData) return path.join(appData, 'openchamber');
}
return path.join(os.homedir(), '.config', 'openchamber');
};
const sanitizeInstallScope = (scope: string): 'desktop-tauri' | 'vscode' | 'web' => {
if (scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope;
return 'web';
};
const getOrCreateInstallId = (scope: string): string => {
const configDir = getOpenChamberConfigDir();
const normalizedScope = sanitizeInstallScope(scope);
const idPath = path.join(configDir, `install-id-${normalizedScope}`);
try {
const existing = fs.readFileSync(idPath, 'utf8').trim();
if (existing) return existing;
} catch {
// Generate new id.
}
const installId = randomUUID();
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(idPath, `${installId}\n`, { encoding: 'utf8', mode: 0o600 });
return installId;
};
const mapNodePlatformToApiPlatform = (value: string): 'macos' | 'windows' | 'linux' | 'web' => {
if (value === 'darwin') return 'macos';
if (value === 'win32') return 'windows';
@@ -129,22 +159,6 @@ const mapNodeArchToApiArch = (value: string): 'arm64' | 'x64' | 'unknown' => {
return 'unknown';
};
const getOrCreateVSCodeInstallId = async (ctx?: BridgeContext): Promise<string> => {
const state = ctx?.context?.globalState;
if (state) {
const existing = state.get<string>(VSCODE_INSTALL_ID_KEY);
if (typeof existing === 'string' && existing.trim().length > 0) {
return existing.trim();
}
}
const generated = randomUUID();
if (state) {
await state.update(VSCODE_INSTALL_ID_KEY, generated);
}
return generated;
};
const guessMimeTypeFromExtension = (ext: string) => {
switch (ext) {
case '.png':
@@ -2935,14 +2949,20 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
const deviceClass = typeof body.deviceClass === 'string' && body.deviceClass.trim().length > 0
? body.deviceClass.trim()
: 'desktop';
const platformRaw = typeof body.platform === 'string' && body.platform.trim().length > 0
? body.platform.trim()
: os.platform();
const archRaw = typeof body.arch === 'string' && body.arch.trim().length > 0
? body.arch.trim()
: os.arch();
const reportUsage = body.reportUsage !== false;
const installId = await getOrCreateVSCodeInstallId(ctx);
const installId = getOrCreateInstallId('vscode');
const requestBody = {
appType: 'vscode',
deviceClass,
platform: mapNodePlatformToApiPlatform(os.platform()),
arch: mapNodeArchToApiArch(os.arch()),
platform: mapNodePlatformToApiPlatform(platformRaw),
arch: mapNodeArchToApiArch(archRaw),
channel: 'stable',
currentVersion,
installId,
+4
View File
@@ -754,12 +754,16 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
const currentVersion = url.searchParams.get('currentVersion') || undefined;
const instanceMode = url.searchParams.get('instanceMode') || 'local';
const deviceClass = url.searchParams.get('deviceClass') || 'desktop';
const platform = url.searchParams.get('platform') || undefined;
const arch = url.searchParams.get('arch') || undefined;
const reportUsageRaw = (url.searchParams.get('reportUsage') || 'true').toLowerCase();
const reportUsage = !(reportUsageRaw === 'false' || reportUsageRaw === '0' || reportUsageRaw === 'no');
const data = await sendBridgeMessage('api:openchamber:update-check', {
currentVersion,
instanceMode,
deviceClass,
platform,
arch,
reportUsage,
});
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });