feat: add telemetry consent fields to Settings (Rust + TypeScript)

New fields: telemetry_consent, crash_reporting_enabled, analytics_enabled,
anonymous_id. All Option/null by default for backward compatibility with
existing settings.json files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-25 16:05:13 +01:00
parent 3028a3616c
commit d7166ea993
8 changed files with 97 additions and 8 deletions

View File

@@ -10,6 +10,10 @@ pub struct Settings {
pub github_token: Option<String>,
pub github_username: Option<String>,
pub auto_pull_interval_minutes: Option<u32>,
pub telemetry_consent: Option<bool>,
pub crash_reporting_enabled: Option<bool>,
pub analytics_enabled: Option<bool>,
pub anonymous_id: Option<String>,
}
fn settings_path() -> Result<PathBuf, String> {
@@ -56,6 +60,13 @@ fn save_settings_at(path: &PathBuf, settings: Settings) -> Result<(), String> {
.map(|k| k.trim().to_string())
.filter(|k| !k.is_empty()),
auto_pull_interval_minutes: settings.auto_pull_interval_minutes,
telemetry_consent: settings.telemetry_consent,
crash_reporting_enabled: settings.crash_reporting_enabled,
analytics_enabled: settings.analytics_enabled,
anonymous_id: settings
.anonymous_id
.map(|k| k.trim().to_string())
.filter(|k| !k.is_empty()),
};
let json = serde_json::to_string_pretty(&cleaned)
@@ -122,6 +133,10 @@ mod tests {
assert!(s.github_token.is_none());
assert!(s.github_username.is_none());
assert!(s.auto_pull_interval_minutes.is_none());
assert!(s.telemetry_consent.is_none());
assert!(s.crash_reporting_enabled.is_none());
assert!(s.analytics_enabled.is_none());
assert!(s.anonymous_id.is_none());
}
#[test]
@@ -132,6 +147,10 @@ mod tests {
google_key: Some("AIza-test".to_string()),
github_token: Some("gho_xyz789".to_string()),
github_username: Some("lucaong".to_string()),
telemetry_consent: Some(true),
crash_reporting_enabled: Some(true),
analytics_enabled: Some(false),
anonymous_id: Some("abc-123-uuid".to_string()),
..Default::default()
};
let json = serde_json::to_string(&settings).unwrap();
@@ -140,6 +159,10 @@ mod tests {
assert_eq!(parsed.google_key, settings.google_key);
assert_eq!(parsed.github_token, settings.github_token);
assert_eq!(parsed.github_username, settings.github_username);
assert_eq!(parsed.telemetry_consent, Some(true));
assert_eq!(parsed.crash_reporting_enabled, Some(true));
assert_eq!(parsed.analytics_enabled, Some(false));
assert_eq!(parsed.anonymous_id.as_deref(), Some("abc-123-uuid"));
}
#[test]
@@ -159,6 +182,7 @@ mod tests {
github_token: Some("gho_token123".to_string()),
github_username: Some("lucaong".to_string()),
auto_pull_interval_minutes: Some(10),
..Default::default()
});
assert_eq!(loaded.anthropic_key.as_deref(), Some("sk-ant-key"));
assert_eq!(loaded.openai_key.as_deref(), Some("sk-openai"));
@@ -223,6 +247,35 @@ mod tests {
assert!(err.contains("Failed to parse settings"));
}
#[test]
fn test_telemetry_fields_roundtrip() {
let loaded = save_and_reload(Settings {
telemetry_consent: Some(true),
crash_reporting_enabled: Some(true),
analytics_enabled: Some(false),
anonymous_id: Some("test-uuid-v4".to_string()),
..Default::default()
});
assert_eq!(loaded.telemetry_consent, Some(true));
assert_eq!(loaded.crash_reporting_enabled, Some(true));
assert_eq!(loaded.analytics_enabled, Some(false));
assert_eq!(loaded.anonymous_id.as_deref(), Some("test-uuid-v4"));
}
#[test]
fn test_old_settings_json_missing_telemetry_fields() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("settings.json");
// Simulate old settings.json without telemetry fields
fs::write(&path, r#"{"anthropic_key":"sk-test"}"#).unwrap();
let loaded = get_settings_at(&path).unwrap();
assert_eq!(loaded.anthropic_key.as_deref(), Some("sk-test"));
assert!(loaded.telemetry_consent.is_none());
assert!(loaded.crash_reporting_enabled.is_none());
assert!(loaded.analytics_enabled.is_none());
assert!(loaded.anonymous_id.is_none());
}
#[test]
fn test_settings_path_returns_ok() {
let result = settings_path();

View File

@@ -67,7 +67,7 @@ const mockCommandResults: Record<string, unknown> = {
get_modified_files: [],
get_note_content: mockAllContent['/vault/project/test.md'] || '',
get_file_history: [],
get_settings: { anthropic_key: null, openai_key: null, google_key: null, github_token: null, github_username: null, auto_pull_interval_minutes: null },
get_settings: { anthropic_key: null, openai_key: null, google_key: null, github_token: null, github_username: null, auto_pull_interval_minutes: null, telemetry_consent: true, crash_reporting_enabled: null, analytics_enabled: null, anonymous_id: null },
git_pull: { status: 'up_to_date', message: 'Already up to date', updatedFiles: [], conflictFiles: [] },
save_settings: null,
check_vault_exists: true,

View File

@@ -24,6 +24,10 @@ const emptySettings: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: null,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
const populatedSettings: Settings = {
@@ -33,6 +37,10 @@ const populatedSettings: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: 5,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
describe('SettingsPanel', () => {
@@ -87,14 +95,14 @@ describe('SettingsPanel', () => {
fireEvent.click(screen.getByTestId('settings-save'))
expect(onSave).toHaveBeenCalledWith({
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
anthropic_key: null,
openai_key: 'sk-openai-test',
google_key: null,
github_token: null,
github_username: null,
auto_pull_interval_minutes: 5,
})
}))
expect(onClose).toHaveBeenCalled()
})
@@ -108,14 +116,14 @@ describe('SettingsPanel', () => {
fireEvent.click(screen.getByTestId('settings-save'))
expect(onSave).toHaveBeenCalledWith({
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
anthropic_key: null,
openai_key: null,
google_key: null,
github_token: null,
github_username: null,
auto_pull_interval_minutes: 5,
})
}))
})
it('calls onClose when Cancel is clicked', () => {
@@ -150,14 +158,14 @@ describe('SettingsPanel', () => {
fireEvent.change(openaiInput, { target: { value: 'sk-openai-test' } })
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Enter', metaKey: true })
expect(onSave).toHaveBeenCalledWith({
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
anthropic_key: null,
openai_key: 'sk-openai-test',
google_key: null,
github_token: null,
github_username: null,
auto_pull_interval_minutes: 5,
})
}))
})
it('calls onClose when clicking backdrop', () => {

View File

@@ -142,7 +142,11 @@ function SettingsPanelInner({ settings, onSave, onClose }: Omit<SettingsPanelPro
github_token: ghOverride ? ghOverride.token : (githubToken ?? null),
github_username: ghOverride ? ghOverride.username : (githubUsername ?? null),
auto_pull_interval_minutes: pullInterval,
}), [openaiKey, googleKey, githubToken, githubUsername, pullInterval])
telemetry_consent: settings.telemetry_consent,
crash_reporting_enabled: settings.crash_reporting_enabled,
analytics_enabled: settings.analytics_enabled,
anonymous_id: settings.anonymous_id,
}), [openaiKey, googleKey, githubToken, githubUsername, pullInterval, settings.telemetry_consent, settings.crash_reporting_enabled, settings.analytics_enabled, settings.anonymous_id])
const handleSave = () => {
onSave(buildSettings())

View File

@@ -10,6 +10,10 @@ const defaultSettings: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: null,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
const savedSettings: Settings = {
@@ -19,6 +23,10 @@ const savedSettings: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: null,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
let mockSettingsStore: Settings = { ...defaultSettings }
@@ -80,6 +88,10 @@ describe('useSettings', () => {
github_token: null,
github_username: null,
auto_pull_interval_minutes: null,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
await act(async () => {

View File

@@ -14,6 +14,10 @@ const EMPTY_SETTINGS: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: null,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
export function useSettings() {

View File

@@ -81,6 +81,10 @@ let mockSettings: Settings = {
github_token: null,
github_username: null,
auto_pull_interval_minutes: 5,
telemetry_consent: null,
crash_reporting_enabled: null,
analytics_enabled: null,
anonymous_id: null,
}
let mockLastVaultPath: string | null = null

View File

@@ -77,6 +77,10 @@ export interface Settings {
github_token: string | null
github_username: string | null
auto_pull_interval_minutes: number | null
telemetry_consent: boolean | null
crash_reporting_enabled: boolean | null
analytics_enabled: boolean | null
anonymous_id: string | null
}
export interface GitPullResult {