send command telemetry one word at a time instead of one letter at a
Some checks failed
Build and Deploy to K3s / deploy (push) Blocked by required conditions
Build and Deploy to K3s / test (push) Successful in 2m9s
Build and Deploy to K3s / lint (push) Successful in 1m34s
Build and Deploy to K3s / build-and-push (push) Has been cancelled

time. remove escape sequences.
This commit is contained in:
jasonwitty 2025-12-01 00:48:25 -08:00
parent 7bfbe0d86e
commit d7efa60cea
3 changed files with 50 additions and 9 deletions

1
Cargo.lock generated
View File

@ -2626,6 +2626,7 @@ dependencies = [
"log", "log",
"pop-telemetry", "pop-telemetry",
"portable-pty", "portable-pty",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",

View File

@ -31,6 +31,7 @@ env_logger = "0.11"
libc = "0.2" libc = "0.2"
pop-telemetry = { git = "https://github.com/jasonwitty/pop-cli", branch = "main" } pop-telemetry = { git = "https://github.com/jasonwitty/pop-cli", branch = "main" }
dirs = "5.0" dirs = "5.0"
regex = "1.10"
[lib] [lib]
name = "webterm" name = "webterm"

View File

@ -44,7 +44,6 @@ use serde_json::json;
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
const IDLE_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes
const IDLE_CHECK_INTERVAL: Duration = Duration::from_secs(30); // Check every 30 seconds const IDLE_CHECK_INTERVAL: Duration = Duration::from_secs(30); // Check every 30 seconds
pub mod analytics; pub mod analytics;
@ -229,6 +228,7 @@ pub struct Terminal {
last_activity: Instant, last_activity: Instant,
idle_timeout: Duration, idle_timeout: Duration,
analytics: Option<Analytics>, analytics: Option<Analytics>,
command_buffer: String,
} }
impl Terminal { impl Terminal {
@ -240,8 +240,9 @@ impl Terminal {
ws, ws,
command, command,
last_activity: Instant::now(), last_activity: Instant::now(),
idle_timeout: IDLE_TIMEOUT, idle_timeout: Duration::from_secs(300),
analytics: None, analytics: None,
command_buffer: String::new(),
} }
} }
@ -253,8 +254,9 @@ impl Terminal {
ws, ws,
command, command,
last_activity: Instant::now(), last_activity: Instant::now(),
idle_timeout: IDLE_TIMEOUT, idle_timeout: Duration::from_secs(300),
analytics: Some(analytics), analytics: Some(analytics),
command_buffer: String::new(),
} }
} }
} }
@ -417,15 +419,52 @@ impl Handler<TerminadoMessage> for Terminal {
// Reset idle timer on user input // Reset idle timer on user input
self.last_activity = Instant::now(); self.last_activity = Instant::now();
// Track command in analytics // Buffer input and track command only when Enter is pressed
if let Some(analytics) = &self.analytics { if let Some(analytics) = &self.analytics {
let command = String::from_utf8_lossy(&io.0).to_string(); let input = String::from_utf8_lossy(&io.0).to_string();
// Check if input contains newline (Enter key)
if input.contains('\n') || input.contains('\r') {
// Strip ANSI escape sequences and control codes
// Pattern matches: ESC[ followed by any characters until a letter
let mut cleaned_command = self.command_buffer.clone();
// Remove ANSI escape sequences like [<35;2;1M
// This regex pattern matches ESC [ followed by any non-letter chars and ending with a letter
let escape_pattern = regex::Regex::new(r"\x1b\[[^\x1b]*?[a-zA-Z]").unwrap();
cleaned_command =
escape_pattern.replace_all(&cleaned_command, "").to_string();
// Remove CSI sequences without ESC prefix like [<35;2;1M
let csi_pattern = regex::Regex::new(r"\[<[0-9;]+[a-zA-Z]").unwrap();
cleaned_command = csi_pattern.replace_all(&cleaned_command, "").to_string();
// Remove any remaining control characters
cleaned_command = cleaned_command
.chars()
.filter(|c| !c.is_control() || c.is_ascii_whitespace())
.collect();
let command = cleaned_command.trim().to_string();
// Track if command has actual content (alphanumeric chars)
if !command.is_empty() && command.chars().any(|c| c.is_ascii_alphanumeric())
{
log::info!("Tracking command: '{}'", command);
let analytics_clone = analytics.clone(); let analytics_clone = analytics.clone();
actix::spawn(async move { actix::spawn(async move {
let _ = analytics_clone.track_command(&command, None).await; let _ = analytics_clone.track_command(&command, None).await;
}); });
} }
// Clear buffer for next command
self.command_buffer.clear();
} else {
// Accumulate input in buffer
self.command_buffer.push_str(&input);
}
}
let writer = match &mut self.pty_writer { let writer = match &mut self.pty_writer {
Some(w) => w, Some(w) => w,
None => { None => {