diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 78bdcf9fc3..42d894fd3d 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -68,11 +68,13 @@ sigstore-verify = { version = "0.6", default-features = false } winapi = { workspace = true } [features] -default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] +default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] code-mode = ["goose/code-mode", "goose-acp/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] +telemetry = ["goose/telemetry"] +otel = ["goose/otel"] # disables the update command disable-update = [] rustls-tls = [ diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 143e3db27e..58735d42f0 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -3,12 +3,15 @@ use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as ClapShell}; use goose::builtin_extension::register_builtin_extensions; use goose::config::{Config, GooseMode}; +#[cfg(feature = "telemetry")] use goose::posthog::get_telemetry_choice; use goose::recipe::Recipe; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer}; -use crate::commands::configure::{configure_telemetry_consent_dialog, handle_configure}; +#[cfg(feature = "telemetry")] +use crate::commands::configure::configure_telemetry_consent_dialog; +use crate::commands::configure::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}; @@ -1108,6 +1111,7 @@ async fn handle_interactive_session( session_opts: SessionOptions, extension_opts: ExtensionOptions, ) -> Result<()> { + #[cfg(feature = "telemetry")] if get_telemetry_choice().is_none() { configure_telemetry_consent_dialog()?; } @@ -1332,6 +1336,7 @@ async fn handle_run_command( output_opts: OutputOptions, model_opts: ModelOptions, ) -> Result<()> { + #[cfg(feature = "telemetry")] if run_behavior.interactive && get_telemetry_choice().is_none() { configure_telemetry_consent_dialog()?; } @@ -1643,6 +1648,7 @@ async fn handle_default_session() -> Result<()> { return handle_configure().await; } + #[cfg(feature = "telemetry")] if get_telemetry_choice().is_none() { configure_telemetry_consent_dialog()?; } diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 338d5b92d2..4f9b29d7bf 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -20,6 +20,7 @@ use goose::config::{ PermissionManager, }; use goose::model::ModelConfig; +#[cfg(feature = "telemetry")] use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY}; use goose::providers::base::ConfigKey; use goose::providers::chatgpt_codex::reasoning_levels_for_model; @@ -44,6 +45,7 @@ pub async fn handle_configure() -> anyhow::Result<()> { } } +#[cfg(feature = "telemetry")] pub fn configure_telemetry_consent_dialog() -> anyhow::Result { let config = Config::global(); @@ -114,6 +116,7 @@ async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { ); println!(); + #[cfg(feature = "telemetry")] configure_telemetry_consent_dialog()?; println!(); @@ -1264,13 +1267,21 @@ 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( + #[allow(unused_mut)] + let mut setting_select = cliclack::select("What setting would you like to configure?").item( + "goose_mode", + "goose mode", + "Configure goose mode", + ); + #[cfg(feature = "telemetry")] + { + setting_select = setting_select.item( "telemetry", "Telemetry", "Enable or disable anonymous usage data collection", - ) + ); + } + let setting_type = setting_select .item( "tool_permission", "Tool Permission", @@ -1309,6 +1320,7 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { "goose_mode" => { configure_goose_mode_dialog()?; } + #[cfg(feature = "telemetry")] "telemetry" => { configure_telemetry_dialog()?; } @@ -1383,6 +1395,7 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> { Ok(()) } +#[cfg(feature = "telemetry")] pub fn configure_telemetry_dialog() -> anyhow::Result<()> { let config = Config::global(); diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index c22db9ef6d..f6cf0d9602 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -6,6 +6,7 @@ use tracing_subscriber::{ Registry, }; +#[cfg(feature = "otel")] use goose::otel::otlp; use goose::tracing::langfuse_layer; @@ -67,6 +68,7 @@ fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> { // Console logging disabled for CLI - all logs go to files only ]; + #[cfg(feature = "otel")] if !force { layers.extend(otlp::init_otlp_layers(goose::config::Config::global())); } diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 97baae1e5c..3530f5a37c 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -9,6 +9,7 @@ async fn main() -> Result<()> { let result = cli().await; + #[cfg(feature = "otel")] if goose::otel::otlp::is_otlp_initialized() { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; goose::otel::otlp::shutdown_otlp(); diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 9f711aeb33..948bad9a2b 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -579,6 +579,7 @@ async fn configure_session_prompts( } pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { + #[cfg(feature = "telemetry")] goose::posthog::set_session_context("cli", session_config.resume); let config = Config::global(); diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 2accd001e7..9621bed61c 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -11,11 +11,13 @@ description.workspace = true workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] +default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] code-mode = ["goose/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] +telemetry = ["goose/telemetry"] +otel = ["goose/otel"] rustls-tls = [ "reqwest/rustls", "tokio-tungstenite/rustls-tls-native-roots", diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index e1e642fac5..f06c464b94 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -119,6 +119,7 @@ pub async fn run() -> Result<()> { .await?; } + #[cfg(feature = "otel")] if goose::otel::otlp::is_otlp_initialized() { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; goose::otel::otlp::shutdown_otlp(); diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index 720b30d3f8..412d943946 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -5,6 +5,7 @@ use tracing_subscriber::{ Registry, }; +#[cfg(feature = "otel")] use goose::otel::otlp; use goose::tracing::langfuse_layer; @@ -55,6 +56,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { console_layer.with_filter(base_env_filter).boxed(), ]; + #[cfg(feature = "otel")] layers.extend(otlp::init_otlp_layers(goose::config::Config::global())); if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index f33e18e565..09b1d361cf 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -197,6 +197,7 @@ async fn start_agent( State(state): State>, Json(payload): Json, ) -> Result, ErrorResponse> { + #[cfg(feature = "telemetry")] goose::posthog::set_session_context("desktop", false); let StartAgentRequest { @@ -212,6 +213,7 @@ async fn start_agent( Ok(recipe) => Some(recipe), Err(err) => { error!("Failed to decode recipe deeplink: {}", err); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("recipe_deeplink_decode_failed", &err.to_string()); return Err(ErrorResponse { message: err.to_string(), @@ -253,6 +255,7 @@ async fn start_agent( .await .map_err(|err| { error!("Failed to create session: {}", err); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("session_create_failed", &err.to_string()); ErrorResponse { message: format!("Failed to create session: {}", err), @@ -370,6 +373,7 @@ async fn resume_agent( State(state): State>, Json(payload): Json, ) -> Result, ErrorResponse> { + #[cfg(feature = "telemetry")] goose::posthog::set_session_context("desktop", true); let session = state @@ -378,6 +382,7 @@ async fn resume_agent( .await .map_err(|err| { error!("Failed to resume session {}: {}", payload.session_id, err); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("session_resume_failed", &err.to_string()); ErrorResponse { message: format!("Failed to resume session: {}", err), @@ -702,6 +707,7 @@ async fn agent_add_extension( .add_extension(request.config, &request.session_id) .await .map_err(|e| { + #[cfg(feature = "telemetry")] goose::posthog::emit_error( "extension_add_failed", &format!("{}: {}", extension_name, e), diff --git a/crates/goose-server/src/routes/recipe.rs b/crates/goose-server/src/routes/recipe.rs index 4e44796b94..dcb452fca2 100644 --- a/crates/goose-server/src/routes/recipe.rs +++ b/crates/goose-server/src/routes/recipe.rs @@ -213,6 +213,7 @@ async fn create_recipe( } Err(e) => { tracing::error!("Error details: {:?}", e); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("recipe_create_failed", &e.to_string()); let error_response = CreateRecipeResponse { recipe: None, @@ -240,6 +241,7 @@ async fn encode_recipe( Ok(encoded) => Ok(Json(EncodeRecipeResponse { deeplink: encoded })), Err(err) => { tracing::error!("Failed to encode recipe: {}", err); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("recipe_encode_failed", &err.to_string()); Err(StatusCode::BAD_REQUEST) } @@ -266,6 +268,7 @@ async fn decode_recipe( }, Err(err) => { tracing::error!("Failed to decode deeplink: {}", err); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("recipe_decode_failed", &err.to_string()); Err(StatusCode::BAD_REQUEST) } @@ -392,6 +395,7 @@ async fn schedule_recipe( Ok(_) => Ok(StatusCode::OK), Err(e) => { tracing::error!("Failed to schedule recipe: {}", e); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("recipe_schedule_failed", &e.to_string()); Err(StatusCode::INTERNAL_SERVER_ERROR) } diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index b076dbc8bc..d66ed390e1 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -403,6 +403,7 @@ async fn fork_session( .await .map_err(|e| { tracing::error!("Failed to get session: {}", e); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("session_get_failed", &e.to_string()); ErrorResponse { message: if e.to_string().contains("not found") { @@ -423,6 +424,7 @@ async fn fork_session( .await .map_err(|e| { tracing::error!("Failed to copy session: {}", e); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("session_copy_failed", &e.to_string()); ErrorResponse { message: format!("Failed to copy session: {}", e), @@ -441,6 +443,7 @@ async fn fork_session( .await .map_err(|e| { tracing::error!("Failed to truncate conversation: {}", e); + #[cfg(feature = "telemetry")] goose::posthog::emit_error("session_truncate_failed", &e.to_string()); ErrorResponse { message: format!("Failed to truncate conversation: {}", e), diff --git a/crates/goose-server/src/routes/telemetry.rs b/crates/goose-server/src/routes/telemetry.rs index 2fde1a3cd2..697f579cf0 100644 --- a/crates/goose-server/src/routes/telemetry.rs +++ b/crates/goose-server/src/routes/telemetry.rs @@ -1,4 +1,5 @@ use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +#[cfg(feature = "telemetry")] use goose::posthog::emit_event; use serde::Deserialize; use std::collections::HashMap; @@ -29,6 +30,7 @@ async fn send_telemetry_event( let event_name = request.event_name; let properties = request.properties; + #[cfg(feature = "telemetry")] tokio::spawn(async move { if let Err(e) = emit_event(&event_name, properties).await { tracing::debug!("Failed to send telemetry event: {}", e); diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 9f5014b07e..30541339bb 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -8,7 +8,16 @@ repository.workspace = true description.workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] +default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] +telemetry = [] +otel = [ + "dep:tracing-opentelemetry", + "dep:opentelemetry", + "dep:opentelemetry_sdk", + "dep:opentelemetry-appender-tracing", + "dep:opentelemetry-otlp", + "dep:opentelemetry-stdout", +] code-mode = ["dep:pctx_code_mode"] local-inference = [ "dep:candle-core", @@ -90,12 +99,12 @@ lazy_static = "1.5.0" tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-futures = { workspace = true } -tracing-opentelemetry = { workspace = true } -opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true } -opentelemetry-appender-tracing = { workspace = true } -opentelemetry-otlp = { workspace = true } -opentelemetry-stdout = { workspace = true } +tracing-opentelemetry = { workspace = true, optional = true } +opentelemetry = { workspace = true, optional = true } +opentelemetry_sdk = { workspace = true, optional = true } +opentelemetry-appender-tracing = { workspace = true, optional = true } +opentelemetry-otlp = { workspace = true, optional = true } +opentelemetry-stdout = { workspace = true, optional = true } keyring = { version = "3.6.2", features = [ "apple-native", "windows-native", diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 3b30b31cdb..107c7fea85 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -583,6 +583,7 @@ impl Agent { ) .await; result.unwrap_or_else(|e| { + #[cfg(feature = "telemetry")] crate::posthog::emit_error( "tool_execution_failed", &format!("{}: {}", tool_call.name, e), @@ -943,6 +944,7 @@ impl Agent { let command = message_text.split_whitespace().next(); if let Some(cmd) = command { if crate::slash_commands::get_recipe_for_command(cmd).is_some() { + #[cfg(feature = "telemetry")] crate::posthog::emit_custom_slash_command_used(); } } @@ -1487,6 +1489,7 @@ impl Agent { } } Err(ref provider_err @ ProviderError::ContextLengthExceeded(_)) => { + #[cfg(feature = "telemetry")] crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); compaction_attempts += 1; @@ -1531,6 +1534,7 @@ impl Agent { break; } Err(e) => { + #[cfg(feature = "telemetry")] crate::posthog::emit_error("compaction_failed", &e.to_string()); error!("Compaction failed: {}", e); yield AgentEvent::Message( @@ -1543,6 +1547,7 @@ impl Agent { } } Err(ref provider_err @ ProviderError::CreditsExhausted { details: _, ref top_up_url }) => { + #[cfg(feature = "telemetry")] crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); error!("Error: {}", provider_err); @@ -1566,6 +1571,7 @@ impl Agent { break; } Err(ref provider_err @ ProviderError::NetworkError(_)) => { + #[cfg(feature = "telemetry")] crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); error!("Error: {}", provider_err); yield AgentEvent::Message( @@ -1576,6 +1582,7 @@ impl Agent { break; } Err(ref provider_err) => { + #[cfg(feature = "telemetry")] crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); error!("Error: {}", provider_err); yield AgentEvent::Message( diff --git a/crates/goose/src/agents/retry.rs b/crates/goose/src/agents/retry.rs index f8f1f0cebe..367c557ef5 100644 --- a/crates/goose/src/agents/retry.rs +++ b/crates/goose/src/agents/retry.rs @@ -140,6 +140,7 @@ impl RetryManager { "Maximum retry attempts ({}) exceeded", retry_config.max_retries ); + #[cfg(feature = "telemetry")] crate::posthog::emit_error( "retry_max_exceeded", &format!("Max retries ({}) exceeded", retry_config.max_retries), diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 1e7d7a92f0..694e0391a1 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -21,8 +21,10 @@ pub mod logging; pub mod mcp_utils; pub mod model; pub mod oauth; +#[cfg(feature = "otel")] pub mod otel; pub mod permission; +#[cfg(feature = "telemetry")] pub mod posthog; pub mod prompt_template; pub mod providers; diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 6612efcd42..970e32c707 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -18,6 +18,7 @@ use crate::config::paths::Paths; use crate::config::{resolve_extensions_for_new_session, Config}; use crate::conversation::message::Message; use crate::conversation::Conversation; +#[cfg(feature = "telemetry")] use crate::posthog; use crate::providers::create; use crate::recipe::Recipe; @@ -267,6 +268,7 @@ impl Scheduler { Ok(_) => tracing::info!("Job '{}' completed", task_job_id), Err(ref e) => { tracing::error!("Job '{}' failed: {}", task_job_id, e); + #[cfg(feature = "telemetry")] crate::posthog::emit_error("scheduler_job_failed", &e.to_string()); } } @@ -838,6 +840,7 @@ async fn execute_job( drop(jobs_guard); let start_time = std::time::Instant::now(); + #[cfg(feature = "telemetry")] tokio::spawn(async move { let mut props = HashMap::new(); props.insert( @@ -907,25 +910,28 @@ async fn execute_job( .apply() .await?; - let duration_secs = start_time.elapsed().as_secs(); - tokio::spawn(async move { - let mut props = HashMap::new(); - props.insert( - "trigger".to_string(), - serde_json::Value::String("automated".to_string()), - ); - props.insert( - "status".to_string(), - serde_json::Value::String("completed".to_string()), - ); - props.insert( - "duration_seconds".to_string(), - serde_json::Value::Number(serde_json::Number::from(duration_secs)), - ); - if let Err(e) = posthog::emit_event("schedule_job_completed", props).await { - tracing::debug!("Failed to send schedule telemetry: {}", e); - } - }); + #[cfg(feature = "telemetry")] + { + let duration_secs = start_time.elapsed().as_secs(); + tokio::spawn(async move { + let mut props = HashMap::new(); + props.insert( + "trigger".to_string(), + serde_json::Value::String("automated".to_string()), + ); + props.insert( + "status".to_string(), + serde_json::Value::String("completed".to_string()), + ); + props.insert( + "duration_seconds".to_string(), + serde_json::Value::Number(serde_json::Number::from(duration_secs)), + ); + if let Err(e) = posthog::emit_event("schedule_job_completed", props).await { + tracing::debug!("Failed to send schedule telemetry: {}", e); + } + }); + } Ok(session.id) } diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index a86465b199..da447d7ca6 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -967,6 +967,7 @@ impl SessionStorage { .await?; tx.commit().await?; + #[cfg(feature = "telemetry")] crate::posthog::emit_session_started(); Ok(session) } diff --git a/crates/goose/tests/session_id_propagation_test.rs b/crates/goose/tests/session_id_propagation_test.rs index 0f68e9851a..fe902dc9d1 100644 --- a/crates/goose/tests/session_id_propagation_test.rs +++ b/crates/goose/tests/session_id_propagation_test.rs @@ -4,14 +4,9 @@ use goose::providers::api_client::{ApiClient, AuthMethod}; use goose::providers::base::Provider; use goose::providers::openai::OpenAiProvider; use goose::session_context::SESSION_ID_HEADER; -use opentelemetry::logs::AnyValue; -use opentelemetry::Key; -use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; -use opentelemetry_sdk::logs::{InMemoryLogExporterBuilder, SdkLoggerProvider}; use serde_json::json; use std::sync::Arc; use std::sync::Mutex; -use tracing_subscriber::prelude::*; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, Request, ResponseTemplate}; @@ -110,7 +105,14 @@ async fn make_request(provider: &dyn Provider, session_id: &str) { } #[tokio::test] +#[cfg(feature = "otel")] async fn test_session_id_propagates_to_log_records() { + use opentelemetry::logs::AnyValue; + use opentelemetry::Key; + use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; + use opentelemetry_sdk::logs::{InMemoryLogExporterBuilder, SdkLoggerProvider}; + use tracing_subscriber::prelude::*; + let exporter = InMemoryLogExporterBuilder::default().build(); let provider = SdkLoggerProvider::builder() .with_simple_exporter(exporter.clone())