hotfix for telemetry hotfix for profiles config
Some checks failed
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:
jasonwitty 2025-11-30 03:34:08 -08:00
parent d3f95b8c52
commit e0535a033b
8 changed files with 378 additions and 228 deletions

335
Cargo.lock generated
View File

@ -77,7 +77,7 @@ dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"base64 0.22.1",
"base64",
"bitflags 2.10.0",
"brotli",
"bytes",
@ -88,7 +88,7 @@ dependencies = [
"foldhash",
"futures-core",
"h2",
"http",
"http 0.2.12",
"httparse",
"httpdate",
"itoa",
@ -124,7 +124,7 @@ checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
dependencies = [
"bytestring",
"cfg-if 1.0.4",
"http",
"http 0.2.12",
"regex",
"regex-lite",
"serde",
@ -349,18 +349,18 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@ -676,6 +676,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -909,6 +930,17 @@ dependencies = [
"version_check 0.9.5",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if 1.0.4",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@ -932,7 +964,7 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
@ -980,13 +1012,35 @@ dependencies = [
]
[[package]]
name = "http-body"
version = "0.4.6"
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"http",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.4.0",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body",
"pin-project-lite",
]
@ -1010,39 +1064,63 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.32"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http 1.4.0",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"pin-utils",
"smallvec",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.4.0",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
]
[[package]]
@ -1184,6 +1262,16 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@ -1226,7 +1314,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"getrandom 0.3.4",
"libc",
]
@ -1258,6 +1346,16 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1463,6 +1561,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1553,6 +1657,22 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "pop-telemetry"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36586f435a76531d01477ce20b10f18d79ffde9ec1bbb7ff0c91640476b11877"
dependencies = [
"dirs",
"env_logger",
"log",
"reqwest",
"serde",
"serde_json",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
@ -1586,7 +1706,7 @@ dependencies = [
"shared_library",
"shell-words",
"winapi",
"winreg 0.10.1",
"winreg",
]
[[package]]
@ -1663,7 +1783,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
"getrandom 0.3.4",
]
[[package]]
@ -1675,6 +1795,17 @@ dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 1.0.69",
]
[[package]]
name = "regex"
version = "1.12.2"
@ -1712,42 +1843,42 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.11.27"
version = "0.12.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64 0.21.7",
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http 1.4.0",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"ipnet",
"hyper-util",
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"winreg 0.50.0",
]
[[package]]
@ -1764,12 +1895,12 @@ dependencies = [
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
name = "rustls-pki-types"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"base64 0.21.7",
"zeroize",
]
[[package]]
@ -1858,6 +1989,7 @@ version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
@ -2035,9 +2167,12 @@ dependencies = [
[[package]]
name = "sync_wrapper"
version = "0.1.2"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
@ -2050,27 +2185,6 @@ dependencies = [
"syn",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.23.0"
@ -2078,7 +2192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@ -2225,6 +2339,45 @@ dependencies = [
"tokio",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
"http 1.4.0",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
@ -2281,18 +2434,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "umami_metrics"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc9ec451bb0504e32cafb076fe46e0126c70ad167846e3de02f0a2bbebc6839"
dependencies = [
"anyhow",
"reqwest",
"serde",
"serde_json",
]
[[package]]
name = "unicase"
version = "2.4.0"
@ -2444,6 +2585,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.83"
@ -2463,21 +2617,20 @@ dependencies = [
"actix-rt",
"actix-web",
"actix-web-actors",
"anyhow",
"bytes",
"clap",
"dirs",
"env_logger",
"futures",
"handlebars",
"libc",
"log",
"pop-telemetry",
"portable-pty",
"reqwest",
"serde",
"serde_json",
"tokio",
"tokio-util",
"umami_metrics",
]
[[package]]
@ -2739,16 +2892,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if 1.0.4",
"windows-sys 0.48.0",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
@ -2825,6 +2968,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"

View File

@ -29,9 +29,8 @@ bytes = "1.9"
log = "0.4"
env_logger = "0.11"
libc = "0.2"
umami_metrics = "0.1.0"
reqwest = { version = "0.11", features = ["json"] }
anyhow = "1.0"
pop-telemetry = "0.12.1"
dirs = "5.0"
[lib]
name = "webterm"

View File

@ -4,7 +4,7 @@
# ============================================================================
# Stage 1: Rust Builder
# ============================================================================
FROM rust:1.90-slim-bookworm AS rust-builder
FROM rust:1.91-slim-bookworm AS rust-builder
WORKDIR /build
@ -110,7 +110,8 @@ COPY --from=node-builder /build/node_modules ./node_modules
# Copy runtime scripts
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/init-config.sh /init-config.sh
RUN chmod +x /entrypoint.sh /init-config.sh
COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell.sh
RUN chmod +x /entrypoint.sh /init-config.sh /usr/local/bin/restricted-shell.sh
# Expose ports
# 8082 - webterm HTTP server
@ -124,5 +125,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
# Set entrypoint (init-config.sh runs as root, copies configs, then switches to socktop user)
ENTRYPOINT ["/init-config.sh"]
# Default command - pass through init-config.sh to entrypoint.sh to webterm-server
CMD ["/entrypoint.sh", "webterm-server", "--host", "0.0.0.0", "--port", "8082"]
# Default command - use restricted shell that only allows socktop commands
CMD ["/entrypoint.sh", "webterm-server", "--host", "0.0.0.0", "--port", "8082", "--command", "/usr/local/bin/restricted-shell.sh"]

View File

@ -6,11 +6,10 @@ services:
container_name: socktop-webterm
restart: unless-stopped
# Use host network mode for direct access to host network
# This allows the container to reach your Pis on port 8443
# Note: The containerized socktop-agent runs on port 3001 (not 3000)
# to avoid conflicts with any agent running on the host machine
network_mode: "host"
# Standard bridge networking
ports:
- "8082:8082" # Webterm HTTP server
- "3001:3001" # Socktop agent (optional)
volumes:
# Mount configuration files directly to proper locations

View File

@ -56,10 +56,23 @@ setup_alacritty() {
# Start socktop agent
start_socktop_agent() {
echo "Starting socktop-agent on port 3000..."
echo "Starting socktop-agent on port 3001..."
# Don't start the agent here - supervisor will handle it
echo "socktop-agent will be started by supervisor"
# Start socktop-agent in the background on port 3001
/usr/bin/socktop_agent --port 3001 > /tmp/socktop-agent.log 2>&1 &
AGENT_PID=$!
echo "socktop-agent started (PID: $AGENT_PID)"
# Give it a moment to start
sleep 1
# Check if it's running
if kill -0 $AGENT_PID 2>/dev/null; then
echo " ✓ socktop-agent is running on port 3001"
else
echo " ⚠ socktop-agent may have failed to start (check /tmp/socktop-agent.log)"
fi
}
# Main initialization
@ -82,7 +95,7 @@ main() {
echo ""
echo "Services:"
echo " - Webterm: http://localhost:8082"
echo " - Socktop Agent: localhost:3001"
echo " - Socktop Agent: ws://localhost:3001/ws"
echo ""
# Execute the main command

View File

@ -13,9 +13,18 @@ echo "==================================="
SOCKTOP_HOME=$(eval echo ~socktop)
echo "Socktop HOME: ${SOCKTOP_HOME}"
# Create necessary directories in the actual HOME
mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs"
mkdir -p "${SOCKTOP_HOME}/.config/alacritty"
# Check if we're running as root
if [ "$(id -u)" -eq 0 ]; then
echo "Running as root, will set permissions"
# Create necessary directories in the actual HOME
mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs"
mkdir -p "${SOCKTOP_HOME}/.config/alacritty"
else
echo "Running as non-root user ($(id -u)), creating directories without root"
# Try to create directories - will work if HOME is writable
mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs" 2>/dev/null || echo " ⚠ Could not create directories (may already exist)"
mkdir -p "${SOCKTOP_HOME}/.config/alacritty" 2>/dev/null || true
fi
# Copy files from mounted locations to actual HOME if they exist
echo "Copying configuration files..."
@ -56,8 +65,11 @@ else
echo " No certificates directory found (optional)"
fi
# Set proper ownership
chown -R socktop:socktop "${SOCKTOP_HOME}/.config"
# Set proper ownership (only if running as root)
if [ "$(id -u)" -eq 0 ]; then
chown -R socktop:socktop "${SOCKTOP_HOME}/.config"
echo " ✓ Set ownership to socktop:socktop"
fi
# Fix paths in profiles.json if it exists
if [ -f "${SOCKTOP_HOME}/.config/socktop/profiles.json" ]; then
@ -70,7 +82,11 @@ fi
echo "==================================="
echo "Configuration initialization complete"
echo "==================================="
echo "Switching to socktop user..."
# Switch to socktop user and execute the main command
exec runuser -u socktop -- "$@"
# Switch to socktop user only if running as root
if [ "$(id -u)" -eq 0 ]; then
echo "Switching to socktop user..."
exec runuser -u socktop -- "$@"
else
echo "Already running as non-root user ($(whoami)), continuing..."
exec "$@"
fi

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;
}
}

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()