hotfix for telemetry hotfix for profiles config
This commit is contained in:
+64
-97
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user