From bef0fab985957a9d58a7eeddea836978a2d646a0 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 9 Dec 2025 10:30:41 -0800 Subject: [PATCH 01/13] add telemetry config management with related UI dialog and settings --- crates/goose-server/src/openapi.rs | 4 + .../src/routes/config_management.rs | 49 +++++++++ crates/goose/src/posthog.rs | 31 +++++- ui/desktop/openapi.json | 73 +++++++++++++ ui/desktop/src/App.tsx | 2 + ui/desktop/src/api/sdk.gen.ts | 13 ++- ui/desktop/src/api/types.gen.ts | 47 ++++++++ .../src/components/TelemetryOptOutModal.tsx | 100 ++++++++++++++++++ .../settings/app/AppSettingsSection.tsx | 62 ++++++++++- ui/desktop/src/updates.ts | 1 + 10 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 ui/desktop/src/components/TelemetryOptOutModal.tsx diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 8909234e7f4e..1774ad6c1682 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -350,6 +350,8 @@ derive_utoipa!(Icon as IconSchema); super::routes::config_management::remove_custom_provider, super::routes::config_management::check_provider, super::routes::config_management::set_config_provider, + super::routes::config_management::get_telemetry_status, + super::routes::config_management::set_telemetry_status, super::routes::agent::start_agent, super::routes::agent::resume_agent, super::routes::agent::get_tools, @@ -411,6 +413,8 @@ derive_utoipa!(Icon as IconSchema); super::routes::config_management::UpdateCustomProviderRequest, super::routes::config_management::CheckProviderRequest, super::routes::config_management::SetProviderRequest, + super::routes::config_management::TelemetryStatusResponse, + super::routes::config_management::SetTelemetryRequest, super::routes::action_required::ConfirmToolActionRequest, super::routes::reply::ChatRequest, super::routes::session::ImportSessionRequest, diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 5e2001af4b48..6c72c306794b 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -802,6 +802,51 @@ pub async fn check_provider( Ok(()) } +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TelemetryStatusResponse { + pub enabled: bool, +} + +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SetTelemetryRequest { + pub enabled: bool, +} + +#[utoipa::path( + get, + path = "/config/telemetry", + responses( + (status = 200, description = "Telemetry status retrieved successfully", body = TelemetryStatusResponse) + ) +)] +pub async fn get_telemetry_status() -> Json { + Json(TelemetryStatusResponse { + enabled: goose::posthog::is_telemetry_enabled(), + }) +} + +#[utoipa::path( + post, + path = "/config/telemetry", + request_body = SetTelemetryRequest, + responses( + (status = 200, description = "Telemetry preference updated successfully", body = TelemetryStatusResponse), + (status = 500, description = "Internal server error") + ) +)] +pub async fn set_telemetry_status( + Json(request): Json, +) -> Result, StatusCode> { + goose::posthog::set_telemetry_enabled(request.enabled) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(TelemetryStatusResponse { + enabled: goose::posthog::is_telemetry_enabled(), + })) +} + #[utoipa::path( post, path = "/config/set_provider", @@ -850,6 +895,10 @@ pub fn routes(state: Arc) -> Router { .route("/config/custom-providers/{id}", get(get_custom_provider)) .route("/config/check_provider", post(check_provider)) .route("/config/set_provider", post(set_config_provider)) + .route( + "/config/telemetry", + get(get_telemetry_status).post(set_telemetry_status), + ) .with_state(state) } diff --git a/crates/goose/src/posthog.rs b/crates/goose/src/posthog.rs index c0401160af33..59b5181bb3b6 100644 --- a/crates/goose/src/posthog.rs +++ b/crates/goose/src/posthog.rs @@ -7,15 +7,42 @@ use std::sync::atomic::{AtomicBool, Ordering}; const POSTHOG_API_KEY: &str = "phc_RyX5CaY01VtZJCQyhSR5KFh6qimUy81YwxsEpotAftT"; -static TELEMETRY_DISABLED: Lazy = Lazy::new(|| { +/// Config key for telemetry opt-out preference +pub const TELEMETRY_ENABLED_KEY: &str = "GOOSE_TELEMETRY_ENABLED"; + +static TELEMETRY_DISABLED_BY_ENV: Lazy = Lazy::new(|| { std::env::var("GOOSE_TELEMETRY_OFF") .map(|v| v == "1" || v.to_lowercase() == "true") .unwrap_or(false) .into() }); +/// Check if telemetry is enabled. +/// +/// Returns false if: +/// - GOOSE_TELEMETRY_OFF environment variable is set to "1" or "true" +/// - GOOSE_TELEMETRY_ENABLED config value is set to false +/// +/// Returns true otherwise (telemetry is opt-out, enabled by default) +pub fn is_telemetry_enabled() -> bool { + // Environment variable takes precedence + if TELEMETRY_DISABLED_BY_ENV.load(Ordering::Relaxed) { + return false; + } + + let config = Config::global(); + config + .get_param::(TELEMETRY_ENABLED_KEY) + .unwrap_or(true) +} + +pub fn set_telemetry_enabled(enabled: bool) -> Result<(), crate::config::ConfigError> { + let config = Config::global(); + config.set_param(TELEMETRY_ENABLED_KEY, enabled) +} + pub fn emit_session_started() { - if TELEMETRY_DISABLED.load(Ordering::Relaxed) { + if !is_telemetry_enabled() { return; } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f8acee9d6302..eb748fd86d14 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -932,6 +932,57 @@ } } }, + "/config/telemetry": { + "get": { + "tags": [ + "super::routes::config_management" + ], + "operationId": "get_telemetry_status", + "responses": { + "200": { + "description": "Telemetry status retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TelemetryStatusResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "super::routes::config_management" + ], + "operationId": "set_telemetry_status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetTelemetryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Telemetry preference updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TelemetryStatusResponse" + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/config/upsert": { "post": { "tags": [ @@ -4871,6 +4922,17 @@ } } }, + "SetTelemetryRequest": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "Settings": { "type": "object", "properties": { @@ -5043,6 +5105,17 @@ "inlineMessage" ] }, + "TelemetryStatusResponse": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "TextContent": { "type": "object", "required": [ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 973b42383810..db68485f8f93 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -14,6 +14,7 @@ import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; import { ToastContainer } from 'react-toastify'; import AnnouncementModal from './components/AnnouncementModal'; +import TelemetryOptOutModal from './components/TelemetryOptOutModal'; import ProviderGuard from './components/ProviderGuard'; import { createSession } from './sessions'; @@ -697,6 +698,7 @@ export default function App() { + ); } diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 2e188b181997..d4dd39ea1118 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetTelemetryStatusData, GetTelemetryStatusResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetTelemetryStatusData, SetTelemetryStatusErrors, SetTelemetryStatusResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -186,6 +186,17 @@ export const setConfigProvider = (options: export const getSlashCommands = (options?: Options) => (options?.client ?? client).get({ url: '/config/slash_commands', ...options }); +export const getTelemetryStatus = (options?: Options) => (options?.client ?? client).get({ url: '/config/telemetry', ...options }); + +export const setTelemetryStatus = (options: Options) => (options.client ?? client).post({ + url: '/config/telemetry', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const upsertConfig = (options: Options) => (options.client ?? client).post({ url: '/config/upsert', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index e082260e9b77..1a843f50ae5e 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -791,6 +791,10 @@ export type SetSlashCommandRequest = { slash_command?: string | null; }; +export type SetTelemetryRequest = { + enabled: boolean; +}; + export type Settings = { goose_model?: string | null; goose_provider?: string | null; @@ -847,6 +851,10 @@ export type SystemNotificationContent = { export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage'; +export type TelemetryStatusResponse = { + enabled: boolean; +}; + export type TextContent = { _meta?: { [key: string]: unknown; @@ -1719,6 +1727,45 @@ export type GetSlashCommandsResponses = { export type GetSlashCommandsResponse = GetSlashCommandsResponses[keyof GetSlashCommandsResponses]; +export type GetTelemetryStatusData = { + body?: never; + path?: never; + query?: never; + url: '/config/telemetry'; +}; + +export type GetTelemetryStatusResponses = { + /** + * Telemetry status retrieved successfully + */ + 200: TelemetryStatusResponse; +}; + +export type GetTelemetryStatusResponse = GetTelemetryStatusResponses[keyof GetTelemetryStatusResponses]; + +export type SetTelemetryStatusData = { + body: SetTelemetryRequest; + path?: never; + query?: never; + url: '/config/telemetry'; +}; + +export type SetTelemetryStatusErrors = { + /** + * Internal server error + */ + 500: unknown; +}; + +export type SetTelemetryStatusResponses = { + /** + * Telemetry preference updated successfully + */ + 200: TelemetryStatusResponse; +}; + +export type SetTelemetryStatusResponse = SetTelemetryStatusResponses[keyof SetTelemetryStatusResponses]; + export type UpsertConfigData = { body: UpsertConfigQuery; path?: never; diff --git a/ui/desktop/src/components/TelemetryOptOutModal.tsx b/ui/desktop/src/components/TelemetryOptOutModal.tsx new file mode 100644 index 000000000000..b3f72f4cfe5c --- /dev/null +++ b/ui/desktop/src/components/TelemetryOptOutModal.tsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import { BaseModal } from './ui/BaseModal'; +import { Button } from './ui/button'; +import { readConfig, setTelemetryStatus } from '../api'; +import { Goose } from './icons/Goose'; +import { TELEMETRY_UI_ENABLED } from '../updates'; + +export default function TelemetryOptOutModal() { + const [showModal, setShowModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const checkTelemetryChoice = async () => { + try { + const response = await readConfig({ + body: { key: 'GOOSE_TELEMETRY_ENABLED', is_secret: false }, + }); + + if (response.data === null || response.data === undefined) { + setShowModal(true); + } + } catch (error) { + console.error('Failed to check telemetry config:', error); + setShowModal(true); + } + }; + + checkTelemetryChoice(); + }, []); + + const handleChoice = async (enabled: boolean) => { + setIsLoading(true); + try { + await setTelemetryStatus({ body: { enabled } }); + setShowModal(false); + } catch (error) { + console.error('Failed to set telemetry preference:', error); + setShowModal(false); + } finally { + setIsLoading(false); + } + }; + + if (!TELEMETRY_UI_ENABLED || !showModal) { + return null; + } + + return ( + + + + + } + > +
+
+ +
+

+ Help improve goose +

+

+ Would you like to help improve goose by sharing anonymous usage data? This helps us + understand how goose is used and identify areas for improvement. +

+
+

What we collect:

+
    +
  • Operating system and architecture
  • +
  • goose version
  • +
  • Provider and model used
  • +
  • Number of extensions enabled
  • +
  • Session count and token usage (aggregated)
  • +
+

+ We never collect your conversations, code, or any personal data. You can change this + setting anytime in Settings → App. +

+
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index f5564521d80c..e8f688f8271f 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -5,12 +5,13 @@ import { Settings, RefreshCw, ExternalLink } from 'lucide-react'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog'; import UpdateSection from './UpdateSection'; import TunnelSection from '../tunnel/TunnelSection'; -import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates'; +import { COST_TRACKING_ENABLED, UPDATES_ENABLED, TELEMETRY_UI_ENABLED } from '../../../updates'; import { getApiUrl } from '../../../config'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; import ThemeSelector from '../../GooseSidebar/ThemeSelector'; import BlockLogoBlack from './icons/block-lockup_black.png'; import BlockLogoWhite from './icons/block-lockup_white.png'; +import { getTelemetryStatus, setTelemetryStatus } from '../../../api'; interface AppSettingsSectionProps { scrollToSection?: string; @@ -28,6 +29,8 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti const [isRefreshing, setIsRefreshing] = useState(false); const [showPricing, setShowPricing] = useState(true); const [isDarkMode, setIsDarkMode] = useState(false); + const [telemetryEnabled, setTelemetryEnabled] = useState(true); + const [isTelemetryLoading, setIsTelemetryLoading] = useState(true); const updateSectionRef = useRef(null); // Check if GOOSE_VERSION is set to determine if Updates section should be shown @@ -68,6 +71,33 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti checkPricingStatus(); }, []); + useEffect(() => { + const loadTelemetryStatus = async () => { + try { + const response = await getTelemetryStatus(); + if (response.data) { + setTelemetryEnabled(response.data.enabled); + } + } catch (error) { + console.error('Failed to load telemetry status:', error); + } finally { + setIsTelemetryLoading(false); + } + }; + loadTelemetryStatus(); + }, []); + + const handleTelemetryToggle = async (checked: boolean) => { + try { + const response = await setTelemetryStatus({ body: { enabled: checked } }); + if (response.data) { + setTelemetryEnabled(response.data.enabled); + } + } catch (error) { + console.error('Failed to update telemetry status:', error); + } + }; + const checkPricingStatus = async () => { try { const apiUrl = getApiUrl('/config/pricing'); @@ -288,7 +318,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti

Prevent Sleep

- Keep your computer awake while Goose is running a task (screen can still lock) + Keep your computer awake while goose is running a task (screen can still lock)

@@ -397,6 +427,34 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti + {TELEMETRY_UI_ENABLED && ( + + + Privacy + Control how your data is used + + +
+
+

Anonymous usage data

+

+ Help improve goose by sharing anonymous usage statistics (OS, version, provider, + extension count). No conversations or personal data is collected. +

+
+
+ +
+
+
+
+ )} + Help & feedback diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index f385af4be5e8..756e73682c3e 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -3,3 +3,4 @@ export const COST_TRACKING_ENABLED = true; export const ANNOUNCEMENTS_ENABLED = false; export const CONFIGURATION_ENABLED = true; export const VOICE_DICTATION_ELEVENLABS_ENABLED = true; +export const TELEMETRY_UI_ENABLED = true; From 5ba9eba696c19f0b6c986c9cb2024acddbbd6af9 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 9 Dec 2025 11:37:36 -0800 Subject: [PATCH 02/13] add section to onboarding and show modal for more info if on onboarding or settings --- ui/desktop/src/components/ProviderGuard.tsx | 4 + .../src/components/TelemetryOptOutModal.tsx | 48 ++++++-- .../settings/app/AppSettingsSection.tsx | 61 +--------- .../settings/app/TelemetrySettings.tsx | 105 ++++++++++++++++++ 4 files changed, 153 insertions(+), 65 deletions(-) create mode 100644 ui/desktop/src/components/settings/app/TelemetrySettings.tsx diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 227be70e49bd..c09d4503453d 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -10,6 +10,7 @@ import { OllamaSetup } from './OllamaSetup'; import ApiKeyTester from './ApiKeyTester'; import { SwitchModelModal } from './settings/models/subcomponents/SwitchModelModal'; import { createNavigationHandler } from '../utils/navigationUtils'; +import TelemetrySettings from './settings/app/TelemetrySettings'; import { Goose, OpenRouter, Tetrate } from './icons'; @@ -306,6 +307,9 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG Go to Provider Settings →
+
+ +
diff --git a/ui/desktop/src/components/TelemetryOptOutModal.tsx b/ui/desktop/src/components/TelemetryOptOutModal.tsx index b3f72f4cfe5c..1b4c54369a5b 100644 --- a/ui/desktop/src/components/TelemetryOptOutModal.tsx +++ b/ui/desktop/src/components/TelemetryOptOutModal.tsx @@ -5,49 +5,83 @@ import { readConfig, setTelemetryStatus } from '../api'; import { Goose } from './icons/Goose'; import { TELEMETRY_UI_ENABLED } from '../updates'; -export default function TelemetryOptOutModal() { +interface TelemetryOptOutModalProps { + isOpen?: boolean; + onClose?: () => void; + showOnFirstLaunch?: boolean; +} + +export default function TelemetryOptOutModal({ + isOpen: controlledIsOpen, + onClose, + showOnFirstLaunch = true, +}: TelemetryOptOutModalProps) { const [showModal, setShowModal] = useState(false); const [isLoading, setIsLoading] = useState(false); + // Check if user has made a telemetry choice (only for first launch mode) + // Only show for existing users who have a provider but haven't made a telemetry choice useEffect(() => { + if (!showOnFirstLaunch) return; + const checkTelemetryChoice = async () => { try { - const response = await readConfig({ + // First check if user has a provider configured (existing user) + const providerResponse = await readConfig({ + body: { key: 'GOOSE_PROVIDER', is_secret: false }, + }); + + // If no provider, user is new and will see the inline settings on Welcome page + if (!providerResponse.data || providerResponse.data === '') { + return; + } + + // User has a provider, check if they've made a telemetry choice + const telemetryResponse = await readConfig({ body: { key: 'GOOSE_TELEMETRY_ENABLED', is_secret: false }, }); - if (response.data === null || response.data === undefined) { + // If the config value is null/undefined, user hasn't made a choice yet + if (telemetryResponse.data === null || telemetryResponse.data === undefined) { setShowModal(true); } } catch (error) { console.error('Failed to check telemetry config:', error); - setShowModal(true); } }; checkTelemetryChoice(); - }, []); + }, [showOnFirstLaunch]); const handleChoice = async (enabled: boolean) => { setIsLoading(true); try { await setTelemetryStatus({ body: { enabled } }); setShowModal(false); + onClose?.(); } catch (error) { console.error('Failed to set telemetry preference:', error); setShowModal(false); + onClose?.(); } finally { setIsLoading(false); } }; - if (!TELEMETRY_UI_ENABLED || !showModal) { + if (!TELEMETRY_UI_ENABLED) { + return null; + } + + // Use controlled state if provided, otherwise use internal state + const isModalOpen = controlledIsOpen !== undefined ? controlledIsOpen : showModal; + + if (!isModalOpen) { return null; } return ( +

+ +
+ +
+ + ); + + const modal = ( + + ); + + if (variant === 'inline') { + return ( + <> + {content} + {modal} + + ); + } + + return ( + <> + + + Privacy + Control how your data is used + + {content} + + {modal} + + ); +} From ab0bd234a66e58449769edaa0dbdb6aa16f11c2c Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 9 Dec 2025 11:53:01 -0800 Subject: [PATCH 03/13] fix not being able to clear modal selector --- .../models/subcomponents/SwitchModelModal.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index 73bb0c8bcb1a..a1c346359093 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -92,6 +92,7 @@ export const SwitchModelModal = ({ const [selectedPredefinedModel, setSelectedPredefinedModel] = useState(null); const [predefinedModels, setPredefinedModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); + const [userClearedModel, setUserClearedModel] = useState(false); // Validate form data const validateForm = useCallback(() => { @@ -265,7 +266,8 @@ export const SwitchModelModal = ({ : []; useEffect(() => { - if (!provider || loadingModels || model || isCustomModel) return; + // Don't auto-select if user explicitly cleared the model + if (!provider || loadingModels || model || isCustomModel || userClearedModel) return; const providerModels = modelOptions .filter((group) => group.options[0]?.provider === provider) @@ -277,7 +279,7 @@ export const SwitchModelModal = ({ setModel(preferredModel); } } - }, [provider, modelOptions, loadingModels, model, isCustomModel]); + }, [provider, modelOptions, loadingModels, model, isCustomModel, userClearedModel]); // Handle model selection change const handleModelChange = (newValue: unknown) => { @@ -285,9 +287,16 @@ export const SwitchModelModal = ({ if (selectedOption?.value === 'custom') { setIsCustomModel(true); setModel(''); + setUserClearedModel(false); + } else if (selectedOption === null) { + // User cleared the selection + setIsCustomModel(false); + setModel(''); + setUserClearedModel(true); } else { setIsCustomModel(false); setModel(selectedOption?.value || ''); + setUserClearedModel(false); } }; @@ -428,6 +437,7 @@ export const SwitchModelModal = ({ setProvider(option?.value || null); setModel(''); setIsCustomModel(false); + setUserClearedModel(false); } }} placeholder="Provider, type to search" @@ -445,26 +455,19 @@ export const SwitchModelModal = ({