mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-12 23:02:41 +00:00
Send telemetry via detached subprocess to survive CLI exit
Replace tokio::spawn fire-and-forget with a detached subprocess pattern: send() serializes Track to ~/.arc/tmp/, spawns `arc __send_analytics <path>` with ARC_TELEMETRY=off, and the child sends to Segment then cleans up the file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8909eafbf3
commit
992a920f72
2 changed files with 63 additions and 21 deletions
|
|
@ -4,6 +4,8 @@ mod init;
|
|||
mod install;
|
||||
mod logging;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing::debug;
|
||||
|
|
@ -102,6 +104,12 @@ enum Command {
|
|||
#[command(subcommand)]
|
||||
command: SystemCommand,
|
||||
},
|
||||
/// Send a queued analytics event (internal)
|
||||
#[command(name = "__send_analytics", hide = true)]
|
||||
SendAnalytics {
|
||||
/// Path to the JSON event file
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -206,6 +214,7 @@ async fn main_inner() -> Result<()> {
|
|||
Command::Ps(_) => "ps",
|
||||
Command::Pr { .. } => "pr",
|
||||
Command::System { .. } => "system",
|
||||
Command::SendAnalytics { .. } => "__send_analytics",
|
||||
};
|
||||
|
||||
let config_log_level = {
|
||||
|
|
@ -475,6 +484,13 @@ async fn main_inner() -> Result<()> {
|
|||
arc_workflows::cli::runs::df_command(&args)?;
|
||||
}
|
||||
},
|
||||
Command::SendAnalytics { path } => {
|
||||
let json = std::fs::read(&path)?;
|
||||
let track: arc_util::telemetry::event::Track = serde_json::from_slice(&json)?;
|
||||
let result = arc_util::telemetry::sender::send_to_segment(&track).await;
|
||||
let _ = std::fs::remove_file(&path);
|
||||
result?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -6,41 +6,67 @@ use super::event::Track;
|
|||
const SEGMENT_API_URL: &str = "https://api.segment.io/v1/track";
|
||||
const SEGMENT_WRITE_KEY: Option<&str> = option_env!("SEGMENT_WRITE_KEY");
|
||||
|
||||
/// Spawns a fire-and-forget tokio task to send a track event to Segment.
|
||||
/// Serializes the track event to a temp file and spawns a detached subprocess
|
||||
/// (`arc __send_analytics <path>`) to deliver it. This ensures the event is
|
||||
/// sent even if the parent CLI process exits immediately.
|
||||
///
|
||||
/// No-ops if the SEGMENT_WRITE_KEY was not set at compile time.
|
||||
pub fn send(track: Track) {
|
||||
let Some(write_key) = SEGMENT_WRITE_KEY else {
|
||||
if SEGMENT_WRITE_KEY.is_none() {
|
||||
tracing::debug!("telemetry: no SEGMENT_WRITE_KEY, skipping send");
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
tokio::spawn(send_track(track, write_key));
|
||||
if let Err(err) = spawn_sender(track) {
|
||||
tracing::debug!(%err, "telemetry: failed to spawn analytics sender");
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_track(track: Track, write_key: &str) {
|
||||
fn spawn_sender(track: Track) -> std::io::Result<()> {
|
||||
let tmp_dir = dirs::home_dir()
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no home directory"))?
|
||||
.join(".arc")
|
||||
.join("tmp");
|
||||
|
||||
std::fs::create_dir_all(&tmp_dir)?;
|
||||
|
||||
let path = tmp_dir.join(format!("arc-event-{}.json", track.message_id));
|
||||
let json = serde_json::to_vec(&track)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
std::fs::write(&path, json)?;
|
||||
|
||||
let exe = std::env::current_exe()?;
|
||||
std::process::Command::new(exe)
|
||||
.arg("__send_analytics")
|
||||
.arg(&path)
|
||||
.env("ARC_TELEMETRY", "off")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a track event to Segment. Called by the `__send_analytics` subcommand.
|
||||
pub async fn send_to_segment(track: &Track) -> anyhow::Result<()> {
|
||||
let write_key = SEGMENT_WRITE_KEY
|
||||
.ok_or_else(|| anyhow::anyhow!("SEGMENT_WRITE_KEY not set at compile time"))?;
|
||||
|
||||
let auth = STANDARD.encode(format!("{write_key}:"));
|
||||
|
||||
let result = reqwest::Client::new()
|
||||
let resp = reqwest::Client::new()
|
||||
.post(SEGMENT_API_URL)
|
||||
.header("Authorization", format!("Basic {auth}"))
|
||||
.json(&track)
|
||||
.json(track)
|
||||
.send()
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
Ok(resp) if !resp.status().is_success() => {
|
||||
tracing::warn!(
|
||||
status = %resp.status(),
|
||||
"telemetry: segment API returned non-success status"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(%err, "telemetry: failed to send event to segment");
|
||||
}
|
||||
Ok(_) => {
|
||||
tracing::debug!("telemetry: event sent successfully");
|
||||
}
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("segment API returned status {}", resp.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue