hotfix for telemetry hotfix for profiles config
Build and Deploy to K3s / test (push) Successful in 2m6s
Build and Deploy to K3s / lint (push) Successful in 1m33s
Build and Deploy to K3s / build-and-push (push) Successful in 5m35s
Build and Deploy to K3s / deploy (push) Failing after 8s

This commit is contained in:
2025-11-30 03:34:08 -08:00
parent d3f95b8c52
commit e0535a033b
8 changed files with 378 additions and 228 deletions
+64 -97
View File
@@ -1,16 +1,17 @@
// Copyright (c) 2024 Jason Witty <jasonpwitty+socktop@proton.me>.
// All rights reserved.
//
// Umami analytics integration for tracking terminal events
// Umami analytics integration for tracking terminal events using pop-telemetry
use anyhow::Result;
use pop_telemetry::{record_cli_command, Telemetry};
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use umami_metrics::Umami;
/// Umami analytics tracker
pub struct Analytics {
client: Arc<Mutex<Option<Umami>>>,
telemetry: Arc<Mutex<Option<Telemetry>>>,
enabled: bool,
}
@@ -18,13 +19,12 @@ impl Analytics {
/// Create a new Analytics instance
///
/// # Arguments
/// * `website_id` - The Umami website ID
/// * `endpoint` - The Umami instance endpoint (e.g., "http://unami.wittyoneoff.com")
pub fn new(website_id: String, endpoint: String) -> Self {
let client = Umami::new(website_id, endpoint);
/// * `config_path` - Path to the telemetry config file (for opt-out checks)
pub fn new(config_path: PathBuf) -> Self {
let telemetry = Telemetry::new(&config_path);
Self {
client: Arc::new(Mutex::new(Some(client))),
telemetry: Arc::new(Mutex::new(Some(telemetry))),
enabled: true,
}
}
@@ -32,7 +32,7 @@ impl Analytics {
/// Create a disabled Analytics instance (no-op)
pub fn disabled() -> Self {
Self {
client: Arc::new(Mutex::new(None)),
telemetry: Arc::new(Mutex::new(None)),
enabled: false,
}
}
@@ -41,138 +41,103 @@ impl Analytics {
///
/// # Arguments
/// * `command` - The command that was typed (will be sanitized)
/// * `user_agent` - Optional user agent string
pub async fn track_command(&self, command: &str, user_agent: Option<String>) -> Result<()> {
/// * `_user_agent` - Optional user agent string (not used with pop-telemetry)
pub async fn track_command(&self, command: &str, _user_agent: Option<String>) {
if !self.enabled {
return Ok(());
return;
}
let client = self.client.lock().await;
let telemetry = self.telemetry.lock().await;
if let Some(umami) = client.as_ref() {
if let Some(t) = telemetry.as_ref() {
// Sanitize the command for analytics
let sanitized_command = sanitize_command(command);
let ua = user_agent.unwrap_or_else(|| "unknown".to_string());
// Track as an event using pop-telemetry
let data = json!({
"command": sanitized_command,
"type": "terminal_command"
});
// Track as an event
match umami
.event(
"/terminal".to_string(),
"command_typed".to_string(),
ua,
"unknown".to_string(), // hostname
"unknown".to_string(), // language
sanitized_command, // event_data (the command)
)
.await
{
Ok(_) => log::debug!("Tracked command event"),
match record_cli_command(t.clone(), "command_typed", data).await {
Ok(_) => log::debug!("Tracked command event: {}", sanitized_command),
Err(e) => log::warn!("Failed to track command event: {:?}", e),
}
}
Ok(())
}
/// Track a page view
///
/// # Arguments
/// * `path` - The page path
/// * `user_agent` - Optional user agent string
pub async fn track_pageview(&self, path: &str, user_agent: Option<String>) -> Result<()> {
/// * `_user_agent` - Optional user agent string (not used with pop-telemetry)
pub async fn track_pageview(&self, path: &str, _user_agent: Option<String>) {
if !self.enabled {
return Ok(());
return;
}
let client = self.client.lock().await;
let telemetry = self.telemetry.lock().await;
if let Some(umami) = client.as_ref() {
let ua = user_agent.unwrap_or_else(|| "unknown".to_string());
if let Some(t) = telemetry.as_ref() {
let data = json!({
"path": path,
"type": "pageview"
});
match umami
.pageview(
path.to_string(),
"pageview".to_string(),
ua,
"unknown".to_string(), // hostname
"unknown".to_string(), // language
)
.await
{
match record_cli_command(t.clone(), "pageview", data).await {
Ok(_) => log::debug!("Tracked pageview: {}", path),
Err(e) => log::warn!("Failed to track pageview: {:?}", e),
}
}
Ok(())
}
/// Track a terminal session start
pub async fn track_session_start(&self, user_agent: Option<String>) -> Result<()> {
pub async fn track_session_start(&self, _user_agent: Option<String>) {
if !self.enabled {
return Ok(());
return;
}
let client = self.client.lock().await;
let telemetry = self.telemetry.lock().await;
if let Some(umami) = client.as_ref() {
let ua = user_agent.unwrap_or_else(|| "unknown".to_string());
if let Some(t) = telemetry.as_ref() {
let data = json!({
"event": "session_start",
"type": "terminal_session"
});
match umami
.event(
"/terminal".to_string(),
"session_start".to_string(),
ua,
"unknown".to_string(),
"unknown".to_string(),
"terminal_session".to_string(),
)
.await
{
match record_cli_command(t.clone(), "session_start", data).await {
Ok(_) => log::debug!("Tracked session start"),
Err(e) => log::warn!("Failed to track session start: {:?}", e),
}
}
Ok(())
}
/// Track a terminal session end
pub async fn track_session_end(&self, user_agent: Option<String>) -> Result<()> {
pub async fn track_session_end(&self, _user_agent: Option<String>) {
if !self.enabled {
return Ok(());
return;
}
let client = self.client.lock().await;
let telemetry = self.telemetry.lock().await;
if let Some(umami) = client.as_ref() {
let ua = user_agent.unwrap_or_else(|| "unknown".to_string());
if let Some(t) = telemetry.as_ref() {
let data = json!({
"event": "session_end",
"type": "terminal_session"
});
match umami
.event(
"/terminal".to_string(),
"session_end".to_string(),
ua,
"unknown".to_string(),
"unknown".to_string(),
"terminal_session".to_string(),
)
.await
{
match record_cli_command(t.clone(), "session_end", data).await {
Ok(_) => log::debug!("Tracked session end"),
Err(e) => log::warn!("Failed to track session end: {:?}", e),
}
}
Ok(())
}
}
impl Clone for Analytics {
fn clone(&self) -> Self {
Self {
client: Arc::clone(&self.client),
telemetry: Arc::clone(&self.telemetry),
enabled: self.enabled,
}
}
@@ -212,19 +177,19 @@ fn sanitize_command(command: &str) -> String {
];
if sensitive_commands.iter().any(|&cmd| base_cmd.contains(cmd)) {
return format!("{} [REDACTED]", base_cmd);
return format!("{}_REDACTED", base_cmd);
}
// For common safe commands, keep the command and count of args
let safe_commands = [
"ls", "cd", "pwd", "cat", "less", "more", "head", "tail", "echo", "grep", "find", "which",
"whoami", "date", "cal", "clear", "exit", "history", "man", "help", "top", "htop", "ps",
"kill", "df", "du", "free", "uptime", "uname",
"kill", "df", "du", "free", "uptime", "uname", "socktop",
];
if safe_commands.contains(&base_cmd) {
if words.len() > 1 {
return format!("{} +{} args", base_cmd, words.len() - 1);
return format!("{}_with_{}_args", base_cmd, words.len() - 1);
} else {
return base_cmd.to_string();
}
@@ -247,23 +212,25 @@ mod tests {
#[test]
fn test_sanitize_safe_commands() {
assert_eq!(sanitize_command("ls"), "ls");
assert_eq!(sanitize_command("ls -la"), "ls +1 args");
assert_eq!(sanitize_command("cd /tmp"), "cd +1 args");
assert_eq!(sanitize_command("ls -la"), "ls_with_1_args");
assert_eq!(sanitize_command("cd /tmp"), "cd_with_1_args");
assert_eq!(sanitize_command("pwd"), "pwd");
assert_eq!(sanitize_command("socktop"), "socktop");
assert_eq!(sanitize_command("socktop -P local"), "socktop_with_2_args");
}
#[test]
fn test_sanitize_sensitive_commands() {
assert_eq!(sanitize_command("ssh user@host"), "ssh [REDACTED]");
assert_eq!(sanitize_command("ssh user@host"), "ssh_REDACTED");
assert_eq!(
sanitize_command("mysql -u root -p password"),
"mysql [REDACTED]"
"mysql_REDACTED"
);
assert_eq!(
sanitize_command("curl https://api.com/secret"),
"curl [REDACTED]"
"curl_REDACTED"
);
assert_eq!(sanitize_command("sudo rm -rf /"), "sudo [REDACTED]");
assert_eq!(sanitize_command("sudo rm -rf /"), "sudo_REDACTED");
}
#[test]
@@ -281,7 +248,7 @@ mod tests {
#[tokio::test]
async fn test_track_command_disabled() {
let analytics = Analytics::disabled();
let result = analytics.track_command("ls -la", None).await;
assert!(result.is_ok());
// Should not panic or error when disabled
analytics.track_command("ls -la", None).await;
}
}
+19 -13
View File
@@ -2,6 +2,7 @@ use actix_web::{App, HttpServer};
use clap::Parser;
use webterm::{validate_command, Analytics, WebTermExt};
use std::path::PathBuf;
use std::process::Command;
#[derive(Parser, Debug)]
@@ -24,13 +25,9 @@ struct Opt {
#[arg(long, default_value = "true")]
enable_analytics: bool,
/// Umami instance endpoint
#[arg(long, default_value = "http://unami.wittyoneoff.com")]
umami_endpoint: String,
/// Umami website ID
#[arg(long, default_value = "caefa16f-86af-4835-8b82-c8649aea0e2a")]
umami_website_id: String,
/// Path to telemetry config file (for opt-out management)
#[arg(long)]
telemetry_config: Option<PathBuf>,
}
#[actix_web::main]
@@ -63,12 +60,21 @@ async fn main() -> std::io::Result<()> {
// Initialize analytics
let analytics = if opt.enable_analytics {
log::info!(
"Analytics enabled: {} (website_id: {})",
opt.umami_endpoint,
opt.umami_website_id
);
Analytics::new(opt.umami_website_id.clone(), opt.umami_endpoint.clone())
// Use default config path if not specified
let config_path = opt.telemetry_config.unwrap_or_else(|| {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("webterm")
.join("telemetry.json")
});
// Create config directory if it doesn't exist
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent).ok();
}
log::info!("Analytics enabled (config: {:?})", config_path);
Analytics::new(config_path)
} else {
log::info!("Analytics disabled");
Analytics::disabled()