diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 68739c008..d9aaabd1c 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -44,6 +44,7 @@ pub async fn run_main( codex_linux_sandbox_exe: Option, cli_config_overrides: CliConfigOverrides, loader_overrides: LoaderOverrides, + default_analytics_enabled: bool, ) -> IoResult<()> { // Set up channels. let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); @@ -96,7 +97,7 @@ pub async fn run_main( &config, env!("CARGO_PKG_VERSION"), Some("codex_app_server"), - false, + default_analytics_enabled, ) .map_err(|e| { std::io::Error::new( diff --git a/codex-rs/app-server/src/main.rs b/codex-rs/app-server/src/main.rs index be57311e8..71d6dc338 100644 --- a/codex-rs/app-server/src/main.rs +++ b/codex-rs/app-server/src/main.rs @@ -20,6 +20,7 @@ fn main() -> anyhow::Result<()> { codex_linux_sandbox_exe, CliConfigOverrides::default(), loader_overrides, + false, ) .await?; Ok(()) diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs new file mode 100644 index 000000000..e18a0d3c8 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use codex_core::config::ConfigBuilder; +use codex_core::config::types::OtelExporterKind; +use codex_core::config::types::OtelHttpProtocol; +use pretty_assertions::assert_eq; +use std::collections::HashMap; +use tempfile::TempDir; + +const SERVICE_VERSION: &str = "0.0.0-test"; + +fn set_metrics_exporter(config: &mut codex_core::config::Config) { + config.otel.metrics_exporter = OtelExporterKind::OtlpHttp { + endpoint: "http://localhost:4318".to_string(), + headers: HashMap::new(), + protocol: OtelHttpProtocol::Json, + tls: None, + }; +} + +#[tokio::test] +async fn app_server_default_analytics_disabled_without_flag() -> Result<()> { + let codex_home = TempDir::new()?; + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await?; + set_metrics_exporter(&mut config); + config.analytics_enabled = None; + + let provider = codex_core::otel_init::build_provider( + &config, + SERVICE_VERSION, + Some("codex_app_server"), + false, + ) + .map_err(|err| anyhow::anyhow!(err.to_string()))?; + + // With analytics unset in the config and the default flag is false, metrics are disabled. + // No provider is built. + assert_eq!(provider.is_none(), true); + Ok(()) +} + +#[tokio::test] +async fn app_server_default_analytics_enabled_with_flag() -> Result<()> { + let codex_home = TempDir::new()?; + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await?; + set_metrics_exporter(&mut config); + config.analytics_enabled = None; + + let provider = codex_core::otel_init::build_provider( + &config, + SERVICE_VERSION, + Some("codex_app_server"), + true, + ) + .map_err(|err| anyhow::anyhow!(err.to_string()))?; + + // With analytics unset in the config and the default flag is true, metrics are enabled. + let has_metrics = provider.as_ref().and_then(|otel| otel.metrics()).is_some(); + assert_eq!(has_metrics, true); + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 5c40c5fc1..b2159ab9c 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -1,4 +1,5 @@ mod account; +mod analytics; mod config_rpc; mod initialize; mod model_list; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 918626c71..c29da7271 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -268,6 +268,24 @@ struct AppServerCommand { /// Omit to run the app server; specify a subcommand for tooling. #[command(subcommand)] subcommand: Option, + + /// Controls whether analytics are enabled by default. + /// + /// Analytics are disabled by default for app-server. Users have to explicitly opt in + /// via the `analytics` section in the config.toml file. + /// + /// However, for first-party use cases like the VSCode IDE extension, we default analytics + /// to be enabled by default by setting this flag. Users can still opt out by setting this + /// in their config.toml: + /// + /// ```toml + /// [analytics] + /// enabled = false + /// ``` + /// + /// See https://developers.openai.com/codex/config-advanced/#metrics for more details. + #[arg(long = "analytics-default-enabled")] + analytics_default_enabled: bool, } #[derive(Debug, clap::Subcommand)] @@ -500,6 +518,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() codex_linux_sandbox_exe, root_config_overrides, codex_core::config_loader::LoaderOverrides::default(), + app_server_cli.analytics_default_enabled, ) .await?; } @@ -910,6 +929,14 @@ mod tests { finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli) } + fn app_server_from_args(args: &[&str]) -> AppServerCommand { + let cli = MultitoolCli::try_parse_from(args).expect("parse"); + let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else { + unreachable!() + }; + app_server + } + fn sample_exit_info(conversation: Option<&str>) -> AppExitInfo { let token_usage = TokenUsage { output_tokens: 2, @@ -1108,6 +1135,19 @@ mod tests { assert!(interactive.fork_show_all); } + #[test] + fn app_server_analytics_default_disabled_without_flag() { + let app_server = app_server_from_args(["codex", "app-server"].as_ref()); + assert!(!app_server.analytics_default_enabled); + } + + #[test] + fn app_server_analytics_default_enabled_with_flag() { + let app_server = + app_server_from_args(["codex", "app-server", "--analytics-default-enabled"].as_ref()); + assert!(app_server.analytics_default_enabled); + } + #[test] fn feature_toggles_known_features_generate_overrides() { let toggles = FeatureToggles {