diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 62d1e55e985e..0c0ea95ab5d6 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as ClapShell}; use goose::config::{Config, ExtensionConfig}; +use goose::posthog::get_telemetry_choice; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{ AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, @@ -9,7 +10,7 @@ use goose_mcp::{ use crate::commands::acp::run_acp_agent; use crate::commands::bench::agent_generator; -use crate::commands::configure::handle_configure; +use crate::commands::configure::{configure_telemetry_consent_dialog, handle_configure}; use crate::commands::info::handle_info; use crate::commands::project::{handle_project_default, handle_projects_interactive}; use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate}; @@ -1039,6 +1040,10 @@ async fn handle_interactive_session( session_opts: SessionOptions, extension_opts: ExtensionOptions, ) -> Result<()> { + if get_telemetry_choice().is_none() { + configure_telemetry_consent_dialog()?; + } + let session_start = std::time::Instant::now(); let session_type = if resume { "resumed" } else { "new" }; @@ -1247,6 +1252,10 @@ async fn handle_run_command( output_opts: OutputOptions, model_opts: ModelOptions, ) -> Result<()> { + if run_behavior.interactive && get_telemetry_choice().is_none() { + configure_telemetry_consent_dialog()?; + } + let parsed = parse_run_input(&input_opts, output_opts.quiet)?; let Some((input_config, recipe_info)) = parsed else { @@ -1397,6 +1406,10 @@ async fn handle_default_session() -> Result<()> { return handle_configure().await; } + if get_telemetry_choice().is_none() { + configure_telemetry_consent_dialog()?; + } + let session_id = get_or_create_session_id(None, false, false).await?; let mut session = build_session(SessionBuilderConfig { diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 8e652e3f4abb..4947ea28ea50 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -19,6 +19,7 @@ use goose::config::{ }; use goose::conversation::message::Message; use goose::model::ModelConfig; +use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY}; use goose::providers::provider_test::test_provider_configuration; use goose::providers::{create, providers, retry_operation, RetryConfig}; use goose::session::{SessionManager, SessionType}; @@ -39,16 +40,78 @@ pub async fn handle_configure() -> anyhow::Result<()> { } } -async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { +pub fn configure_telemetry_consent_dialog() -> anyhow::Result { + let config = Config::global(); + + println!(); + println!("{}", style("Help improve goose").bold()); + println!(); + println!( + "{}", + style("Would you like to help improve goose by sharing anonymous usage data?").dim() + ); + println!( + "{}", + style("This helps us understand how goose is used and identify areas for improvement.") + .dim() + ); println!(); + println!("{}", style("What we collect:").dim()); + println!( + "{}", + style(" • Operating system, version, and architecture").dim() + ); + println!("{}", style(" • goose version and install method").dim()); + println!("{}", style(" • Provider and model used").dim()); println!( "{}", - style("Welcome to goose! Let's get you set up with a provider.").dim() + style(" • Extensions and tool usage counts (names only)").dim() ); + println!( + "{}", + style(" • Session metrics (duration, interaction count, token usage)").dim() + ); + println!( + "{}", + style(" • Error types (e.g., \"rate_limit\", \"auth\" - no details)").dim() + ); + println!(); + println!( + "{}", + style("We never collect your conversations, code, tool arguments, error messages,").dim() + ); + println!( + "{}", + style("or any personal data. You can change this anytime with 'goose configure'.").dim() + ); + println!(); + + let enabled = cliclack::confirm("Share anonymous usage data to help improve goose?") + .initial_value(true) + .interact()?; + + config.set_param(TELEMETRY_ENABLED_KEY, enabled)?; + + if enabled { + let _ = cliclack::log::success("Thank you for helping improve goose!"); + } else { + let _ = cliclack::log::info("Telemetry disabled. You can enable it anytime in settings."); + } + + Ok(enabled) +} + +async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { + println!(); + println!("{}", style("Welcome to goose! Let's get you set up.").dim()); println!( "{}", style(" you can rerun this command later to update your configuration").dim() ); + println!(); + + configure_telemetry_consent_dialog()?; + println!(); cliclack::intro(style(" goose-configure ").on_cyan().black())?; @@ -1031,6 +1094,11 @@ pub fn remove_extension_dialog() -> anyhow::Result<()> { pub async fn configure_settings_dialog() -> anyhow::Result<()> { let setting_type = cliclack::select("What setting would you like to configure?") .item("goose_mode", "goose mode", "Configure goose mode") + .item( + "telemetry", + "Telemetry", + "Enable or disable anonymous usage data collection", + ) .item( "tool_permission", "Tool Permission", @@ -1069,6 +1137,9 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { "goose_mode" => { configure_goose_mode_dialog()?; } + "telemetry" => { + configure_telemetry_dialog()?; + } "tool_permission" => { configure_tool_permissions_dialog().await.and(Ok(()))?; // No need to print config file path since it's already handled. @@ -1140,6 +1211,37 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> { Ok(()) } +pub fn configure_telemetry_dialog() -> anyhow::Result<()> { + let config = Config::global(); + + if std::env::var("GOOSE_TELEMETRY_OFF").is_ok() { + let _ = cliclack::log::info("Notice: GOOSE_TELEMETRY_OFF environment variable is set and will override the configuration here."); + } + + let current_choice = get_telemetry_choice(); + let current_status = match current_choice { + Some(true) => "Enabled", + Some(false) => "Disabled", + None => "Not set", + }; + + let _ = cliclack::log::info(format!("Current telemetry status: {}", current_status)); + + let enabled = cliclack::confirm("Share anonymous usage data to help improve goose?") + .initial_value(current_choice.unwrap_or(true)) + .interact()?; + + config.set_param(TELEMETRY_ENABLED_KEY, enabled)?; + + if enabled { + cliclack::outro("Telemetry enabled - thank you for helping improve goose!")?; + } else { + cliclack::outro("Telemetry disabled")?; + } + + Ok(()) +} + pub fn configure_tool_output_dialog() -> anyhow::Result<()> { let config = Config::global(); diff --git a/crates/goose/src/posthog.rs b/crates/goose/src/posthog.rs index 540a4868b662..42ff2f8f2103 100644 --- a/crates/goose/src/posthog.rs +++ b/crates/goose/src/posthog.rs @@ -24,22 +24,30 @@ static TELEMETRY_DISABLED_BY_ENV: Lazy = Lazy::new(|| { .into() }); +/// Check if the user has made a telemetry choice. +/// +/// Returns Some(true) if telemetry is enabled, Some(false) if disabled, +/// or None if the user hasn't made a choice yet. +pub fn get_telemetry_choice() -> Option { + // If disabled by env var, treat as explicit choice to disable + if TELEMETRY_DISABLED_BY_ENV.load(Ordering::Relaxed) { + return Some(false); + } + + let config = Config::global(); + config.get_param::(TELEMETRY_ENABLED_KEY).ok() +} + /// 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 +/// - User has not made a telemetry choice yet (opt-in required) /// -/// Returns true otherwise (telemetry is opt-out, enabled by default) +/// Returns true only if the user has explicitly opted in. pub fn is_telemetry_enabled() -> bool { - if TELEMETRY_DISABLED_BY_ENV.load(Ordering::Relaxed) { - return false; - } - - let config = Config::global(); - config - .get_param::(TELEMETRY_ENABLED_KEY) - .unwrap_or(true) + get_telemetry_choice().unwrap_or(false) } // ============================================================================