From a8b0cf9cde113cb7f0cefb6258bb9aba8462183c Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Thu, 1 Oct 2026 11:38:10 -0700 Subject: [PATCH 1/3] feat: record orx up launch mode and browser-open failures (v0.2.15) Co-Authored-By: Claude --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/browser.rs | 38 ++++++++++++++----- src/commands/up.rs | 4 +- src/commands/up_remote.rs | 2 +- src/main.rs | 4 ++ src/telemetry.rs | 80 +++++++++++++++++++++++++++++++++++++-- 7 files changed, 115 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74f65956..00dc6cd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2189,7 +2189,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openresearch-cli" -version = "0.2.14" +version = "0.2.15" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 94f6ae5b..e7be58a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openresearch-cli" -version = "0.2.14" +version = "0.2.15" edition = "2021" description = "OpenResearch CLI (orx) — Rust port" repository = "https://github.com/alphaXiv/OpenResearch" diff --git a/src/browser.rs b/src/browser.rs index 840c5b5b..e9f6404d 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -1,23 +1,43 @@ //! Cross-platform "open URL in browser". -use std::process::{Command, Stdio}; +use std::process::{Child, Command, Stdio}; /// Opens `url` in the user's default browser. Best-effort and non-fatal: errors /// (e.g. no browser, headless) are swallowed, since the caller is expected to /// have already printed the URL for manual opening. The child is detached so the /// CLI does not block on it. pub fn open_browser(url: &str) { - for mut cmd in openers(url) { - if cmd - .stdin(Stdio::null()) + launch(url); +} + +/// [`open_browser`] for `orx up`, reporting an opener that fails to spawn or +/// exits non-zero (e.g. `xdg-open` without a display) to telemetry. +pub fn open_dashboard(url: &str, mode: crate::telemetry::UpLaunchMode) { + let Some(mut child) = launch(url) else { + return crate::telemetry::capture_browser_open_failed(mode); + }; + // Openers that run the browser in the foreground never exit; stop watching then. + tokio::spawn(async move { + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) if !status.success() => { + return crate::telemetry::capture_browser_open_failed(mode) + } + Ok(None) => tokio::time::sleep(std::time::Duration::from_millis(100)).await, + _ => return, + } + } + }); +} + +fn launch(url: &str) -> Option { + openers(url).into_iter().find_map(|mut cmd| { + cmd.stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() - .is_ok() - { - return; - } - } + .ok() + }) } /// The open commands to try, in order. On WSL the URL goes to the Windows diff --git a/src/commands/up.rs b/src/commands/up.rs index da976ac5..d99bcb37 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -82,7 +82,7 @@ pub async fn run(args: UpArgs) -> Result<()> { let url = format!("http://127.0.0.1:{port}"); eprintln!("orx up: already running — opening {url}"); if !args.no_browser { - browser::open_browser(&url); + browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args)); } return Ok(()); } @@ -220,7 +220,7 @@ pub async fn run(args: UpArgs) -> Result<()> { eprintln!("orx up: warning: {warning}"); } if !args.no_browser { - browser::open_browser(&url); + browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args)); } } diff --git a/src/commands/up_remote.rs b/src/commands/up_remote.rs index 01bf3490..eb89f82d 100644 --- a/src/commands/up_remote.rs +++ b/src/commands/up_remote.rs @@ -1673,7 +1673,7 @@ pub async fn run(host: &str, args: UpArgs) -> Result<()> { }; eprintln!("orx up --remote: dashboard on {}", session.gateway_url); if !args.no_browser { - browser::open_browser(&session.gateway_url); + browser::open_dashboard(&session.gateway_url, crate::telemetry::UpLaunchMode::Remote); } eprintln!("orx up --remote: press Ctrl-C to stop."); let _ = tokio::signal::ctrl_c().await; diff --git a/src/main.rs b/src/main.rs index 20cb896b..ec6d6688 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1020,6 +1020,10 @@ async fn main() { telemetry::set_flag(cli.no_telemetry); let session = telemetry::TelemetrySession::start( should_capture_command(&command).then(|| command_name(&command)), + match &command { + Command::Up(args) => Some(telemetry::UpLaunchMode::of(args)), + _ => None, + }, ); let result = dispatch(command).await; diff --git a/src/telemetry.rs b/src/telemetry.rs index 096a8853..2fa117a0 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1024,6 +1024,38 @@ pub(crate) enum ProjectCreationMode { Paper, } +/// How `orx up` presents the dashboard. The desktop app reports `app_started` instead. +#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum UpLaunchMode { + Browser, + NoBrowser, + /// Plain `orx up` inside an SSH session, which prints forwarding steps instead. + Ssh, + Remote, + RemoteHost, +} + +impl UpLaunchMode { + pub(crate) fn of(args: &crate::UpArgs) -> Self { + if args.remote_host { + Self::RemoteHost + } else if args.remote.is_some() { + Self::Remote + } else if args.no_browser { + Self::NoBrowser + } else if crate::remote::detect_ssh_session().is_some() { + Self::Ssh + } else { + Self::Browser + } + } +} + +pub(crate) fn capture_browser_open_failed(mode: UpLaunchMode) { + capture("browser_open_failed", json!({ "mode": mode })); +} + pub(crate) fn capture_project_created(local: bool, mode: Option) { let mut properties = json!({ "local": local }); if let Some(mode) = mode { @@ -1088,11 +1120,15 @@ impl TelemetrySession { /// The `--no-telemetry` flag is read from the process-global (set in `main` /// before this is called), matching every other event path. The handle is /// registered in the pending set and flushed by `finish`. - pub(crate) fn start(command: Option<&str>) -> TelemetrySession { + pub(crate) fn start(command: Option<&str>, mode: Option) -> TelemetrySession { retry_outbox(); if let Some(command) = command { // Bare base name; `build_payload` prefixes it → wire event `cli_command`. - capture("command", json!({ "command": command })); + let mut properties = json!({ "command": command }); + if let Some(mode) = mode { + properties["mode"] = json!(mode); + } + capture("command", properties); } TelemetrySession } @@ -2033,6 +2069,16 @@ mod tests { "cli-release-contract-test", json!({ "command": "up" }), ), + build_payload( + "command", + "cli-release-contract-test", + json!({ "command": "up", "mode": UpLaunchMode::Remote }), + ), + build_payload( + "browser_open_failed", + "cli-release-contract-test", + json!({ "mode": UpLaunchMode::Browser }), + ), build_payload("app_started", "cli-release-contract-test", json!({})), build_payload( "telemetry_consent", @@ -2241,6 +2287,33 @@ mod tests { assert!(serde_json::from_value::(json!("/private/path")).is_err()); } + #[test] + fn up_launch_modes_follow_the_dashboard_code_paths() { + let _g = EnvGuard::new(&["SSH_CONNECTION", "SSH_TTY", "SSH_CLIENT"]); + let args = |argv: &[&str]| { + use clap::Parser; + match crate::Cli::parse_from(argv).command { + Some(crate::Command::Up(args)) => UpLaunchMode::of(&args), + _ => unreachable!(), + } + }; + let cases = [ + (&["orx", "up"][..], "browser"), + (&["orx", "up", "--no-browser"], "no_browser"), + (&["orx", "up", "--remote", "box"], "remote"), + ( + &["orx", "up", "--no-browser", "--remote-host"], + "remote_host", + ), + ]; + for (argv, mode) in cases { + assert_eq!(json!(args(argv)), json!(mode), "{argv:?}"); + } + std::env::set_var("SSH_CONNECTION", "1.2.3.4 5 6.7.8.9 22"); + assert_eq!(args(&["orx", "up"]), UpLaunchMode::Ssh); + assert_eq!(json!(UpLaunchMode::Ssh), json!("ssh")); + } + #[tokio::test] async fn environment_disabled_consent_never_creates_an_install_id() { let _g = EnvGuard::new(OPT_VARS); @@ -2271,12 +2344,13 @@ mod tests { } assert!(environment_disabled_reason().is_some()); - let session = TelemetrySession::start(Some("up")); + let session = TelemetrySession::start(Some("up"), Some(UpLaunchMode::Browser)); harness::capture_initial(&json!({"harnesses":[]})); harness::SetupAttempt::new("opencode", "install", "automatic"); capture_onboarding_completed(); capture_onboarding_research_profile(&ResearchProfile::default()); capture_project_created(true, Some(ProjectCreationMode::Blank)); + capture_browser_open_failed(UpLaunchMode::Browser); capture_demo_welcome_choice("explore_demo"); capture_chat_session_started("codex"); capture_chat_message_sent("codex"); From 87935f72ec055427ba782d960a05e0108961116c Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Thu, 1 Oct 2026 11:45:53 -0700 Subject: [PATCH 2/3] Await the browser watcher on the already-running path; gate every launch mode Co-Authored-By: Claude --- src/browser.rs | 13 +++++++++---- src/commands/up.rs | 6 +++++- src/telemetry.rs | 37 ++++++++++++++++++++++++++----------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/browser.rs b/src/browser.rs index e9f6404d..5b89c21a 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -12,12 +12,17 @@ pub fn open_browser(url: &str) { /// [`open_browser`] for `orx up`, reporting an opener that fails to spawn or /// exits non-zero (e.g. `xdg-open` without a display) to telemetry. -pub fn open_dashboard(url: &str, mode: crate::telemetry::UpLaunchMode) { +/// Returns the watcher so a command about to exit can await it. +pub fn open_dashboard( + url: &str, + mode: crate::telemetry::UpLaunchMode, +) -> Option> { let Some(mut child) = launch(url) else { - return crate::telemetry::capture_browser_open_failed(mode); + crate::telemetry::capture_browser_open_failed(mode); + return None; }; // Openers that run the browser in the foreground never exit; stop watching then. - tokio::spawn(async move { + Some(tokio::spawn(async move { for _ in 0..50 { match child.try_wait() { Ok(Some(status)) if !status.success() => { @@ -27,7 +32,7 @@ pub fn open_dashboard(url: &str, mode: crate::telemetry::UpLaunchMode) { _ => return, } } - }); + })) } fn launch(url: &str) -> Option { diff --git a/src/commands/up.rs b/src/commands/up.rs index d99bcb37..0e0449ea 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -82,7 +82,11 @@ pub async fn run(args: UpArgs) -> Result<()> { let url = format!("http://127.0.0.1:{port}"); eprintln!("orx up: already running — opening {url}"); if !args.no_browser { - browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args)); + if let Some(watch) = + browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args)) + { + let _ = watch.await; + } } return Ok(()); } diff --git a/src/telemetry.rs b/src/telemetry.rs index 2fa117a0..2649b13d 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -2069,16 +2069,6 @@ mod tests { "cli-release-contract-test", json!({ "command": "up" }), ), - build_payload( - "command", - "cli-release-contract-test", - json!({ "command": "up", "mode": UpLaunchMode::Remote }), - ), - build_payload( - "browser_open_failed", - "cli-release-contract-test", - json!({ "mode": UpLaunchMode::Browser }), - ), build_payload("app_started", "cli-release-contract-test", json!({})), build_payload( "telemetry_consent", @@ -2146,9 +2136,34 @@ mod tests { json!({ "kind": "run", "local": true, "computeTarget": "local" }), ), ]; + let launch_modes = [ + UpLaunchMode::Browser, + UpLaunchMode::NoBrowser, + UpLaunchMode::Ssh, + UpLaunchMode::Remote, + UpLaunchMode::RemoteHost, + ]; + let launch_payloads = launch_modes.into_iter().flat_map(|mode| { + [ + build_payload( + "command", + "cli-release-contract-test", + json!({ "command": "up", "mode": mode }), + ), + build_payload( + "browser_open_failed", + "cli-release-contract-test", + json!({ "mode": mode }), + ), + ] + }); let mut localized = build_payload("app_started", "cli-release-contract-test", json!({})); localized["context"]["locale"] = json!("zh-CN"); - for payload in payloads.into_iter().chain([localized]) { + for payload in payloads + .into_iter() + .chain(launch_payloads) + .chain([localized]) + { assert_eq!(post_payload(&payload).await, DeliveryOutcome::Acknowledged); } } From 0875c2d0008f73a52163ed30559dc857b350450b Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Thu, 1 Oct 2026 16:37:45 -0700 Subject: [PATCH 3/3] Drop version bump from this PR --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2a88e93..6edd0588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2189,7 +2189,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openresearch-cli" -version = "0.2.15" +version = "0.2.14" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 2c2e612a..7809d81c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openresearch-cli" -version = "0.2.15" +version = "0.2.14" edition = "2021" description = "OpenResearch CLI (orx) — Rust port" repository = "https://github.com/alphaXiv/OpenResearch"