From cae8390b546c5ce5d613d99ebbe76b973984ee31 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Tue, 21 Jul 2026 23:40:39 +0200 Subject: [PATCH] feat(telemetry): Implement structured telemetry logging and reporting Adds comprehensive client-side telemetry capabilities for usage, errors, and performance metrics. This includes integrating OpenTelemetry standards into both the frontend (Svelte) and backend (Tauri/Rust) layers to capture events, spans, and system resource utilization. The implementation ensures that all collected data is privacy-filtered by design, explicitly excluding sensitive information like repository paths, credentials, source code, or email addresses from being logged. - Updates README with detailed SigNoz telemetry guide - Adds process metrics collection (CPU/Memory) in Rust backend - Exposes `setTelemetryEnabled` state management to the frontend --- README.md | 12 +- src-tauri/Cargo.lock | 375 ++++++++++++------ src-tauri/Cargo.toml | 2 + src-tauri/src/main.rs | 8 +- src-tauri/src/telemetry.rs | 363 +++++++++++++++++ src/App.svelte | 15 +- src/lib/analytics.ts | 3 + .../components/AnalyticsNoticeDialog.svelte | 6 +- src/lib/components/AppSettingsDialog.svelte | 2 +- src/lib/git.ts | 2 +- src/lib/telemetry.ts | 72 ++++ src/main.ts | 3 + 12 files changed, 728 insertions(+), 135 deletions(-) create mode 100644 src-tauri/src/telemetry.rs create mode 100644 src/lib/telemetry.ts diff --git a/README.md b/README.md index 6accf3b..273b981 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,14 @@ Contributions, feedback and feature requests are always welcome! ## 📄 License -MIT License \ No newline at end of file +MIT License + +--- + +## SigNoz telemetry + +After the user opts in, Gitty sends privacy-filtered product events and technical errors as OTLP/HTTP JSON logs. Every Git/Tauri command is also captured as a trace span with its command name, duration and success state, but without command arguments or results. CPU utilization and resident memory usage of the Gitty process are sampled every 30 seconds and exported as OpenTelemetry gauges. The default endpoints are `https://telemetry.cbsk-tech.de/v1/logs`, `https://telemetry.cbsk-tech.de/v1/traces`, and `https://telemetry.cbsk-tech.de/v1/metrics`. + +The SigNoz reverse proxy must forward all three paths to the Collector's OTLP/HTTP receiver (normally port `4318`). For development or a separate collector hostname, set `GITTY_OTLP_LOGS_ENDPOINT`, `GITTY_OTLP_TRACES_ENDPOINT`, and `GITTY_OTLP_METRICS_ENDPOINT` to the full signal URLs before starting Gitty. + +Telemetry is non-blocking, bounded, disabled until consent is stored, and never includes repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, source code, URLs, or email addresses. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9e300b4..c4e8b37 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -574,13 +574,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2", + "objc2 0.6.4", ] [[package]] @@ -1119,6 +1128,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" @@ -1592,9 +1614,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.0", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -2538,8 +2560,10 @@ dependencies = [ "commit_ai", "keyring", "log", + "reqwest 0.12.28", "serde", "serde_json", + "sysinfo 0.38.3", "tauri", "tauri-build", "tauri-plugin-aptabase", @@ -3884,7 +3908,7 @@ dependencies = [ "statrs", "strum 0.27.2", "symphonia", - "sysinfo", + "sysinfo 0.36.1", "thiserror 2.0.18", "tokenizers 0.21.4", "tokio", @@ -4014,10 +4038,10 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", "once_cell", "png 0.18.1", "serde", @@ -4311,6 +4335,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.4" @@ -4323,92 +4363,80 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.13.0", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-cloud-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ "bitflags 2.13.0", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-core-data" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-core-foundation" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.13.0", "dispatch2", - "objc2", + "objc2 0.6.4", ] [[package]] name = "objc2-core-graphics" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.13.0", "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] [[package]] name = "objc2-core-image" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-core-location" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", + "objc2 0.6.4", + "objc2-foundation 0.3.1", ] [[package]] @@ -4428,22 +4456,34 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.13.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", "objc2-core-foundation", ] [[package]] name = "objc2-io-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" dependencies = [ "libc", "objc2-core-foundation", @@ -4451,82 +4491,106 @@ dependencies = [ [[package]] name = "objc2-io-surface" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.13.0", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-osa-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ "bitflags 2.13.0", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-quartz-core" -version = "0.3.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.0", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-ui-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ "bitflags 2.13.0", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", "objc2-core-image", "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", "objc2-user-notifications", ] [[package]] name = "objc2-user-notifications" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-web-kit" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ "bitflags 2.13.0", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", ] [[package]] @@ -4664,8 +4728,8 @@ dependencies = [ "android_system_properties", "log", "nix 0.31.3", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", "objc2-ui-kit", "serde", "windows-sys 0.61.2", @@ -4677,8 +4741,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.1", "objc2-osa-kit", "serde", "serde_json", @@ -5540,17 +5604,17 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "block2", + "block2 0.6.2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -6358,24 +6422,24 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.8" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "foreign-types 0.5.0", "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", "raw-window-handle", "redox_syscall", - "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6772,7 +6836,21 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.61.3", +] + +[[package]] +name = "sysinfo" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03c61d2a49c649a15c407338afe7accafde9dac869995dccb73e5f7ef7d9034" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", ] [[package]] @@ -6816,9 +6894,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.0", - "block2", + "block2 0.6.2", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", "dbus", "dispatch2", @@ -6832,9 +6910,9 @@ dependencies = [ "log", "ndk", "ndk-sys", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", "objc2-ui-kit", "once_cell", "parking_lot", @@ -6843,7 +6921,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -6899,9 +6977,9 @@ dependencies = [ "log", "mime", "muda", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -6925,7 +7003,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -7054,7 +7132,7 @@ dependencies = [ "dunce", "glob", "log", - "objc2-foundation", + "objc2-foundation 0.3.1", "percent-encoding", "schemars 0.8.22", "serde", @@ -7127,7 +7205,7 @@ dependencies = [ "gtk", "http", "jni 0.21.1", - "objc2", + "objc2 0.6.4", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", @@ -7138,7 +7216,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -7151,7 +7229,7 @@ dependencies = [ "http", "jni 0.21.1", "log", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "once_cell", "percent-encoding", @@ -7163,7 +7241,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -7811,11 +7889,11 @@ dependencies = [ "dirs", "libappindicator", "muda", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.1", "once_cell", "png 0.18.1", "serde", @@ -8389,7 +8467,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -8413,7 +8491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -8476,10 +8554,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -8491,11 +8569,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -8507,6 +8597,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -8541,7 +8640,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -8588,6 +8698,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -8761,6 +8881,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -9003,7 +9132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs", @@ -9017,10 +9146,10 @@ dependencies = [ "jni 0.21.1", "libc", "ndk", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.1", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -9034,7 +9163,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f81922f..6fbb8d6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,6 +22,8 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "async- commit_ai = { path = "crates/commit_ai" } tokio = "1.52.3" log = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +sysinfo = { version = "=0.38.3", default-features = false, features = ["system"] } [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8e0d866..a41858c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,6 +2,7 @@ mod badge; mod git; +mod telemetry; use badge::set_sync_badge; use git::{ @@ -24,6 +25,7 @@ use git::{ unstage_files, update_remote, }; use tauri::Manager; +use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled}; struct ConsoleLogger; @@ -78,6 +80,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> { #[tokio::main] async fn main() { init_console_logging(); + telemetry::init(); if let Some(result) = run_sequence_editor_if_requested() { if let Err(error) = result { eprintln!("{error}"); @@ -193,7 +196,10 @@ async fn main() { cred_save, cred_delete, set_sync_badge, - close_splashscreen + close_splashscreen, + set_telemetry_enabled, + emit_frontend_log, + emit_frontend_span ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/telemetry.rs b/src-tauri/src/telemetry.rs new file mode 100644 index 0000000..a210c66 --- /dev/null +++ b/src-tauri/src/telemetry.rs @@ -0,0 +1,363 @@ +use std::{ + sync::{ + OnceLock, + atomic::{AtomicBool, Ordering}, + mpsc::{SyncSender, sync_channel}, + }, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use serde::Deserialize; +use serde_json::json; + +const DEFAULT_OTLP_LOGS_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/logs"; +const DEFAULT_OTLP_TRACES_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/traces"; +const DEFAULT_OTLP_METRICS_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/metrics"; +const METRICS_INTERVAL: Duration = Duration::from_secs(30); +const MAX_BODY_BYTES: usize = 2_048; + +static ENABLED: AtomicBool = AtomicBool::new(false); +static SENDER: OnceLock> = OnceLock::new(); + +#[derive(Debug)] +enum TelemetrySignal { + Log(TelemetryRecord), + Span(FrontendSpan), + Metrics(ProcessMetrics), +} + +#[derive(Debug)] +struct ProcessMetrics { + cpu_utilization: f64, + memory_bytes: u64, +} + +#[derive(Debug)] +struct TelemetryRecord { + severity: &'static str, + severity_number: u8, + body: String, + event_name: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FrontendLog { + level: String, + message: String, + event_name: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FrontendSpan { + name: String, + trace_id: String, + span_id: String, + started_at_ms: u64, + duration_ms: f64, + success: bool, +} + +pub fn init() { + let endpoint = std::env::var("GITTY_OTLP_LOGS_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_OTLP_LOGS_ENDPOINT.to_string()); + let traces_endpoint = std::env::var("GITTY_OTLP_TRACES_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_OTLP_TRACES_ENDPOINT.to_string()); + let metrics_endpoint = std::env::var("GITTY_OTLP_METRICS_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_OTLP_METRICS_ENDPOINT.to_string()); + let (sender, receiver) = sync_channel::(256); + if SENDER.set(sender.clone()).is_err() { + return; + } + + start_process_metrics(sender); + + std::thread::Builder::new() + .name("gitty-telemetry".into()) + .spawn(move || { + let client = match reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + { + Ok(client) => client, + Err(error) => { + eprintln!("[WARN] [gitty::telemetry] could not create OTLP client: {error}"); + return; + } + }; + + let mut export_warning_shown = false; + while let Ok(signal) = receiver.recv() { + let (target_endpoint, payload) = match signal { + TelemetrySignal::Log(record) => (&endpoint, log_payload(record)), + TelemetrySignal::Span(span) => (&traces_endpoint, span_payload(span)), + TelemetrySignal::Metrics(metrics) => { + (&metrics_endpoint, metrics_payload(metrics)) + } + }; + export( + &client, + target_endpoint, + &payload, + &mut export_warning_shown, + ); + } + }) + .expect("failed to start telemetry worker"); +} + +fn start_process_metrics(sender: SyncSender) { + std::thread::Builder::new() + .name("gitty-process-metrics".into()) + .spawn(move || { + use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; + + let Ok(pid) = sysinfo::get_current_pid() else { + return; + }; + let refresh_kind = ProcessRefreshKind::nothing().with_cpu().with_memory(); + let cpu_count = std::thread::available_parallelism() + .map(|count| count.get()) + .unwrap_or(1) as f64; + let mut system = System::new(); + let mut primed = false; + + loop { + std::thread::sleep(METRICS_INTERVAL); + if !ENABLED.load(Ordering::Relaxed) { + primed = false; + continue; + } + system.refresh_processes_specifics( + ProcessesToUpdate::Some(&[pid]), + true, + refresh_kind, + ); + let Some(process) = system.process(pid) else { + continue; + }; + if !primed { + primed = true; + continue; + } + let metrics = ProcessMetrics { + cpu_utilization: (f64::from(process.cpu_usage()) / (100.0 * cpu_count)) + .clamp(0.0, 1.0), + memory_bytes: process.memory(), + }; + let _ = sender.try_send(TelemetrySignal::Metrics(metrics)); + } + }) + .expect("failed to start process metrics worker"); +} + +fn resource_attributes() -> serde_json::Value { + json!([ + { "key": "service.name", "value": { "stringValue": "gitty-desktop" } }, + { "key": "service.version", "value": { "stringValue": env!("CARGO_PKG_VERSION") } }, + { "key": "deployment.environment.name", "value": { "stringValue": if cfg!(debug_assertions) { "development" } else { "production" } } }, + { "key": "os.type", "value": { "stringValue": std::env::consts::OS } }, + { "key": "host.arch", "value": { "stringValue": std::env::consts::ARCH } } + ]) +} + +fn log_payload(record: TelemetryRecord) -> serde_json::Value { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .to_string(); + let mut attributes = vec![json!({ + "key": "telemetry.sdk.language", + "value": { "stringValue": "rust" } + })]; + if let Some(event_name) = record.event_name { + attributes.push(json!({ + "key": "event.name", + "value": { "stringValue": event_name } + })); + } + let payload = json!({ + "resourceLogs": [{ + "resource": { "attributes": resource_attributes() }, + "scopeLogs": [{ + "scope": { "name": "gitty.telemetry" }, + "logRecords": [{ + "timeUnixNano": timestamp, + "observedTimeUnixNano": timestamp, + "severityNumber": record.severity_number, + "severityText": record.severity, + "body": { "stringValue": record.body }, + "attributes": attributes + }] + }] + }] + }); + payload +} + +fn span_payload(mut span: FrontendSpan) -> serde_json::Value { + if span.name.len() > 128 { + span.name.truncate(128); + } + let start = u128::from(span.started_at_ms) * 1_000_000; + let duration = (span.duration_ms.max(0.0) * 1_000_000.0) as u128; + let attributes = vec![ + json!({ "key": "rpc.system", "value": { "stringValue": "tauri" } }), + json!({ "key": "rpc.method", "value": { "stringValue": span.name } }), + ]; + json!({ + "resourceSpans": [{ + "resource": { "attributes": resource_attributes() }, + "scopeSpans": [{ + "scope": { "name": "gitty.tauri", "version": env!("CARGO_PKG_VERSION") }, + "spans": [{ + "traceId": span.trace_id, + "spanId": span.span_id, + "name": format!("tauri.{}", span.name), + "kind": 1, + "startTimeUnixNano": start.to_string(), + "endTimeUnixNano": (start + duration).to_string(), + "attributes": attributes, + "status": { + "code": if span.success { 1 } else { 2 }, + "message": if span.success { "" } else { "Tauri command failed" } + } + }] + }] + }] + }) +} + +fn metrics_payload(metrics: ProcessMetrics) -> serde_json::Value { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .to_string(); + json!({ + "resourceMetrics": [{ + "resource": { "attributes": resource_attributes() }, + "scopeMetrics": [{ + "scope": { "name": "gitty.process", "version": env!("CARGO_PKG_VERSION") }, + "metrics": [ + { + "name": "process.cpu.utilization", + "description": "Normalized CPU utilization of the Gitty process", + "unit": "1", + "gauge": { "dataPoints": [{ + "timeUnixNano": timestamp, + "asDouble": metrics.cpu_utilization, + "attributes": [] + }] } + }, + { + "name": "process.memory.usage", + "description": "Resident memory used by the Gitty process", + "unit": "By", + "gauge": { "dataPoints": [{ + "timeUnixNano": timestamp, + "asInt": metrics.memory_bytes.to_string(), + "attributes": [] + }] } + } + ] + }] + }] + }) +} + +fn export( + client: &reqwest::blocking::Client, + endpoint: &str, + payload: &serde_json::Value, + export_warning_shown: &mut bool, +) { + match client.post(endpoint).json(payload).send() { + Ok(response) if response.status().is_success() => { + if response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("text/html")) + && !*export_warning_shown + { + eprintln!( + "[WARN] [gitty::telemetry] OTLP endpoint returned HTML; configure the collector endpoint with GITTY_OTLP_LOGS_ENDPOINT" + ); + *export_warning_shown = true; + } + } + Ok(response) if !*export_warning_shown => { + eprintln!( + "[WARN] [gitty::telemetry] OTLP export failed with status {}", + response.status() + ); + *export_warning_shown = true; + } + Err(error) if !*export_warning_shown => { + eprintln!("[WARN] [gitty::telemetry] OTLP export failed: {error}"); + *export_warning_shown = true; + } + _ => {} + } +} + +fn emit(record: TelemetryRecord) { + if !ENABLED.load(Ordering::Relaxed) { + return; + } + if let Some(sender) = SENDER.get() { + let _ = sender.try_send(TelemetrySignal::Log(record)); + } +} + +#[tauri::command] +pub fn set_telemetry_enabled(enabled: bool) { + ENABLED.store(enabled, Ordering::Relaxed); + if enabled { + emit(TelemetryRecord { + severity: "INFO", + severity_number: 9, + body: "Telemetry enabled".into(), + event_name: Some("telemetry.enabled".into()), + }); + } +} + +#[tauri::command] +pub fn emit_frontend_log(log: FrontendLog) { + let (severity, severity_number) = match log.level.as_str() { + "error" => ("ERROR", 17), + "warn" => ("WARN", 13), + _ => ("INFO", 9), + }; + let mut body = log.message; + if body.len() > MAX_BODY_BYTES { + body.truncate(MAX_BODY_BYTES); + } + emit(TelemetryRecord { + severity, + severity_number, + body, + event_name: log.event_name, + }); +} + +#[tauri::command] +pub fn emit_frontend_span(span: FrontendSpan) { + if !ENABLED.load(Ordering::Relaxed) { + return; + } + if span.trace_id.len() != 32 + || span.span_id.len() != 16 + || !span.trace_id.bytes().all(|byte| byte.is_ascii_hexdigit()) + || !span.span_id.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return; + } + if let Some(sender) = SENDER.get() { + let _ = sender.try_send(TelemetrySignal::Span(span)); + } +} diff --git a/src/App.svelte b/src/App.svelte index e50650e..8b49f8a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,7 +1,6 @@