mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Record daemon startup and update telemetry with consent handling (#46126)
## Why TUI startup metrics report the selected app-server mode before a connection succeeds, which can misrepresent embedded fallback or failed startup. Daemon startup and update actions also need distinct outcome observations. ## What changed - Record `codex.tui.start` once after the first connection attempt, using the actual mode or `unconfirmed`, and include daemon selection and auto-start tags. - Add `codex.daemon.start` for TUI auto-start and `codex.daemon.update` for foreground CLI updates and TUI update handoffs, using existing analytics consent and identity handling. - Report TUI handoffs as `handoff_requested` and suppress duplicate child reporting. Remove the suppression flag when spawning long-lived daemon processes. - Tag daemon settings by enabled state and explicit presence, without exporting setting values or contents; report invalid or unreadable settings as `unknown`. ## Testing Add coverage for analytics defaults and explicit consent overrides, unconfirmed CLI updates, settings presence, and exactly-once launch observations. Extend the worktree TUI test to check startup tags and a single update handoff observation. GitOrigin-RevId: 176d2ee594f930033fa1ece55d4c1c9603dbbc16
This commit is contained in:
@@ -89,6 +89,8 @@ impl PidBackend {
|
||||
}
|
||||
};
|
||||
command
|
||||
// Handoff suppression belongs to the foreground CLI, not its long-lived children.
|
||||
.env_remove(crate::telemetry::HANDOFF_ENV)
|
||||
.args(self.command_args())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use prepare_install::InstallRequest;
|
||||
pub use prepare_install::update_from_cli;
|
||||
mod remote_control_client;
|
||||
mod settings;
|
||||
pub mod telemetry;
|
||||
mod thread_recovery;
|
||||
mod update_loop;
|
||||
|
||||
|
||||
@@ -178,6 +178,38 @@ impl DaemonSettings {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn telemetry_tags(path: &Path) -> Result<[(&'static str, &'static str); 4]> {
|
||||
let raw: Map<String, Value> = read_settings(path).await?;
|
||||
// Validate the same snapshot used for presence checks, using the existing settings parser.
|
||||
let settings: StoredSettings = serde_json::from_value(Value::Object(raw.clone()))?;
|
||||
settings.updater.validate()?;
|
||||
validate_shutdown_grace(settings.shutdown_grace_seconds)?;
|
||||
let updater = raw.get("updater");
|
||||
let presence = |configured| if configured { "configured" } else { "default" };
|
||||
Ok([
|
||||
(
|
||||
"auto_update",
|
||||
if settings.updater.auto_update_enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
},
|
||||
),
|
||||
(
|
||||
"auto_update_setting",
|
||||
presence(updater.is_some_and(|value| value.get("autoUpdateEnabled").is_some())),
|
||||
),
|
||||
(
|
||||
"update_interval_setting",
|
||||
presence(updater.is_some_and(|value| value.get("updateIntervalMinutes").is_some())),
|
||||
),
|
||||
(
|
||||
"shutdown_grace_setting",
|
||||
presence(raw.contains_key("shutdownGraceSeconds")),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
async fn read_settings<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
|
||||
let contents = match fs::read_to_string(path).await {
|
||||
Ok(contents) => contents,
|
||||
|
||||
@@ -128,3 +128,27 @@ async fn update_interval_accepts_long_values_and_rejects_zero() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn telemetry_distinguishes_presence_from_default_values() -> anyhow::Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
let dir = home.path().join("app-server-daemon");
|
||||
tokio::fs::create_dir(&dir).await?;
|
||||
let path = dir.join("settings.json");
|
||||
for (contents, presence) in [
|
||||
("{}", "default"),
|
||||
(
|
||||
r#"{"updater":{"autoUpdateEnabled":true,"updateIntervalMinutes":60},"shutdownGraceSeconds":60}"#,
|
||||
"configured",
|
||||
),
|
||||
] {
|
||||
tokio::fs::write(&path, contents).await?;
|
||||
assert_eq!(
|
||||
crate::telemetry::settings_tags(home.path())
|
||||
.await
|
||||
.map(|(_, value)| value),
|
||||
["enabled", presence, presence, presence]
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Bounded observations for foreground callers. This module never exports telemetry;
|
||||
//! in particular, the detached updater must not create a telemetry provider.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Suppress child reporting when the foreground TUI handoff owns the observation.
|
||||
pub const HANDOFF_ENV: &str = "CODEX_DAEMON_TELEMETRY_HANDOFF";
|
||||
|
||||
/// Only presence and enabled state leave the settings reader, never values or contents.
|
||||
/// Missing files mean defaults; unreadable or invalid settings remain unknown.
|
||||
pub async fn settings_tags(codex_home: &Path) -> [(&'static str, &'static str); 4] {
|
||||
let path = codex_home
|
||||
.join(crate::STATE_DIR_NAME)
|
||||
.join(crate::SETTINGS_FILE_NAME);
|
||||
crate::settings::telemetry_tags(&path).await.unwrap_or([
|
||||
("auto_update", "unknown"),
|
||||
("auto_update_setting", "unknown"),
|
||||
("update_interval_setting", "unknown"),
|
||||
("shutdown_grace_setting", "unknown"),
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Foreground daemon observations use the CLI's existing consent and identity handling.
|
||||
//! Detached updater entry points never call this module.
|
||||
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
pub(crate) async fn record_command(
|
||||
overrides: &CliConfigOverrides,
|
||||
analytics_default_enabled: bool,
|
||||
target: &'static str,
|
||||
result: &anyhow::Result<Option<codex_app_server_daemon::UpdateOutput>>,
|
||||
) {
|
||||
if std::env::var_os(codex_app_server_daemon::telemetry::HANDOFF_ENV).is_some() {
|
||||
return;
|
||||
}
|
||||
// Skip telemetry if consent-aware configuration cannot be loaded promptly.
|
||||
let Ok(Ok(config)) = timeout(
|
||||
Duration::from_secs(/*secs*/ 2),
|
||||
crate::cloud_config::load_config(overrides, codex_config::LoaderOverrides::default()),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
use codex_app_server_daemon::UpdateStatus;
|
||||
let outcome = match result {
|
||||
Ok(Some(output)) => match output.status {
|
||||
UpdateStatus::Updated => "updated",
|
||||
UpdateStatus::NoUpdate => "no_update",
|
||||
UpdateStatus::Unsupported => "unsupported",
|
||||
},
|
||||
Ok(None) => "cancelled",
|
||||
Err(_) => "unconfirmed",
|
||||
};
|
||||
let Ok(Ok(Some(otel))) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
/*service_name_override*/ None,
|
||||
analytics_default_enabled,
|
||||
)
|
||||
})) else {
|
||||
return;
|
||||
};
|
||||
if let Some(metrics) = otel.metrics() {
|
||||
let mut tags = codex_app_server_daemon::telemetry::settings_tags(&config.codex_home)
|
||||
.await
|
||||
.to_vec();
|
||||
tags.extend([
|
||||
("initiation_source", "cli"),
|
||||
("update_target", target),
|
||||
("outcome", outcome),
|
||||
]);
|
||||
let _ = metrics.counter("codex.daemon.update", /*inc*/ 1, &tags);
|
||||
}
|
||||
let _ = otel
|
||||
.shutdown_with_timeout(std::time::Duration::from_secs(/*secs*/ 2))
|
||||
.await;
|
||||
}
|
||||
@@ -58,6 +58,7 @@ static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
mod app_cmd;
|
||||
mod cloud_config;
|
||||
mod daemon_install;
|
||||
mod daemon_telemetry;
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
mod desktop_app;
|
||||
mod doctor;
|
||||
@@ -936,6 +937,7 @@ fn run_update_action(
|
||||
println!("Updating the local background server...");
|
||||
let status = std::process::Command::new(executable)
|
||||
.args(source.command_args())
|
||||
.env(codex_app_server_daemon::telemetry::HANDOFF_ENV, "1")
|
||||
.status()?;
|
||||
anyhow::ensure!(
|
||||
status.success(),
|
||||
@@ -1451,11 +1453,18 @@ async fn cli_main(
|
||||
from_cli: true,
|
||||
yes,
|
||||
} => {
|
||||
if let Some(output) = codex_app_server_daemon::update_from_cli(|request| {
|
||||
let result = codex_app_server_daemon::update_from_cli(|request| {
|
||||
daemon_install::confirm_install(request, yes)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
.await;
|
||||
daemon_telemetry::record_command(
|
||||
&root_config_overrides,
|
||||
analytics_default_enabled,
|
||||
"this_cli",
|
||||
&result,
|
||||
)
|
||||
.await;
|
||||
if let Some(output) = result? {
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
}
|
||||
}
|
||||
@@ -1499,9 +1508,19 @@ async fn cli_main(
|
||||
daemon_cli.subcommand,
|
||||
AppServerDaemonSubcommand::Update { .. }
|
||||
) {
|
||||
let output =
|
||||
codex_app_server_daemon::update(http_client_factory).await?;
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
let result = codex_app_server_daemon::update(http_client_factory)
|
||||
.await
|
||||
.map(Some);
|
||||
daemon_telemetry::record_command(
|
||||
&root_config_overrides,
|
||||
analytics_default_enabled,
|
||||
"public_stable",
|
||||
&result,
|
||||
)
|
||||
.await;
|
||||
if let Some(output) = result? {
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
}
|
||||
} else {
|
||||
let AppServerDaemonSubcommand::PidUpdateLoop {
|
||||
restore_release, ..
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Foreground updates honor app-server consent and never claim unconfirmed installation.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreground_update_respects_consent_and_reports_unconfirmed() -> anyhow::Result<()> {
|
||||
for (analytics_config, analytics_default_enabled, expected_requests) in [
|
||||
("", false, 0),
|
||||
("", true, 1),
|
||||
("analytics.enabled = true\n", false, 1),
|
||||
("analytics.enabled = false\n", true, 0),
|
||||
] {
|
||||
let codex = codex_utils_cargo_bin::cargo_bin("codex")?;
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(wiremock::matchers::path("/metrics"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let home = tempfile::tempdir()?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = \"file\"\n{analytics_config}[otel]\nmetrics_exporter = {{ otlp-http = {{ endpoint = \"{}/metrics\", protocol = \"json\" }} }}\n",
|
||||
server.uri(),
|
||||
),
|
||||
)?;
|
||||
// No managed installation exists, so the command cannot confirm an applied update.
|
||||
let mut command = tokio::process::Command::new(&codex);
|
||||
command
|
||||
.current_dir(home.path())
|
||||
.env("CODEX_HOME", home.path())
|
||||
.env_remove(codex_app_server_daemon::telemetry::HANDOFF_ENV)
|
||||
.arg("app-server");
|
||||
if analytics_default_enabled {
|
||||
command.arg("--analytics-default-enabled");
|
||||
}
|
||||
let output = command
|
||||
.args(["daemon", "update", "--from-cli", "--yes"])
|
||||
.output()
|
||||
.await?;
|
||||
assert!(!output.status.success());
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("metric requests")?;
|
||||
assert_eq!(requests.len(), expected_requests);
|
||||
if expected_requests == 0 {
|
||||
continue;
|
||||
}
|
||||
let body: Value = serde_json::from_slice(&requests[0].body)?;
|
||||
let metric = &body["resourceMetrics"][0]["scopeMetrics"][0]["metrics"][0];
|
||||
assert_eq!(metric["name"], "codex.daemon.update");
|
||||
assert!(
|
||||
metric["sum"]["dataPoints"][0]["attributes"]
|
||||
.as_array()
|
||||
.context("metric attributes")?
|
||||
.iter()
|
||||
.any(|tag| tag["key"] == "outcome" && tag["value"]["stringValue"] == "unconfirmed")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -446,7 +446,29 @@ trust_level = "trusted"
|
||||
);
|
||||
}
|
||||
if matches!(startup_result, Ok(Ok(()))) && renamed.is_ok() {
|
||||
session.writer_sender().send(b"/quit\r".to_vec()).await?;
|
||||
if !fork {
|
||||
for (input, expected) in [
|
||||
("/daemon\r", "Install latest public stable"),
|
||||
("\r", "Update and exit"),
|
||||
("\x1b[B\r", "Updating the local background server..."),
|
||||
] {
|
||||
session
|
||||
.writer_sender()
|
||||
.send(input.as_bytes().to_vec())
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(/*secs*/ 10), async {
|
||||
while !output.contains(expected) {
|
||||
let bytes = stdout.recv().await.context("TUI exited before handoff")?;
|
||||
output.push_str(&String::from_utf8_lossy(&bytes));
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("waiting for {expected}: {output}"))??;
|
||||
}
|
||||
} else {
|
||||
session.writer_sender().send(b"/quit\r".to_vec()).await?;
|
||||
}
|
||||
} else {
|
||||
session.terminate();
|
||||
}
|
||||
@@ -459,7 +481,9 @@ trust_level = "trusted"
|
||||
}
|
||||
})
|
||||
.await;
|
||||
assert_eq!(exit, 0, "{output}");
|
||||
let elevated_handoff = cfg!(windows)
|
||||
&& output.contains("start the Windows daemon from a non-elevated terminal");
|
||||
assert_eq!(exit, i32::from(elevated_handoff), "{output}");
|
||||
assert!(!output.contains("The checkout was kept"), "{output}");
|
||||
let metrics = server
|
||||
.received_requests()
|
||||
@@ -470,11 +494,20 @@ trust_level = "trusted"
|
||||
.map(|request| serde_json::from_slice::<Value>(&request.body))
|
||||
.collect::<serde_json::Result<Vec<_>>>()?;
|
||||
if analytics {
|
||||
let point = metrics
|
||||
let exported = metrics
|
||||
.iter()
|
||||
.flat_map(|payload| payload["resourceMetrics"].as_array().into_iter().flatten())
|
||||
.flat_map(|resource| resource["scopeMetrics"].as_array().into_iter().flatten())
|
||||
.flat_map(|scope| scope["metrics"].as_array().into_iter().flatten())
|
||||
.collect::<Vec<_>>();
|
||||
let update = exported
|
||||
.iter()
|
||||
.find(|metric| metric["name"] == "codex.daemon.update")
|
||||
.context("handoff metric")?;
|
||||
let update_point = &update["sum"]["dataPoints"][0];
|
||||
assert_eq!(update_point["asInt"], 1);
|
||||
let point = exported
|
||||
.iter()
|
||||
.filter(|metric| metric["name"] == "codex.tui.start")
|
||||
.flat_map(|metric| metric["sum"]["dataPoints"].as_array().into_iter().flatten())
|
||||
.next()
|
||||
@@ -498,6 +531,12 @@ trust_level = "trusted"
|
||||
("app_server_mode", "in_process"),
|
||||
("terminal_name", "unknown"),
|
||||
("multiplexer", "none"),
|
||||
("daemon_selection_reason", "incompatible_option"),
|
||||
("daemon_auto_start", "disabled"),
|
||||
("auto_update", "enabled"),
|
||||
("auto_update_setting", "default"),
|
||||
("update_interval_setting", "default"),
|
||||
("shutdown_grace_setting", "default"),
|
||||
])
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,25 @@ use crate::legacy_core::config::ConfigBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn daemon_launch_telemetry_records_once_on_connection_or_early_return() {
|
||||
for connected in [false, true] {
|
||||
let observations = std::cell::RefCell::new(Vec::new());
|
||||
let launch = daemon_telemetry::Launch(Some(|target: &AppServerTarget, actual: bool| {
|
||||
observations.borrow_mut().push((target.clone(), actual));
|
||||
}));
|
||||
if connected {
|
||||
launch.record(&AppServerTarget::Embedded, connected);
|
||||
} else {
|
||||
drop(launch);
|
||||
}
|
||||
assert_eq!(
|
||||
observations.into_inner(),
|
||||
vec![(AppServerTarget::Embedded, connected)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn daemon_connection_rejects_unprotected_socket_before_handshake() -> color_eyre::Result<()> {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Records automatic daemon startup and ensures launch observations survive setup failures.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Emit once after connection, or as unconfirmed if startup returns early.
|
||||
pub(super) struct Launch<F: FnOnce(&AppServerTarget, bool)>(pub(super) Option<F>);
|
||||
|
||||
impl<F: FnOnce(&AppServerTarget, bool)> Launch<F> {
|
||||
pub(super) fn record(mut self, target: &AppServerTarget, connected: bool) {
|
||||
if let Some(record) = self.0.take() {
|
||||
record(target, connected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce(&AppServerTarget, bool)> Drop for Launch<F> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(record) = self.0.take() {
|
||||
record(&AppServerTarget::Embedded, /*connected*/ false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn record_start(
|
||||
config: &Config,
|
||||
result: &anyhow::Result<codex_app_server_daemon::LifecycleOutput>,
|
||||
) {
|
||||
use codex_app_server_daemon::LifecycleStatus;
|
||||
let outcome = match result {
|
||||
Ok(output) => match output.status {
|
||||
LifecycleStatus::Started => "started",
|
||||
LifecycleStatus::AlreadyRunning => "already_running",
|
||||
LifecycleStatus::Restarted => "restarted",
|
||||
LifecycleStatus::Stopped | LifecycleStatus::NotRunning | LifecycleStatus::Running => {
|
||||
"unconfirmed"
|
||||
}
|
||||
},
|
||||
Err(_) => "failed",
|
||||
};
|
||||
// Failure can happen before normal TUI telemetry is initialized. This short-lived
|
||||
// provider uses exactly the same consent and identity construction as TUI startup.
|
||||
let Ok(Ok(Some(otel))) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
codex_app_server_client::build_otel_provider(
|
||||
config,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
/*service_name_override*/ None,
|
||||
/*default_analytics_enabled*/ true,
|
||||
)
|
||||
})) else {
|
||||
return;
|
||||
};
|
||||
if let Some(metrics) = otel.metrics() {
|
||||
let mut tags = codex_app_server_daemon::telemetry::settings_tags(&config.codex_home)
|
||||
.await
|
||||
.to_vec();
|
||||
tags.extend([
|
||||
("initiation_source", "tui_auto_start"),
|
||||
("outcome", outcome),
|
||||
]);
|
||||
let _ = metrics.counter("codex.daemon.start", /*inc*/ 1, &tags);
|
||||
}
|
||||
let _ = otel
|
||||
.shutdown_with_timeout(INTERACTIVE_OTEL_SHUTDOWN_TIMEOUT)
|
||||
.await;
|
||||
}
|
||||
@@ -1061,6 +1061,7 @@ async fn run_ratatui_app(
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
managed_worktree: Option<ManagedTuiWorktree>,
|
||||
daemon_startup_warning: Option<String>,
|
||||
launch_telemetry: daemon_telemetry::Launch<impl FnOnce(&AppServerTarget, bool)>,
|
||||
startup_draft: startup_draft::StartupDraft,
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
let uses_remote_workspace = app_server_target.uses_remote_workspace();
|
||||
@@ -1126,6 +1127,7 @@ async fn run_ratatui_app(
|
||||
),
|
||||
)
|
||||
.await;
|
||||
launch_telemetry.record(&app_server_target, matches!(&startup_app_server, Ok(Ok(_))));
|
||||
let app_server_session = match startup_app_server {
|
||||
Ok(Ok(app_server)) => {
|
||||
AppServerSession::new(app_server, app_server_target.thread_params_mode())
|
||||
@@ -2185,6 +2187,7 @@ fn should_show_bedrock_setup_wizard(
|
||||
}
|
||||
|
||||
mod daemon_startup;
|
||||
mod daemon_telemetry;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "daemon_startup_tests.rs"]
|
||||
|
||||
@@ -418,11 +418,13 @@ pub(super) async fn run_main_inner(
|
||||
.with_restored(|| async {
|
||||
// Package installation may print progress; keep ordinary Ctrl+C handling.
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
codex_app_server_daemon::run(codex_app_server_daemon::LifecycleCommand::Start)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
std::io::Error::other(format!("{err:#}\n{}", daemon_startup::FAILURE_HINT))
|
||||
})
|
||||
let result =
|
||||
codex_app_server_daemon::run(codex_app_server_daemon::LifecycleCommand::Start)
|
||||
.await;
|
||||
daemon_telemetry::record_start(&config, &result).await;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!("{err:#}\n{}", daemon_startup::FAILURE_HINT))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
app_server_target = AppServerTarget::LocalDaemon {
|
||||
@@ -517,13 +519,52 @@ pub(super) async fn run_main_inner(
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(metrics) = otel.as_ref().and_then(codex_otel::OtelProvider::metrics) {
|
||||
let metrics = otel
|
||||
.as_ref()
|
||||
.and_then(codex_otel::OtelProvider::metrics)
|
||||
.cloned();
|
||||
if let Some(metrics) = &metrics {
|
||||
let _ = codex_otel::record_process_start_once(metrics, otel_originator.as_str());
|
||||
// Count the selected mode once per TUI launch, independently of reconnects.
|
||||
let app_server_mode = match &app_server_target {
|
||||
AppServerTarget::Embedded => "in_process",
|
||||
AppServerTarget::LocalDaemon { .. } => "local_daemon",
|
||||
AppServerTarget::Remote { .. } => "remote",
|
||||
let telemetry =
|
||||
codex_rollout::sqlite_telemetry_recorder(metrics.clone(), otel_originator.as_str());
|
||||
let _ = codex_state::install_process_db_telemetry(telemetry);
|
||||
}
|
||||
let selection_reason = match (&app_server_target, daemon_exclusion) {
|
||||
(AppServerTarget::Remote { .. }, _) => "explicit_remote",
|
||||
_ if cli.agents_overview => "agents",
|
||||
(_, Some("--no-daemon")) => "explicit_no_daemon",
|
||||
(_, Some(_)) => "incompatible_option",
|
||||
_ if auto_start_daemon => "auto_start",
|
||||
(AppServerTarget::LocalDaemon { .. }, _) => "existing_daemon",
|
||||
(AppServerTarget::Embedded, None) => "auto_start_disabled",
|
||||
};
|
||||
let daemon_settings = if metrics.is_some() {
|
||||
codex_app_server_daemon::telemetry::settings_tags(&config.codex_home)
|
||||
.await
|
||||
.to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut launch_tags = daemon_settings.to_vec();
|
||||
launch_tags.extend([
|
||||
("daemon_selection_reason", selection_reason),
|
||||
(
|
||||
"daemon_auto_start",
|
||||
if config.features.enabled(Feature::DaemonAutoStart) {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
},
|
||||
),
|
||||
]);
|
||||
// Record the first connection attempt's actual mode, including embedded fallback; never reconnects.
|
||||
let launch_telemetry = move |target: &AppServerTarget, connected: bool| {
|
||||
let Some(metrics) = metrics else { return };
|
||||
let app_server_mode = match (connected, target) {
|
||||
(false, _) => "unconfirmed",
|
||||
(true, AppServerTarget::Embedded) => "in_process",
|
||||
(true, AppServerTarget::LocalDaemon { .. }) => "local_daemon",
|
||||
(true, AppServerTarget::Remote { .. }) => "remote",
|
||||
};
|
||||
// Use a fixed category, not the versioned or user-provided terminal identifier.
|
||||
let terminal_info = codex_terminal_detection::terminal_info();
|
||||
@@ -548,19 +589,14 @@ pub(super) async fn run_main_inner(
|
||||
Some(Multiplexer::Zellij { .. }) => "zellij",
|
||||
None => "none",
|
||||
};
|
||||
let _ = metrics.counter(
|
||||
"codex.tui.start",
|
||||
/*inc*/ 1,
|
||||
&[
|
||||
("app_server_mode", app_server_mode),
|
||||
("terminal_name", terminal_name),
|
||||
("multiplexer", multiplexer),
|
||||
],
|
||||
);
|
||||
let telemetry =
|
||||
codex_rollout::sqlite_telemetry_recorder(metrics.clone(), otel_originator.as_str());
|
||||
let _ = codex_state::install_process_db_telemetry(telemetry);
|
||||
}
|
||||
launch_tags.extend([
|
||||
("app_server_mode", app_server_mode),
|
||||
("terminal_name", terminal_name),
|
||||
("multiplexer", multiplexer),
|
||||
]);
|
||||
let _ = metrics.counter("codex.tui.start", /*inc*/ 1, &launch_tags);
|
||||
};
|
||||
let launch_telemetry = daemon_telemetry::Launch(Some(launch_telemetry));
|
||||
let state_db = startup_draft
|
||||
.run_until(init_state_db_for_app_server_target(
|
||||
&config,
|
||||
@@ -592,6 +628,12 @@ pub(super) async fn run_main_inner(
|
||||
if let Some(worktree) = managed_worktree.as_ref() {
|
||||
worktree.report_startup_failure();
|
||||
}
|
||||
launch_telemetry.record(&app_server_target, /*connected*/ false);
|
||||
if let Some(otel) = otel {
|
||||
let _ = otel
|
||||
.shutdown_with_timeout(INTERACTIVE_OTEL_SHUTDOWN_TIMEOUT)
|
||||
.await;
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -607,6 +649,12 @@ pub(super) async fn run_main_inner(
|
||||
if let Some(worktree) = managed_worktree.as_ref() {
|
||||
worktree.report_startup_failure();
|
||||
}
|
||||
launch_telemetry.record(&app_server_target, /*connected*/ false);
|
||||
if let Some(otel) = otel {
|
||||
let _ = otel
|
||||
.shutdown_with_timeout(INTERACTIVE_OTEL_SHUTDOWN_TIMEOUT)
|
||||
.await;
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -715,6 +763,7 @@ pub(super) async fn run_main_inner(
|
||||
environment_manager,
|
||||
managed_worktree.clone(),
|
||||
daemon_startup_warning,
|
||||
launch_telemetry,
|
||||
startup_draft,
|
||||
)
|
||||
.await
|
||||
@@ -727,6 +776,26 @@ pub(super) async fn run_main_inner(
|
||||
worktree.report_startup_failure();
|
||||
}
|
||||
|
||||
// The TUI owns this request's consent. The child is silent; installation remains unconfirmed.
|
||||
if let Ok(exit) = &app_result
|
||||
&& let Some(UpdateAction::Daemon(source)) = exit.update_action
|
||||
&& let Some(metrics) = otel.as_ref().and_then(codex_otel::OtelProvider::metrics)
|
||||
{
|
||||
let mut tags = daemon_settings.to_vec();
|
||||
tags.extend([
|
||||
("initiation_source", "tui_handoff"),
|
||||
(
|
||||
"update_target",
|
||||
match source {
|
||||
DaemonUpdateSource::PublicStable => "public_stable",
|
||||
DaemonUpdateSource::ThisCli => "this_cli",
|
||||
},
|
||||
),
|
||||
("outcome", "handoff_requested"),
|
||||
]);
|
||||
let _ = metrics.counter("codex.daemon.update", /*inc*/ 1, &tags);
|
||||
}
|
||||
|
||||
if let Some(otel) = otel
|
||||
&& let Err(err) = otel
|
||||
.shutdown_with_timeout(INTERACTIVE_OTEL_SHUTDOWN_TIMEOUT)
|
||||
|
||||
Reference in New Issue
Block a user