Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ 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,
};

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};
Expand Down Expand Up @@ -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" };

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
106 changes: 104 additions & 2 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<bool> {
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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How annoying would it be to have the text in just one place for CLI and UI?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is worth it, tbh. We could make it into a larger effort and share more text of course, but bridging between the CLI and the react code, that's quite a bit apart.

);
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())?;

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();

Expand Down
26 changes: 17 additions & 9 deletions crates/goose/src/posthog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,30 @@ static TELEMETRY_DISABLED_BY_ENV: Lazy<AtomicBool> = 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<bool> {
// 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::<bool>(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::<bool>(TELEMETRY_ENABLED_KEY)
.unwrap_or(true)
get_telemetry_choice().unwrap_or(false)
}

// ============================================================================
Expand Down
Loading